Files
dawarich/app/javascript/maps/live_map_handler.js
Eugene Burmakin c4c829b4b0 Fix some nitpicks
2025-08-01 18:39:01 +02:00

262 lines
6.9 KiB
JavaScript

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.
}
}