import { createPolylinesLayer } from "./polylines"; import { createLiveMarker } from "./marker_factory"; /** * LiveMapHandler - Manages real-time GPS point streaming and live map updates * * This class handles the memory-efficient live mode functionality that was * previously causing memory leaks in the main maps controller. * * Features: * - Incremental marker addition (no layer recreation) * - Bounded data structures (prevents memory leaks) * - Efficient polyline segment updates * - Smart last marker tracking */ export class LiveMapHandler { constructor(map, layers, options = {}) { this.map = map; this.markersLayer = layers.markersLayer; this.polylinesLayer = layers.polylinesLayer; this.heatmapLayer = layers.heatmapLayer; this.fogOverlay = layers.fogOverlay; // Data arrays - can be initialized with existing data this.markers = options.existingMarkers || []; this.markersArray = options.existingMarkersArray || []; this.heatmapMarkers = options.existingHeatmapMarkers || []; // Configuration options this.maxPoints = options.maxPoints || 1000; this.routeOpacity = options.routeOpacity || 1; this.timezone = options.timezone || 'UTC'; this.distanceUnit = options.distanceUnit || 'km'; this.userSettings = options.userSettings || {}; this.clearFogRadius = options.clearFogRadius || 100; this.fogLineThreshold = options.fogLineThreshold || 10; // State tracking this.isEnabled = false; this.lastMarkerRef = null; // Bind methods this.appendPoint = this.appendPoint.bind(this); this.enable = this.enable.bind(this); this.disable = this.disable.bind(this); } /** * Enable live mode */ enable() { this.isEnabled = true; console.log('Live map mode enabled'); } /** * Disable live mode and cleanup */ disable() { this.isEnabled = false; this._cleanup(); console.log('Live map mode disabled'); } /** * Check if live mode is currently enabled */ get enabled() { return this.isEnabled; } /** * Append a new GPS point to the live map (memory-efficient implementation) * * @param {Array} data - Point data [lat, lng, battery, altitude, timestamp, velocity, id, country] */ appendPoint(data) { if (!this.isEnabled) { console.warn('LiveMapHandler: appendPoint called but live mode is not enabled'); return; } // Parse the received point data const newPoint = data; // Add the new point to the markers array this.markers.push(newPoint); // Implement bounded markers array (keep only last maxPoints in live mode) this._enforcePointLimits(); // Create and add new marker incrementally const newMarker = this._createMarker(newPoint); this.markersArray.push(newMarker); this.markersLayer.addLayer(newMarker); // Update heatmap with bounds this._updateHeatmap(newPoint); // Update polylines incrementally this._updatePolylines(newPoint); // Pan map to new location this.map.setView([newPoint[0], newPoint[1]], 16); // Update fog of war if enabled this._updateFogOfWar(); // Update the last marker efficiently this._updateLastMarker(); } /** * Get current statistics about the live map state */ getStats() { return { totalPoints: this.markers.length, visibleMarkers: this.markersArray.length, heatmapPoints: this.heatmapMarkers.length, isEnabled: this.isEnabled, maxPoints: this.maxPoints }; } /** * Update configuration options */ updateOptions(newOptions) { Object.assign(this, newOptions); } /** * Clear all live mode data */ clear() { // Clear data arrays this.markers = []; this.markersArray = []; this.heatmapMarkers = []; // Clear map layers this.markersLayer.clearLayers(); this.polylinesLayer.clearLayers(); this.heatmapLayer.setLatLngs([]); // Clear last marker reference if (this.lastMarkerRef) { this.map.removeLayer(this.lastMarkerRef); this.lastMarkerRef = null; } } // Private helper methods /** * Enforce point limits to prevent memory leaks * @private */ _enforcePointLimits() { if (this.markers.length > this.maxPoints) { this.markers.shift(); // Remove oldest point // Also remove corresponding marker from display if (this.markersArray.length > this.maxPoints) { const oldMarker = this.markersArray.shift(); this.markersLayer.removeLayer(oldMarker); } } } /** * Create a new marker using the shared factory (memory-efficient for live streaming) * @private */ _createMarker(point) { return createLiveMarker(point); } /** * Update heatmap with bounded data * @private */ _updateHeatmap(point) { this.heatmapMarkers.push([point[0], point[1], 0.2]); // Keep heatmap bounded if (this.heatmapMarkers.length > this.maxPoints) { this.heatmapMarkers.shift(); // Remove oldest point } this.heatmapLayer.setLatLngs(this.heatmapMarkers); } /** * Update polylines incrementally (only add new segments) * @private */ _updatePolylines(newPoint) { // Only update polylines if we have more than one point if (this.markers.length > 1) { const prevPoint = this.markers[this.markers.length - 2]; const newSegment = L.polyline([ [prevPoint[0], prevPoint[1]], [newPoint[0], newPoint[1]] ], { color: this.routeOpacity > 0 ? '#3388ff' : 'transparent', weight: 3, opacity: this.routeOpacity }); // Add only the new segment instead of recreating all polylines this.polylinesLayer.addLayer(newSegment); } } /** * Update fog of war if enabled * @private */ _updateFogOfWar() { if (this.map.hasLayer(this.fogOverlay)) { // This would need to be implemented based on the existing fog logic // For now, we'll just log that it needs updating console.log('LiveMapHandler: Fog of war update needed'); } } /** * Update the last marker efficiently using direct reference tracking * @private */ _updateLastMarker() { // Remove previous last marker if (this.lastMarkerRef) { this.map.removeLayer(this.lastMarkerRef); } // Add new last marker and store reference if (this.markers.length > 0) { const lastPoint = this.markers[this.markers.length - 1]; const lastMarker = L.marker([lastPoint[0], lastPoint[1]]); this.lastMarkerRef = lastMarker.addTo(this.map); } } /** * Cleanup resources when disabling live mode * @private */ _cleanup() { // Remove last marker if (this.lastMarkerRef) { this.map.removeLayer(this.lastMarkerRef); this.lastMarkerRef = null; } // Note: We don't clear the data arrays here as the user might want to keep // the points visible after disabling live mode. Use clear() for that. } }