diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js index f2a042c..6f3c495 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/app.js @@ -3224,13 +3224,16 @@ function sizeAprsMapToViewport() { const mapEl = document.getElementById("aprs-map"); if (!mapEl) return; const mapRect = mapEl.getBoundingClientRect(); + const width = mapEl.clientWidth || mapRect.width; const footer = document.querySelector(".footer"); let bottom = window.innerHeight; if (footer) { const fr = footer.getBoundingClientRect(); if (fr.top > mapRect.top + 50) bottom = fr.top; } - const target = Math.max(150, Math.floor(bottom - mapRect.top - 8)); + const available = Math.max(0, Math.floor(bottom - mapRect.top - 8)); + const widthDriven = width > 0 ? Math.floor(width / 1.55) : available; + const target = Math.max(0, Math.min(available, widthDriven)); mapEl.style.height = `${target}px`; if (aprsMap) aprsMap.invalidateSize(); } @@ -3489,6 +3492,47 @@ function ensureAisTrack(mmsi, entry) { entry.track = track; } +function aisMarkerOptionsFromMessage(msg) { + return { + heading: msg?.heading_deg, + course: msg?.cog_deg, + speed: msg?.sog_knots, + color: "#ff7559", + outline: "#6b2118", + size: 22, + }; +} + +function createAisMarker(lat, lon, msg) { + if (typeof L !== "undefined" && typeof L.trxAisTrackSymbol === "function") { + return L.trxAisTrackSymbol([lat, lon], aisMarkerOptionsFromMessage(msg)); + } + return L.circleMarker([lat, lon], { + radius: 6, + color: "#e2553d", + fillColor: "#ff7559", + fillOpacity: 0.82, + }); +} + +function updateAisMarker(marker, msg, popupHtml) { + if (!marker) return; + marker.setLatLng([msg.lat, msg.lon]); + if (typeof marker.setAisState === "function") { + marker.setAisState(aisMarkerOptionsFromMessage(msg)); + } + if (typeof marker.setStyle === "function" && typeof marker.setAisState !== "function") { + const hasHeading = Number.isFinite(msg?.heading_deg) || Number.isFinite(msg?.cog_deg); + marker.setStyle({ + radius: hasHeading ? 6.5 : 6, + color: hasHeading ? "#c8412f" : "#e2553d", + fillColor: hasHeading ? "#ff6f4d" : "#ff7559", + fillOpacity: 0.84, + }); + } + marker.setPopupContent(popupHtml); +} + window.aisMapAddVessel = function(msg) { if (msg == null || msg.lat == null || msg.lon == null || !Number.isFinite(msg.mmsi)) return; if (!aprsMap) initAprsMap(); @@ -3508,18 +3552,12 @@ window.aisMapAddVessel = function(msg) { ensureAisTrack(key, existing); } if (existing.marker) { - existing.marker.setLatLng([msg.lat, msg.lon]); - existing.marker.setPopupContent(popupHtml); + updateAisMarker(existing.marker, msg, popupHtml); } return; } if (!aprsMap) return; - const marker = L.circleMarker([msg.lat, msg.lon], { - radius: 6, - color: "#e2553d", - fillColor: "#ff7559", - fillOpacity: 0.82, - }).addTo(aprsMap).bindPopup(popupHtml); + const marker = createAisMarker(msg.lat, msg.lon, msg).addTo(aprsMap).bindPopup(popupHtml); marker.__trxType = "ais"; marker._aisMmsi = key; mapMarkers.add(marker); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html index 2b9a54c..516ff27 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/index.html @@ -11,6 +11,7 @@ +
diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/leaflet-ais-tracksymbol.js b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/leaflet-ais-tracksymbol.js new file mode 100644 index 0000000..94b704d --- /dev/null +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/leaflet-ais-tracksymbol.js @@ -0,0 +1,118 @@ +(function() { + if (typeof L === "undefined") return; + + function clamp(value, min, max) { + return Math.max(min, Math.min(max, value)); + } + + function finiteAngle(value) { + if (!Number.isFinite(value)) return null; + const normalized = ((Number(value) % 360) + 360) % 360; + return normalized; + } + + function svgColor(value, fallback) { + const text = String(value || fallback || ""); + return text.replace(/"/g, """); + } + + function buildSymbolHtml(options, zoom) { + const heading = finiteAngle(options.heading); + const course = finiteAngle(options.course); + const angle = heading != null ? heading : course; + const speed = Number.isFinite(options.speed) ? Math.max(0, Number(options.speed)) : 0; + const sizeBase = Number.isFinite(options.size) ? Number(options.size) : 22; + const zoomBoost = zoom >= 12 ? 4 : zoom >= 9 ? 2 : 0; + const size = clamp(sizeBase + zoomBoost, 16, 32); + const courseLen = course != null ? clamp(size * (0.55 + Math.min(speed, 30) / 30), size * 0.55, size * 1.2) : 0; + const color = svgColor(options.color, "#ff7559"); + const outline = svgColor(options.outline, "#6b2118"); + + const body = angle != null + ? `` + + `` + + `` + : ``; + + const courseLine = course != null + ? `` + + `` + + `` + : ""; + + return ( + `` + ); + } + + L.TrxAisTrackSymbol = L.Marker.extend({ + options: { + heading: null, + course: null, + speed: null, + color: "#ff7559", + outline: "#6b2118", + size: 22, + interactive: true, + keyboard: true, + riseOnHover: true, + }, + + initialize: function(latlng, options) { + const merged = L.Util.extend({}, this.options, options || {}); + merged.icon = L.divIcon({ + className: "trx-ais-track-symbol-icon", + html: "", + iconSize: [merged.size, merged.size], + iconAnchor: [merged.size / 2, merged.size / 2], + }); + L.Marker.prototype.initialize.call(this, latlng, merged); + }, + + onAdd: function(map) { + L.Marker.prototype.onAdd.call(this, map); + this._refreshIcon(); + this._boundZoomRefresh = this._refreshIcon.bind(this); + map.on("zoomend", this._boundZoomRefresh); + }, + + onRemove: function(map) { + if (this._boundZoomRefresh) { + map.off("zoomend", this._boundZoomRefresh); + this._boundZoomRefresh = null; + } + L.Marker.prototype.onRemove.call(this, map); + }, + + setAisState: function(next) { + if (next && typeof next === "object") { + if ("heading" in next) this.options.heading = next.heading; + if ("course" in next) this.options.course = next.course; + if ("speed" in next) this.options.speed = next.speed; + } + this._refreshIcon(); + return this; + }, + + _refreshIcon: function() { + if (!this._icon) return; + const zoom = this._map && typeof this._map.getZoom === "function" ? this._map.getZoom() : 0; + const html = buildSymbolHtml(this.options, zoom); + this._icon.innerHTML = html; + const sizeBase = Number.isFinite(this.options.size) ? Number(this.options.size) : 22; + const zoomBoost = zoom >= 12 ? 4 : zoom >= 9 ? 2 : 0; + const size = clamp(sizeBase + zoomBoost, 16, 32); + this._icon.style.width = `${size}px`; + this._icon.style.height = `${size}px`; + this._icon.style.marginLeft = `${-size / 2}px`; + this._icon.style.marginTop = `${-size / 2}px`; + }, + }); + + L.trxAisTrackSymbol = function(latlng, options) { + return new L.TrxAisTrackSymbol(latlng, options); + }; +})(); diff --git a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css index 3e30c35..68a74c9 100644 --- a/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css +++ b/src/trx-client/trx-frontend/trx-frontend-http/assets/web/style.css @@ -1086,7 +1086,18 @@ small { color: var(--text-muted); } .sub-tab { flex-shrink: 0; background: transparent; border: none; border-bottom: 2px solid transparent; border-radius: 0; padding: 0.35rem 0.75rem; color: var(--text-muted); cursor: pointer; font-size: 0.85rem; height: auto; } .sub-tab.active { border-bottom-color: var(--accent-green); color: var(--accent-green); font-weight: 600; } .sub-tab:hover:not(.active) { color: var(--text); } -#aprs-map { min-height: 150px; border-radius: 6px; } +#tab-map { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-height: 0; +} +#aprs-map { + flex: 0 1 auto; + width: 100%; + min-height: 0; + border-radius: 6px; +} .aprs-controls { display: flex; gap: 0.6rem; align-items: center; margin-bottom: 0.75rem; } #subtab-aprs .aprs-controls > button { min-height: 1.65rem;