`;
// Removed symbol info from popup
// content += `
${symbolDesc}
`;
if (comment) {
content += `
${comment}
`;
}
if (data.timestamp) {
const timestamp = parseTimestamp(data.timestamp);
const date = new Date(timestamp);
if (!isNaN(date.getTime())) {
content += `
${date.toISOString()}
`;
}
}
content += "
";
return content;
},
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;
}
// Clear any dimension check timeouts
if (self.cleanupTimeouts !== undefined) {
self.cleanupTimeouts.forEach((timeout: number) => {
clearTimeout(timeout);
});
self.cleanupTimeouts = [];
}
// 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);
}
}
if (self.boundsTimer !== undefined) {
clearTimeout(self.boundsTimer);
}
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: APRSMarker) => {
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:`, e);
}
});
}
// Clean up OMS (OverlappingMarkerSpiderfier)
if (self.oms !== undefined) {
try {
self.oms.clearMarkers();
self.oms.clearListeners('click');
self.oms.clearListeners('spiderfy');
self.oms.clearListeners('unspiderfy');
self.oms = undefined;
} catch (e) {
console.debug("Error cleaning up OMS:", e);
}
}
// Clean up heat layer
if (self.heatLayer !== undefined && self.map !== undefined) {
try {
if (self.map.hasLayer(self.heatLayer)) {
self.map.removeLayer(self.heatLayer);
}
self.heatLayer = undefined;
} catch (e) {
console.debug("Error removing heat layer:", e);
}
}
if (self.markerLayer !== undefined) {
self.markerLayer!.clearLayers();
// If it's a cluster group, remove it from the map
if (self.markerClusterGroup !== undefined && self.map !== undefined) {
try {
self.map.removeLayer(self.markerClusterGroup);
self.markerClusterGroup = undefined;
} catch (e) {
console.debug("Error removing cluster group:", e);
}
}
}
if (self.markers !== undefined) {
self.markers!.clear();
}
if (self.markerStates !== undefined) {
self.markerStates!.clear();
}
if (self.map !== undefined) {
self.map!.remove();
self.map = undefined;
}
// Restore original pushEvent (though it won't be used since we're destroyed)
self.pushEvent = originalPushEvent;
},
setupMobileGestures() {
const self = this as unknown as LiveViewHookContext;
if (!self.map) return;
// Long press to show station info
let longPressTimer: NodeJS.Timeout | null = null;
let touchStartPos: { x: number; y: number } | null = null;
self.map.on('touchstart', (e: LeafletTouchEvent) => {
touchStartPos = { x: e.originalEvent.touches[0].pageX, y: e.originalEvent.touches[0].pageY };
longPressTimer = setTimeout(() => {
// Get the closest marker to the touch point
const point = e.containerPoint;
let closestMarker: APRSMarker | null = null;
let closestDistance = Infinity;
self.markers.forEach((marker) => {
const markerPoint = self.map.latLngToContainerPoint(marker.getLatLng());
const distance = Math.sqrt(
Math.pow(markerPoint.x - point.x, 2) +
Math.pow(markerPoint.y - point.y, 2)
);
if (distance < closestDistance && distance < 50) { // 50px tolerance
closestDistance = distance;
closestMarker = marker;
}
});
if (closestMarker) {
closestMarker.openPopup();
}
}, 600); // 600ms for long press
});
self.map.on('touchmove', (e: LeafletTouchEvent) => {
if (longPressTimer && touchStartPos) {
const moveThreshold = 10; // pixels
const currentPos = { x: e.originalEvent.touches[0].pageX, y: e.originalEvent.touches[0].pageY };
const distance = Math.sqrt(
Math.pow(currentPos.x - touchStartPos.x, 2) +
Math.pow(currentPos.y - touchStartPos.y, 2)
);
if (distance > moveThreshold) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
}
});
self.map.on('touchend touchcancel', () => {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
touchStartPos = null;
});
// Improve popup behavior on mobile
self.map.on('popupopen', (e: LeafletPopupEvent) => {
// Ensure popup is visible on mobile
const popup = e.popup;
const px = self.map.project(popup.getLatLng());
const popupHeight = popup.getElement()?.offsetHeight || 200;
const mapHeight = self.map.getContainer().offsetHeight;
// If popup would be cut off at bottom, pan the map
const containerPoint = self.map.latLngToContainerPoint(popup.getLatLng());
if (containerPoint.y + popupHeight > mapHeight - 50) {
px.y -= (popupHeight / 2);
self.map.panTo(self.map.unproject(px), { animate: true });
}
});
},
};
// Helper to validate and fallback symbol code per aprs.fi logic
function getValidSymbolCode(
symbolCode: string | undefined,
symbolTableId: string | undefined,
): string {
if (!symbolCode || symbolCode.length !== 1) {
return symbolTableId === "\\" ? "O" : ">";
}
const code = symbolCode.charCodeAt(0);
if (code < 33 || code > 126) {
return symbolTableId === "\\" ? "O" : ">";
}
return symbolCode;
}
export default MapAPRSMap;