From 43d4792760bdd88b94df31124159e5801520f0f7 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Sun, 22 Jun 2025 17:39:10 -0500 Subject: [PATCH] map tweaks --- assets/js/features/trail_manager.ts | 26 ++++- assets/js/map.ts | 101 +++++++++++++++----- lib/aprs_web/live/map_live/callsign_view.ex | 45 ++++++++- 3 files changed, 139 insertions(+), 33 deletions(-) diff --git a/assets/js/features/trail_manager.ts b/assets/js/features/trail_manager.ts index b7e6b8b..af847e5 100644 --- a/assets/js/features/trail_manager.ts +++ b/assets/js/features/trail_manager.ts @@ -23,12 +23,22 @@ export class TrailManager { this.trails = new Map(); this.showTrails = true; this.trailDuration = trailDuration; + + // Ensure trail layer is added to the map and visible + if (this.trailLayer && this.trailLayer.bringToFront) { + setTimeout(() => this.trailLayer.bringToFront(), 100); + } } setShowTrails(show: boolean) { this.showTrails = show; if (!show) { this.clearAllTrails(); + } else { + // When re-enabling trails, recreate all trails immediately + this.trails.forEach((trailState, baseCallsign) => { + this.updateTrailVisualization(baseCallsign, trailState, true); + }); } } @@ -77,8 +87,9 @@ export class TrailManager { }); } - // Always update trail visualization - this.updateTrailVisualization(baseCallsign, trailState); + // Always update trail visualization, with immediate=true for historical dots + // This ensures historical trails are immediately visible when loaded all at once + this.updateTrailVisualization(baseCallsign, trailState, isHistoricalDot); } private extractBaseCallsign(markerId: string | any): string { @@ -97,7 +108,11 @@ export class TrailManager { return id; } - private updateTrailVisualization(baseCallsign: string, trailState: TrailState) { + private updateTrailVisualization( + baseCallsign: string, + trailState: TrailState, + immediate: boolean = false, + ) { // Remove old trail and dots if (trailState.trail) { this.trailLayer.removeLayer(trailState.trail); @@ -113,14 +128,17 @@ export class TrailManager { const latLngs = trailState.positions.map((pos) => [pos.lat, pos.lng]); // Create blue polyline connecting the historical position dots + // For historical positions (immediate=true), use higher opacity for better visibility trailState.trail = L.polyline(latLngs, { color: "#1E90FF", weight: 3, - opacity: 0.8, + opacity: immediate ? 0.9 : 0.8, smoothFactor: 1, lineCap: "round", lineJoin: "round", className: "historical-trail-line", + // Ensure the trail renders on top of other map elements + zIndexOffset: immediate ? 1000 : 0, }).addTo(this.trailLayer); // Don't create additional dots here since historical positions are now shown as markers diff --git a/assets/js/map.ts b/assets/js/map.ts index 162eab5..64e8e36 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -305,9 +305,10 @@ let MapAPRSMap = { const currentZoom = self.map!.getZoom(); const zoomDifference = self.lastZoom ? Math.abs(currentZoom - self.lastZoom) : 0; - // If zoom changed significantly (more than 2 levels), clear and reload + // If zoom changed significantly (more than 2 levels), request reload of normal markers + // but preserve historical ones and current position if (zoomDifference > 2) { - self.pushEvent("clear_and_reload_markers", {}); + self.pushEvent("refresh_markers", {}); } self.sendBoundsToServer(); @@ -668,25 +669,15 @@ let MapAPRSMap = { self.markers!.delete(id); self.markerStates!.delete(id); - // Only remove trail if this was the only marker for this callsign - if (self.trailManager && markerState) { - const callsign = markerState.callsign_group || markerState.callsign || id; - // Check if there are any live markers left for this callsign - let hasLiveMarkers = false; - self.markerStates!.forEach((state) => { - const stateCallsign = state.callsign_group || state.callsign; - if (stateCallsign === callsign && !state.historical) { - hasLiveMarkers = true; - } - }); - - // Only remove trail if no live markers remain - if (!hasLiveMarkers) { - self.trailManager.removeTrail(callsign); - } - } + // We now NEVER remove trails when clearing historical packets + // This preserves the historical trail visibility } }); + + // Make sure trail layer is visible and on top + if (self.trailLayer && self.trailLayer.bringToFront) { + self.trailLayer.bringToFront(); + } }); // Handle bounds-based marker filtering @@ -708,7 +699,13 @@ let MapAPRSMap = { // Handle clear_all_markers event self.handleEvent("clear_all_markers", () => { + // Modified to preserve historical trails - only removes non-historical markers self.clearAllMarkers(); + + // Force trail layer to front to ensure visibility + if (self.trailLayer && self.trailLayer.bringToFront) { + self.trailLayer.bringToFront(); + } }); }, @@ -720,8 +717,9 @@ let MapAPRSMap = { const center = self.map!.getCenter(); const zoom = self.map!.getZoom(); - // Remove markers that are now outside the visible bounds - self.removeMarkersOutsideBounds(bounds); + // We're no longer removing markers outside bounds during normal panning/zooming + // to preserve historical positions and the current marker + // self.removeMarkersOutsideBounds(bounds); self.pushEvent("bounds_changed", { bounds: { @@ -825,6 +823,13 @@ let MapAPRSMap = { marker.addTo(self.markerLayer!); self.markers!.set(data.id, marker); + // Make sure historical markers and trails stay visible + if (data.historical || data.is_most_recent_for_callsign) { + if (self.trailLayer && self.trailLayer.bringToFront) { + setTimeout(() => self.trailLayer.bringToFront(), 50); + } + } + // Store marker state for optimization self.markerStates!.set(data.id, { lat: lat, @@ -923,13 +928,49 @@ let MapAPRSMap = { clearAllMarkers() { const self = this as unknown as LiveViewHookContext; - self.markerLayer!.clearLayers(); + + // Instead of clearing all markers, only remove non-historical and non-current markers + const markersToPreserve = new Map(); + const statesToPreserve = new Map(); + + // Identify historical markers and most recent markers to preserve + self.markers!.forEach((marker, id) => { + const markerState = self.markerStates!.get(String(id)); + const isHistorical = (marker as any)._isHistorical || (markerState && markerState.historical); + const isMostRecent = markerState && markerState.is_most_recent_for_callsign; + + // Keep historical markers and current position markers + if (isHistorical || isMostRecent) { + markersToPreserve.set(String(id), marker); + if (markerState) { + statesToPreserve.set(String(id), markerState); + } + } else { + // Remove only non-historical, non-current markers + self.markerLayer!.removeLayer(marker); + } + }); + + // Replace the markers and states with just the preserved ones self.markers!.clear(); self.markerStates!.clear(); - // Clear all trails - if (self.trailManager) { - self.trailManager.clearAllTrails(); + markersToPreserve.forEach((marker, id) => { + self.markers!.set(id, marker); + }); + + statesToPreserve.forEach((state, id) => { + self.markerStates!.set(id, state); + }); + + // Don't clear trails - keep them visible + // if (self.trailManager) { + // self.trailManager.clearAllTrails(); + // } + + // Make sure trail layer is on top + if (self.trailLayer && self.trailLayer.bringToFront) { + setTimeout(() => self.trailLayer.bringToFront(), 100); } }, @@ -940,6 +981,16 @@ let MapAPRSMap = { const markersToRemove: string[] = []; self.markers!.forEach((marker: L.Marker, id: string) => { + // Check if this is a historical marker or the most recent marker for a callsign + const markerState = self.markerStates!.get(String(id)); + const isHistorical = (marker as any)._isHistorical || (markerState && markerState.historical); + const isMostRecent = markerState && markerState.is_most_recent_for_callsign; + + // Always preserve historical markers and the most recent marker for a callsign + if (isHistorical || isMostRecent) { + return; // Skip this marker, don't remove it + } + const position = marker.getLatLng(); const lat = position.lat; const lng = position.lng; diff --git a/lib/aprs_web/live/map_live/callsign_view.ex b/lib/aprs_web/live/map_live/callsign_view.ex index c261c4b..8112447 100644 --- a/lib/aprs_web/live/map_live/callsign_view.ex +++ b/lib/aprs_web/live/map_live/callsign_view.ex @@ -160,7 +160,26 @@ defmodule AprsWeb.MapLive.CallsignView do |> assign(packets_loaded: true) end - # Don't start historical replay yet - wait for bounds to be available + # Immediately start historical replay without waiting for bounds + # If bounds aren't available, use a default world bounds + socket = + if is_nil(socket.assigns.map_bounds) do + # Use a default global bounds to ensure we get all historical points + default_bounds = %{ + north: 90.0, + south: -90.0, + east: 180.0, + west: -180.0 + } + + socket = assign(socket, map_bounds: default_bounds) + socket = start_historical_replay(socket) + assign(socket, replay_started: true, replay_active: true) + else + # Bounds already available, use them + socket = start_historical_replay(socket) + assign(socket, replay_started: true, replay_active: true) + end # If we have a pending geolocation, zoom to it after a delay socket = @@ -252,12 +271,30 @@ defmodule AprsWeb.MapLive.CallsignView do end def handle_info(:auto_start_replay, socket) do - if not socket.assigns.replay_started and socket.assigns.map_ready and - not is_nil(socket.assigns.map_bounds) do + # We now handle this in map_ready event, but keep this for backward compatibility + # and in case auto_start_replay is triggered from elsewhere + if socket.assigns.map_ready and not socket.assigns.replay_started do + # Update socket with bounds if needed + socket = + if is_nil(socket.assigns.map_bounds) do + # Use a default global bounds if needed + default_bounds = %{ + north: 90.0, + south: -90.0, + east: 180.0, + west: -180.0 + } + + assign(socket, map_bounds: default_bounds) + else + socket + end + socket = start_historical_replay(socket) {:noreply, assign(socket, replay_started: true, replay_active: true)} else - if not socket.assigns.map_ready or is_nil(socket.assigns.map_bounds) do + # Only retry if map isn't ready yet + if not socket.assigns.map_ready do Process.send_after(self(), :auto_start_replay, 1000) end