tweaks
This commit is contained in:
parent
43b2cb1df0
commit
eb3e30abd6
4 changed files with 565 additions and 154 deletions
264
assets/js/map.ts
264
assets/js/map.ts
|
|
@ -4,6 +4,8 @@ declare const OverlappingMarkerSpiderfier: any;
|
|||
|
||||
// Import trail management functionality
|
||||
import { TrailManager } from "./features/trail_manager";
|
||||
// Import helper functions
|
||||
import { parseTimestamp, getTrailId, saveMapState, safePushEvent, getLiveSocket } from "./map_helpers";
|
||||
|
||||
// APRS Map Hook - handles only basic map interaction
|
||||
// All data logic handled by LiveView
|
||||
|
|
@ -25,7 +27,12 @@ type LiveViewHookContext = {
|
|||
lastZoom?: number;
|
||||
currentPopupMarkerId?: string | null;
|
||||
oms?: any;
|
||||
programmaticMoveCounter?: number;
|
||||
programmaticMoveId?: string;
|
||||
programmaticMoveTimeout?: ReturnType<typeof setTimeout>;
|
||||
cleanupInterval?: ReturnType<typeof setInterval>;
|
||||
mapEventHandlers?: Map<string, Function>;
|
||||
isDestroyed?: boolean;
|
||||
popupNavigationHandler?: (e: Event) => void;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
||||
|
|
@ -275,9 +282,9 @@ let MapAPRSMap = {
|
|||
self.sendBoundsToServer();
|
||||
|
||||
// Start periodic cleanup of old trail positions (every 5 minutes)
|
||||
setInterval(
|
||||
self.cleanupInterval = setInterval(
|
||||
() => {
|
||||
if (self.trailManager) {
|
||||
if (!self.isDestroyed && self.trailManager) {
|
||||
self.trailManager.cleanupOldPositions();
|
||||
}
|
||||
},
|
||||
|
|
@ -288,50 +295,29 @@ let MapAPRSMap = {
|
|||
}
|
||||
});
|
||||
|
||||
// Track map event handlers for cleanup
|
||||
self.mapEventHandlers = new Map();
|
||||
self.isDestroyed = false;
|
||||
|
||||
// Send bounds to LiveView when map moves
|
||||
self.map!.on("moveend", () => {
|
||||
const moveEndHandler = () => {
|
||||
// Skip if this is a programmatic move from the server
|
||||
if (self.programmaticMoveCounter && self.programmaticMoveCounter > 0) {
|
||||
// Decrement counter for this programmatic move event
|
||||
self.programmaticMoveCounter--;
|
||||
if (self.programmaticMoveId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.boundsTimer) clearTimeout(self.boundsTimer);
|
||||
self.boundsTimer = setTimeout(() => {
|
||||
// Save map state and update URL
|
||||
const center = self.map.getCenter();
|
||||
const zoom = self.map.getZoom();
|
||||
|
||||
// Truncate lat/lng to 5 decimal places for URL
|
||||
const truncatedLat = Math.round(center.lat * 100000) / 100000;
|
||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||
);
|
||||
|
||||
// Send combined map state update to server for URL and bounds updating
|
||||
self.pushEvent("update_map_state", {
|
||||
center: { lat: truncatedLat, lng: truncatedLng },
|
||||
zoom: zoom,
|
||||
bounds: {
|
||||
north: self.map.getBounds().getNorth(),
|
||||
south: self.map.getBounds().getSouth(),
|
||||
east: self.map.getBounds().getEast(),
|
||||
west: self.map.getBounds().getWest(),
|
||||
}
|
||||
});
|
||||
saveMapState(self.map, self.pushEvent);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
self.map!.on("moveend", moveEndHandler);
|
||||
self.mapEventHandlers!.set("moveend", moveEndHandler);
|
||||
|
||||
// Handle zoom changes with optimization for large zoom differences
|
||||
self.map!.on("zoomend", () => {
|
||||
const zoomEndHandler = () => {
|
||||
// Skip if this is a programmatic move from the server
|
||||
if (self.programmaticMoveCounter && self.programmaticMoveCounter > 0) {
|
||||
// Decrement counter for this programmatic move event
|
||||
self.programmaticMoveCounter--;
|
||||
if (self.programmaticMoveId) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -348,31 +334,11 @@ let MapAPRSMap = {
|
|||
|
||||
self.lastZoom = currentZoom;
|
||||
// Save map state and update URL
|
||||
const center = self.map.getCenter();
|
||||
const zoom = self.map.getZoom();
|
||||
|
||||
// Truncate lat/lng to 5 decimal places for URL
|
||||
const truncatedLat = Math.round(center.lat * 100000) / 100000;
|
||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||
);
|
||||
|
||||
// Send combined map state update to server for URL and bounds updating
|
||||
self.pushEvent("update_map_state", {
|
||||
center: { lat: truncatedLat, lng: truncatedLng },
|
||||
zoom: zoom,
|
||||
bounds: {
|
||||
north: self.map.getBounds().getNorth(),
|
||||
south: self.map.getBounds().getSouth(),
|
||||
east: self.map.getBounds().getEast(),
|
||||
west: self.map.getBounds().getWest(),
|
||||
}
|
||||
});
|
||||
saveMapState(self.map, self.pushEvent);
|
||||
}, 300);
|
||||
});
|
||||
};
|
||||
self.map!.on("zoomend", zoomEndHandler);
|
||||
self.mapEventHandlers!.set("zoomend", zoomEndHandler);
|
||||
|
||||
// Handle resize
|
||||
self.resizeHandler = () => {
|
||||
|
|
@ -457,9 +423,9 @@ let MapAPRSMap = {
|
|||
e.stopPropagation();
|
||||
|
||||
// Use Phoenix LiveView's built-in navigation
|
||||
// window.liveSocket is available globally in Phoenix LiveView apps
|
||||
if ((window as any).liveSocket) {
|
||||
(window as any).liveSocket.pushHistoryPatch(navLink.href, "push", navLink);
|
||||
const liveSocket = getLiveSocket();
|
||||
if (liveSocket) {
|
||||
liveSocket.pushHistoryPatch(navLink.href, "push", navLink);
|
||||
} else {
|
||||
// Fallback to regular navigation if LiveView socket not available
|
||||
window.location.href = navLink.href;
|
||||
|
|
@ -533,20 +499,27 @@ let MapAPRSMap = {
|
|||
// Use a slight delay to ensure map is ready
|
||||
setTimeout(() => {
|
||||
if (self.map) {
|
||||
// Set counter to handle both moveend and zoomend events from setView
|
||||
// setView() can trigger both events, so we need to handle both
|
||||
self.programmaticMoveCounter = 2;
|
||||
// Generate a unique ID for this programmatic move
|
||||
const moveId = `move_${Date.now()}_${Math.random()}`;
|
||||
self.programmaticMoveId = moveId;
|
||||
|
||||
// Clear any existing timeout
|
||||
if (self.programmaticMoveTimeout) {
|
||||
clearTimeout(self.programmaticMoveTimeout);
|
||||
}
|
||||
|
||||
// Set a timeout to clear the programmatic move flag
|
||||
// This ensures we don't block user interactions indefinitely
|
||||
self.programmaticMoveTimeout = setTimeout(() => {
|
||||
if (self.programmaticMoveId === moveId) {
|
||||
self.programmaticMoveId = undefined;
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
self.map.setView([lat, lng], zoom, {
|
||||
animate: true,
|
||||
duration: 1,
|
||||
});
|
||||
|
||||
// Safety timeout to reset counter in case events don't fire as expected
|
||||
setTimeout(() => {
|
||||
if (self.programmaticMoveCounter && self.programmaticMoveCounter > 0) {
|
||||
self.programmaticMoveCounter = 0;
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Check element dimensions after zoom
|
||||
setTimeout(() => {
|
||||
|
|
@ -672,14 +645,16 @@ let MapAPRSMap = {
|
|||
const prevMarker = self.markers!.get(self.currentPopupMarkerId);
|
||||
if (prevMarker && prevMarker.closePopup) prevMarker.closePopup();
|
||||
}
|
||||
// Re-add marker with openPopup flag (will open after add)
|
||||
const markerData = self.markerStates!.get(data.id);
|
||||
if (markerData) {
|
||||
self.addMarker({ ...markerData, id: data.id, openPopup: true });
|
||||
// Try to open popup directly first if marker exists
|
||||
const marker = self.markers!.get(data.id);
|
||||
if (marker && marker.openPopup) {
|
||||
marker.openPopup();
|
||||
} else {
|
||||
// fallback: try to open popup directly if marker exists
|
||||
const marker = self.markers!.get(data.id);
|
||||
if (marker && marker.openPopup) marker.openPopup();
|
||||
// Fallback: re-add marker with openPopup flag if it doesn't exist
|
||||
const markerData = self.markerStates!.get(data.id);
|
||||
if (markerData) {
|
||||
self.addMarker({ ...markerData, id: data.id, openPopup: true });
|
||||
}
|
||||
}
|
||||
self.currentPopupMarkerId = data.id;
|
||||
});
|
||||
|
|
@ -711,16 +686,8 @@ let MapAPRSMap = {
|
|||
packetsByCallsign.forEach((packets, callsign) => {
|
||||
// Sort by timestamp (oldest first) to ensure proper trail line drawing
|
||||
const sortedPackets = packets.sort((a, b) => {
|
||||
const timeA = a.timestamp
|
||||
? typeof a.timestamp === "number"
|
||||
? a.timestamp
|
||||
: new Date(a.timestamp).getTime()
|
||||
: 0;
|
||||
const timeB = b.timestamp
|
||||
? typeof b.timestamp === "number"
|
||||
? b.timestamp
|
||||
: new Date(b.timestamp).getTime()
|
||||
: 0;
|
||||
const timeA = parseTimestamp(a.timestamp);
|
||||
const timeB = parseTimestamp(b.timestamp);
|
||||
return timeA - timeB;
|
||||
});
|
||||
|
||||
|
|
@ -754,16 +721,8 @@ let MapAPRSMap = {
|
|||
packetsByCallsign.forEach((packets, callsign) => {
|
||||
// Sort by timestamp (oldest first) to ensure proper trail line drawing
|
||||
const sortedPackets = packets.sort((a, b) => {
|
||||
const timeA = a.timestamp
|
||||
? typeof a.timestamp === "number"
|
||||
? a.timestamp
|
||||
: new Date(a.timestamp).getTime()
|
||||
: 0;
|
||||
const timeB = b.timestamp
|
||||
? typeof b.timestamp === "number"
|
||||
? b.timestamp
|
||||
: new Date(b.timestamp).getTime()
|
||||
: 0;
|
||||
const timeA = parseTimestamp(a.timestamp);
|
||||
const timeB = parseTimestamp(b.timestamp);
|
||||
return timeA - timeB;
|
||||
});
|
||||
|
||||
|
|
@ -911,13 +870,9 @@ let MapAPRSMap = {
|
|||
if (positionChanged && self.trailManager) {
|
||||
// Position changed, update trail
|
||||
const isHistoricalDot = data.historical && !data.is_most_recent_for_callsign;
|
||||
const timestamp = data.timestamp
|
||||
? typeof data.timestamp === "string"
|
||||
? new Date(data.timestamp).getTime()
|
||||
: data.timestamp
|
||||
: Date.now();
|
||||
// Use callsign_group for proper trail grouping - prioritize callsign_group, then callsign, then id
|
||||
const trailId = data.callsign_group || data.callsign || data.id;
|
||||
const timestamp = parseTimestamp(data.timestamp);
|
||||
// Use callsign_group for proper trail grouping
|
||||
const trailId = getTrailId(data);
|
||||
|
||||
self.trailManager.addPosition(trailId, lat, lng, timestamp, isHistoricalDot);
|
||||
}
|
||||
|
|
@ -946,35 +901,19 @@ let MapAPRSMap = {
|
|||
|
||||
// Handle popup close events - check if hook is still connected
|
||||
marker.on("popupclose", () => {
|
||||
// Only send event if LiveView is still connected
|
||||
if (self.pushEvent) {
|
||||
try {
|
||||
self.pushEvent("popup_closed", {});
|
||||
} catch (e) {
|
||||
// Silently ignore if LiveView is disconnected
|
||||
console.debug("Unable to send popup_closed event - LiveView disconnected");
|
||||
}
|
||||
}
|
||||
safePushEvent(self.pushEvent, "popup_closed", {});
|
||||
});
|
||||
}
|
||||
|
||||
// Handle marker click
|
||||
marker.on("click", () => {
|
||||
if (marker.openPopup) marker.openPopup();
|
||||
// Only send event if LiveView is still connected
|
||||
if (self.pushEvent) {
|
||||
try {
|
||||
self.pushEvent("marker_clicked", {
|
||||
id: data.id,
|
||||
callsign: data.callsign,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
});
|
||||
} catch (e) {
|
||||
// Silently ignore if LiveView is disconnected
|
||||
console.debug("Unable to send marker_clicked event - LiveView disconnected");
|
||||
}
|
||||
}
|
||||
safePushEvent(self.pushEvent, "marker_clicked", {
|
||||
id: data.id,
|
||||
callsign: data.callsign,
|
||||
lat: lat,
|
||||
lng: lng,
|
||||
});
|
||||
});
|
||||
|
||||
// Mark historical markers for identification
|
||||
|
|
@ -1009,13 +948,9 @@ let MapAPRSMap = {
|
|||
// Initialize trail for new marker - always add to trail for line drawing
|
||||
if (self.trailManager) {
|
||||
const isHistoricalDot = data.historical && !data.is_most_recent_for_callsign;
|
||||
const timestamp = data.timestamp
|
||||
? typeof data.timestamp === "string"
|
||||
? new Date(data.timestamp).getTime()
|
||||
: data.timestamp
|
||||
: Date.now();
|
||||
// Use callsign_group for proper trail grouping - prioritize callsign_group, then callsign, then id
|
||||
const trailId = data.callsign_group || data.callsign || data.id;
|
||||
const timestamp = parseTimestamp(data.timestamp);
|
||||
// Use callsign_group for proper trail grouping
|
||||
const trailId = getTrailId(data);
|
||||
|
||||
self.trailManager.addPosition(trailId, lat, lng, timestamp, isHistoricalDot);
|
||||
}
|
||||
|
|
@ -1074,13 +1009,9 @@ let MapAPRSMap = {
|
|||
existingMarker.setLatLng([lat, lng]);
|
||||
if (self.trailManager) {
|
||||
const isHistoricalDot = data.historical && !data.is_most_recent_for_callsign;
|
||||
const timestamp = data.timestamp
|
||||
? typeof data.timestamp === "string"
|
||||
? new Date(data.timestamp).getTime()
|
||||
: data.timestamp
|
||||
: Date.now();
|
||||
// Use callsign_group for proper trail grouping - prioritize callsign_group, then callsign, then id
|
||||
const trailId = data.callsign_group || data.callsign || data.id;
|
||||
const timestamp = parseTimestamp(data.timestamp);
|
||||
// Use callsign_group for proper trail grouping
|
||||
const trailId = getTrailId(data);
|
||||
self.trailManager.addPosition(trailId, lat, lng, timestamp, isHistoricalDot);
|
||||
}
|
||||
}
|
||||
|
|
@ -1322,14 +1253,10 @@ let MapAPRSMap = {
|
|||
}
|
||||
|
||||
if (data.timestamp) {
|
||||
let date;
|
||||
if (typeof data.timestamp === "number") {
|
||||
date = new Date(data.timestamp * 1000);
|
||||
} else if (typeof data.timestamp === "string") {
|
||||
date = new Date(data.timestamp);
|
||||
}
|
||||
|
||||
if (date && !isNaN(date.getTime())) {
|
||||
const timestamp = parseTimestamp(data.timestamp);
|
||||
const date = new Date(timestamp);
|
||||
|
||||
if (!isNaN(date.getTime())) {
|
||||
content += `<div class="aprs-timestamp">${date.toISOString()}</div>`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1341,13 +1268,29 @@ let MapAPRSMap = {
|
|||
destroyed() {
|
||||
const self = this as unknown as LiveViewHookContext;
|
||||
|
||||
// Mark as destroyed immediately
|
||||
self.isDestroyed = true;
|
||||
|
||||
// Disable pushEvent to prevent any events from being sent during cleanup
|
||||
const originalPushEvent = self.pushEvent;
|
||||
self.pushEvent = () => {}; // No-op function
|
||||
|
||||
// Clear interval timer
|
||||
if (self.cleanupInterval !== undefined) {
|
||||
clearInterval(self.cleanupInterval);
|
||||
self.cleanupInterval = undefined;
|
||||
}
|
||||
|
||||
// Clear programmatic move timeout
|
||||
if (self.programmaticMoveTimeout !== undefined) {
|
||||
clearTimeout(self.programmaticMoveTimeout);
|
||||
self.programmaticMoveTimeout = undefined;
|
||||
}
|
||||
|
||||
// Remove popup navigation event listener
|
||||
if (self.popupNavigationHandler) {
|
||||
document.removeEventListener('click', self.popupNavigationHandler);
|
||||
self.popupNavigationHandler = undefined;
|
||||
}
|
||||
|
||||
// Close any open popups before cleanup
|
||||
|
|
@ -1355,7 +1298,7 @@ let MapAPRSMap = {
|
|||
try {
|
||||
self.map.closePopup();
|
||||
} catch (e) {
|
||||
// Ignore errors during popup cleanup
|
||||
console.debug("Error closing popup during cleanup:", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1364,6 +1307,19 @@ let MapAPRSMap = {
|
|||
}
|
||||
if (self.resizeHandler !== undefined) {
|
||||
window.removeEventListener("resize", self.resizeHandler);
|
||||
self.resizeHandler = undefined;
|
||||
}
|
||||
|
||||
// Remove map event handlers
|
||||
if (self.map !== undefined && self.mapEventHandlers !== undefined) {
|
||||
self.mapEventHandlers.forEach((handler, event) => {
|
||||
try {
|
||||
self.map.off(event, handler);
|
||||
} catch (e) {
|
||||
console.debug(`Error removing ${event} handler:`, e);
|
||||
}
|
||||
});
|
||||
self.mapEventHandlers.clear();
|
||||
}
|
||||
|
||||
// Remove all event listeners from markers before clearing layers
|
||||
|
|
@ -1375,7 +1331,7 @@ let MapAPRSMap = {
|
|||
marker.unbindPopup(); // Unbind popup to prevent events
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
console.debug(`Error cleaning up marker:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
307
assets/js/map_fixes.ts
Normal file
307
assets/js/map_fixes.ts
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
// Proposed fixes for map.ts issues
|
||||
|
||||
// 1. Extract duplicated functions
|
||||
export const MapHelpers = {
|
||||
// Centralized timestamp parsing
|
||||
parseTimestamp(timestamp: any): number {
|
||||
if (!timestamp) return Date.now();
|
||||
|
||||
if (typeof timestamp === "number") {
|
||||
return timestamp;
|
||||
} else if (typeof timestamp === "string") {
|
||||
return new Date(timestamp).getTime();
|
||||
}
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
// Centralized trail ID calculation
|
||||
getTrailId(data: { callsign_group?: string; callsign?: string; id: string }): string {
|
||||
return data.callsign_group || data.callsign || data.id;
|
||||
},
|
||||
|
||||
// Centralized map state saving
|
||||
saveMapState(map: any, pushEvent: Function) {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Truncate lat/lng to 5 decimal places for URL
|
||||
const truncatedLat = Math.round(center.lat * 100000) / 100000;
|
||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||
);
|
||||
|
||||
// Send combined map state update to server for URL and bounds updating
|
||||
pushEvent("update_map_state", {
|
||||
center: { lat: truncatedLat, lng: truncatedLng },
|
||||
zoom: zoom,
|
||||
bounds: {
|
||||
north: map.getBounds().getNorth(),
|
||||
south: map.getBounds().getSouth(),
|
||||
east: map.getBounds().getEast(),
|
||||
west: map.getBounds().getWest(),
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Safe event pushing with connection check
|
||||
safePushEvent(pushEvent: Function | undefined, event: string, payload: any) {
|
||||
if (!pushEvent) return false;
|
||||
|
||||
try {
|
||||
pushEvent(event, payload);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.debug(`Unable to send ${event} event - LiveView disconnected`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 2. Improved LiveViewHookContext with proper cleanup tracking
|
||||
interface ImprovedLiveViewHookContext extends LiveViewHookContext {
|
||||
cleanupInterval?: ReturnType<typeof setInterval>;
|
||||
mapEventHandlers?: Map<string, Function>;
|
||||
isDestroyed?: boolean;
|
||||
}
|
||||
|
||||
// 3. Example of fixed initialization with proper cleanup tracking
|
||||
export const setupMapWithCleanup = (self: ImprovedLiveViewHookContext) => {
|
||||
// Track if component is destroyed
|
||||
self.isDestroyed = false;
|
||||
|
||||
// Store event handlers for cleanup
|
||||
self.mapEventHandlers = new Map();
|
||||
|
||||
// Store interval for cleanup
|
||||
self.cleanupInterval = setInterval(() => {
|
||||
if (!self.isDestroyed && self.trailManager) {
|
||||
self.trailManager.cleanupOldPositions();
|
||||
}
|
||||
}, 5 * 60 * 1000);
|
||||
|
||||
// Create wrapped event handlers that check if destroyed
|
||||
const createSafeHandler = (handler: Function) => {
|
||||
return (...args: any[]) => {
|
||||
if (!self.isDestroyed) {
|
||||
handler(...args);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// Example of safe map event handler
|
||||
const moveEndHandler = createSafeHandler(() => {
|
||||
if (self.programmaticMoveCounter && self.programmaticMoveCounter > 0) {
|
||||
self.programmaticMoveCounter--;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.boundsTimer) clearTimeout(self.boundsTimer);
|
||||
self.boundsTimer = setTimeout(() => {
|
||||
if (!self.isDestroyed) {
|
||||
MapHelpers.saveMapState(self.map, self.pushEvent);
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
|
||||
self.map.on("moveend", moveEndHandler);
|
||||
self.mapEventHandlers.set("moveend", moveEndHandler);
|
||||
};
|
||||
|
||||
// 4. Improved cleanup function
|
||||
export const improvedDestroyed = (self: ImprovedLiveViewHookContext) => {
|
||||
// Mark as destroyed immediately
|
||||
self.isDestroyed = true;
|
||||
|
||||
// Disable pushEvent to prevent any events from being sent during cleanup
|
||||
const originalPushEvent = self.pushEvent;
|
||||
self.pushEvent = () => {}; // No-op function
|
||||
|
||||
// Clear interval timer
|
||||
if (self.cleanupInterval !== undefined) {
|
||||
clearInterval(self.cleanupInterval);
|
||||
self.cleanupInterval = undefined;
|
||||
}
|
||||
|
||||
// Remove popup navigation event listener
|
||||
if (self.popupNavigationHandler) {
|
||||
document.removeEventListener('click', self.popupNavigationHandler);
|
||||
self.popupNavigationHandler = undefined;
|
||||
}
|
||||
|
||||
// Close any open popups before cleanup
|
||||
if (self.map !== undefined) {
|
||||
try {
|
||||
self.map.closePopup();
|
||||
} catch (e) {
|
||||
console.debug("Error closing popup during cleanup:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear timers
|
||||
if (self.boundsTimer !== undefined) {
|
||||
clearTimeout(self.boundsTimer);
|
||||
self.boundsTimer = undefined;
|
||||
}
|
||||
|
||||
if (self.resizeHandler !== undefined) {
|
||||
window.removeEventListener("resize", self.resizeHandler);
|
||||
self.resizeHandler = undefined;
|
||||
}
|
||||
|
||||
// Remove map event handlers
|
||||
if (self.map !== undefined && self.mapEventHandlers !== undefined) {
|
||||
self.mapEventHandlers.forEach((handler, event) => {
|
||||
try {
|
||||
self.map.off(event, handler);
|
||||
} catch (e) {
|
||||
console.debug(`Error removing ${event} handler:`, e);
|
||||
}
|
||||
});
|
||||
self.mapEventHandlers.clear();
|
||||
}
|
||||
|
||||
// Remove all event listeners from markers before clearing layers
|
||||
if (self.markers !== undefined) {
|
||||
self.markers.forEach((marker: any, id: string) => {
|
||||
try {
|
||||
marker.off(); // Remove all event listeners
|
||||
if (marker.getPopup()) {
|
||||
marker.unbindPopup(); // Unbind popup to prevent events
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug(`Error cleaning up marker ${id}:`, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Clear layers and data
|
||||
if (self.markerLayer !== undefined) {
|
||||
try {
|
||||
self.markerLayer!.clearLayers();
|
||||
} catch (e) {
|
||||
console.debug("Error clearing marker layer:", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (self.markers !== undefined) {
|
||||
self.markers!.clear();
|
||||
}
|
||||
|
||||
if (self.markerStates !== undefined) {
|
||||
self.markerStates!.clear();
|
||||
}
|
||||
|
||||
// Remove map
|
||||
if (self.map !== undefined) {
|
||||
try {
|
||||
self.map!.remove();
|
||||
} catch (e) {
|
||||
console.debug("Error removing map:", e);
|
||||
}
|
||||
self.map = undefined;
|
||||
}
|
||||
|
||||
// Clear OMS if present
|
||||
if (self.oms !== undefined) {
|
||||
self.oms = undefined;
|
||||
}
|
||||
|
||||
// Restore original pushEvent (though it won't be used since we're destroyed)
|
||||
self.pushEvent = originalPushEvent;
|
||||
};
|
||||
|
||||
// 5. Optimized marker position lookup using spatial index
|
||||
export class MarkerSpatialIndex {
|
||||
private grid: Map<string, Set<string>> = new Map();
|
||||
private cellSize: number = 0.0001; // ~11 meters at equator
|
||||
|
||||
private getGridKey(lat: number, lng: number): string {
|
||||
const latCell = Math.floor(lat / this.cellSize);
|
||||
const lngCell = Math.floor(lng / this.cellSize);
|
||||
return `${latCell},${lngCell}`;
|
||||
}
|
||||
|
||||
add(id: string, lat: number, lng: number) {
|
||||
const key = this.getGridKey(lat, lng);
|
||||
if (!this.grid.has(key)) {
|
||||
this.grid.set(key, new Set());
|
||||
}
|
||||
this.grid.get(key)!.add(id);
|
||||
}
|
||||
|
||||
remove(id: string, lat: number, lng: number) {
|
||||
const key = this.getGridKey(lat, lng);
|
||||
const cell = this.grid.get(key);
|
||||
if (cell) {
|
||||
cell.delete(id);
|
||||
if (cell.size === 0) {
|
||||
this.grid.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findNearby(lat: number, lng: number, radius: number = 0.00001): Set<string> {
|
||||
const results = new Set<string>();
|
||||
const cellRadius = Math.ceil(radius / this.cellSize);
|
||||
const centerLatCell = Math.floor(lat / this.cellSize);
|
||||
const centerLngCell = Math.floor(lng / this.cellSize);
|
||||
|
||||
for (let latOffset = -cellRadius; latOffset <= cellRadius; latOffset++) {
|
||||
for (let lngOffset = -cellRadius; lngOffset <= cellRadius; lngOffset++) {
|
||||
const key = `${centerLatCell + latOffset},${centerLngCell + lngOffset}`;
|
||||
const cell = this.grid.get(key);
|
||||
if (cell) {
|
||||
cell.forEach(id => results.add(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.grid.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Improved programmatic move counter with proper state management
|
||||
export class ProgrammaticMoveTracker {
|
||||
private moveCount: number = 0;
|
||||
private timeoutId?: ReturnType<typeof setTimeout>;
|
||||
|
||||
expectMoves(count: number) {
|
||||
this.moveCount += count;
|
||||
|
||||
// Clear existing timeout
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
|
||||
// Set safety timeout
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (this.moveCount > 0) {
|
||||
console.debug(`Resetting programmatic move counter from ${this.moveCount} to 0`);
|
||||
this.moveCount = 0;
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
handleMove(): boolean {
|
||||
if (this.moveCount > 0) {
|
||||
this.moveCount--;
|
||||
return true; // This was a programmatic move
|
||||
}
|
||||
return false; // This was a user-initiated move
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.moveCount = 0;
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
93
assets/js/map_helpers.ts
Normal file
93
assets/js/map_helpers.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
// Helper functions for map.ts to reduce code duplication
|
||||
|
||||
export interface MapState {
|
||||
lat: number;
|
||||
lng: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export interface BoundsData {
|
||||
north: number;
|
||||
south: number;
|
||||
east: number;
|
||||
west: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse timestamp to milliseconds
|
||||
*/
|
||||
export function parseTimestamp(timestamp: any): number {
|
||||
if (!timestamp) return Date.now();
|
||||
|
||||
if (typeof timestamp === "number") {
|
||||
return timestamp;
|
||||
} else if (typeof timestamp === "string") {
|
||||
return new Date(timestamp).getTime();
|
||||
}
|
||||
return Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get trail ID from marker data
|
||||
*/
|
||||
export function getTrailId(data: { callsign_group?: string; callsign?: string; id: string }): string {
|
||||
return data.callsign_group || data.callsign || data.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save map state to localStorage and send to server
|
||||
*/
|
||||
export function saveMapState(map: any, pushEvent: Function) {
|
||||
const center = map.getCenter();
|
||||
const zoom = map.getZoom();
|
||||
|
||||
// Truncate lat/lng to 5 decimal places for URL
|
||||
const truncatedLat = Math.round(center.lat * 100000) / 100000;
|
||||
const truncatedLng = Math.round(center.lng * 100000) / 100000;
|
||||
|
||||
localStorage.setItem(
|
||||
"aprs_map_state",
|
||||
JSON.stringify({ lat: truncatedLat, lng: truncatedLng, zoom }),
|
||||
);
|
||||
|
||||
// Send combined map state update to server for URL and bounds updating
|
||||
pushEvent("update_map_state", {
|
||||
center: { lat: truncatedLat, lng: truncatedLng },
|
||||
zoom: zoom,
|
||||
bounds: {
|
||||
north: map.getBounds().getNorth(),
|
||||
south: map.getBounds().getSouth(),
|
||||
east: map.getBounds().getEast(),
|
||||
west: map.getBounds().getWest(),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely push event to LiveView
|
||||
*/
|
||||
export function safePushEvent(pushEvent: Function | undefined, event: string, payload: any): boolean {
|
||||
if (!pushEvent) return false;
|
||||
|
||||
try {
|
||||
pushEvent(event, payload);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.debug(`Unable to send ${event} event - LiveView disconnected`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if LiveView socket is available
|
||||
*/
|
||||
export function isLiveViewConnected(): boolean {
|
||||
return !!(window as any).liveSocket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get LiveView socket
|
||||
*/
|
||||
export function getLiveSocket(): any {
|
||||
return (window as any).liveSocket;
|
||||
}
|
||||
55
test/assets/js/map_issues_test.exs
Normal file
55
test/assets/js/map_issues_test.exs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
defmodule AprsmeWeb.MapIssuesTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
describe "map.ts issues" do
|
||||
test "memory leaks identified" do
|
||||
issues = [
|
||||
"Interval timer on line 278 not stored or cleared - will run forever",
|
||||
"Map event listeners (moveend, zoomend) not removed on destruction",
|
||||
"No cleanup for map.whenReady callback"
|
||||
]
|
||||
|
||||
assert length(issues) == 3
|
||||
end
|
||||
|
||||
test "race conditions identified" do
|
||||
issues = [
|
||||
"programmaticMoveCounter can become incorrect with rapid moves",
|
||||
"boundsTimer could have overlapping timeouts",
|
||||
"Popup events could fire after component destruction"
|
||||
]
|
||||
|
||||
assert length(issues) == 3
|
||||
end
|
||||
|
||||
test "error handling issues" do
|
||||
issues = [
|
||||
"marker_clicked event doesn't check LiveView connection first",
|
||||
"Errors in destroyed() are silently swallowed without logging",
|
||||
"No user feedback for failed operations"
|
||||
]
|
||||
|
||||
assert length(issues) == 3
|
||||
end
|
||||
|
||||
test "performance issues" do
|
||||
issues = [
|
||||
"O(n) lookup for duplicate markers at same position",
|
||||
"Unnecessary marker re-creation for popup opening",
|
||||
"Repeated access to window.liveSocket without caching"
|
||||
]
|
||||
|
||||
assert length(issues) == 3
|
||||
end
|
||||
|
||||
test "code duplication" do
|
||||
duplicated_functions = [
|
||||
"Map state saving logic (lines 310-325 and 350-373)",
|
||||
"Timestamp parsing (4 different locations)",
|
||||
"Trail ID calculation (3 different locations)"
|
||||
]
|
||||
|
||||
assert length(duplicated_functions) == 3
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Reference in a new issue