import { createPopupContent } from "./popups"; const MARKER_DATA_INDICES = { LATITUDE: 0, LONGITUDE: 1, BATTERY: 2, ALTITUDE: 3, TIMESTAMP: 4, VELOCITY: 5, ID: 6, COUNTRY: 7 }; /** * MarkerFactory - Centralized marker creation with consistent styling * * This module provides reusable marker creation functions to ensure * consistent styling and prevent code duplication between different * map components. * * Memory-safe: Creates fresh instances, no shared references that could * cause memory leaks. */ /** * Create a standard divIcon for GPS points * @param {string} color - Marker color ('blue', 'orange', etc.) * @param {number} size - Icon size in pixels (default: 8) * @returns {L.DivIcon} Leaflet divIcon instance */ export function createStandardIcon(color = 'blue', size = 8) { return L.divIcon({ className: 'custom-div-icon', html: `
`, iconSize: [size, size], iconAnchor: [size / 2, size / 2] }); } /** * Create a basic marker for live streaming (no drag handlers, minimal features) * Memory-efficient for high-frequency creation/destruction * * @param {Array} point - Point data [lat, lng, battery, altitude, timestamp, velocity, id, country] * @param {Object} options - Optional marker configuration * @returns {L.Marker} Leaflet marker instance */ export function createLiveMarker(point, options = {}) { const [lat, lng] = point; const velocity = point[5] || 0; // velocity is at index 5 const markerColor = velocity < 0 ? 'orange' : 'blue'; const size = options.size || 8; return L.marker([lat, lng], { icon: createStandardIcon(markerColor, size), // Live markers don't need these heavy features draggable: false, autoPan: false, // Store minimal data needed for cleanup pointId: point[6], // ID is at index 6 ...options // Allow overriding defaults }); } /** * Create a full-featured marker with drag handlers and popups * Used for static map display where full interactivity is needed * * @param {Array} point - Point data [lat, lng, battery, altitude, timestamp, velocity, id, country] * @param {number} index - Marker index in the array * @param {Object} userSettings - User configuration * @param {string} apiKey - API key for backend operations * @param {L.Renderer} renderer - Optional Leaflet renderer * @returns {L.Marker} Fully configured Leaflet marker with event handlers */ export function createInteractiveMarker(point, index, userSettings, apiKey, renderer = null) { const [lat, lng] = point; const pointId = point[6]; // ID is at index 6 const velocity = point[5] || 0; // velocity is at index 5 const markerColor = velocity < 0 ? 'orange' : 'blue'; const marker = L.marker([lat, lng], { icon: createStandardIcon(markerColor), draggable: true, autoPan: true, pointIndex: index, pointId: pointId, originalLat: lat, originalLng: lng, markerData: point, // Store the complete marker data renderer: renderer }); // Add popup marker.bindPopup(createPopupContent(point, userSettings.timezone, userSettings.distanceUnit)); // Add drag event handlers addDragHandlers(marker, apiKey, userSettings); return marker; } /** * Create a simplified marker with minimal features * Used for simplified rendering mode * * @param {Array} point - Point data [lat, lng, battery, altitude, timestamp, velocity, id, country] * @param {Object} userSettings - User configuration (optional) * @returns {L.Marker} Leaflet marker with basic drag support */ export function createSimplifiedMarker(point, userSettings = {}) { const [lat, lng] = point; const velocity = point[5] || 0; const markerColor = velocity < 0 ? 'orange' : 'blue'; const marker = L.marker([lat, lng], { icon: createStandardIcon(markerColor), draggable: true, autoPan: true }); // Add popup if user settings provided if (userSettings.timezone && userSettings.distanceUnit) { marker.bindPopup(createPopupContent(point, userSettings.timezone, userSettings.distanceUnit)); } // Add simple drag handlers marker.on('dragstart', function() { this.closePopup(); }); marker.on('dragend', function(e) { const newLatLng = e.target.getLatLng(); this.setLatLng(newLatLng); this.openPopup(); }); return marker; } /** * Add comprehensive drag handlers to a marker * Handles polyline updates and backend synchronization * * @param {L.Marker} marker - The marker to add handlers to * @param {string} apiKey - API key for backend operations * @param {Object} userSettings - User configuration * @private */ function addDragHandlers(marker, apiKey, userSettings) { marker.on('dragstart', function(e) { this.closePopup(); }); marker.on('drag', function(e) { const newLatLng = e.target.getLatLng(); const map = e.target._map; const pointIndex = e.target.options.pointIndex; const originalLat = e.target.options.originalLat; const originalLng = e.target.options.originalLng; // Find polylines by iterating through all map layers map.eachLayer((layer) => { // Check if this is a LayerGroup containing polylines if (layer instanceof L.LayerGroup) { layer.eachLayer((featureGroup) => { if (featureGroup instanceof L.FeatureGroup) { featureGroup.eachLayer((segment) => { if (segment instanceof L.Polyline) { const coords = segment.getLatLngs(); const tolerance = 0.0000001; let updated = false; // Check and update start point if (Math.abs(coords[0].lat - originalLat) < tolerance && Math.abs(coords[0].lng - originalLng) < tolerance) { coords[0] = newLatLng; updated = true; } // Check and update end point if (Math.abs(coords[1].lat - originalLat) < tolerance && Math.abs(coords[1].lng - originalLng) < tolerance) { coords[1] = newLatLng; updated = true; } // Only update if we found a matching endpoint if (updated) { segment.setLatLngs(coords); segment.redraw(); } } }); } }); } }); // Update the marker's original position for the next drag event e.target.options.originalLat = newLatLng.lat; e.target.options.originalLng = newLatLng.lng; }); marker.on('dragend', function(e) { const newLatLng = e.target.getLatLng(); const pointId = e.target.options.pointId; const pointIndex = e.target.options.pointIndex; const originalMarkerData = e.target.options.markerData; fetch(`/api/v1/points/${pointId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ point: { latitude: newLatLng.lat.toString(), longitude: newLatLng.lng.toString() } }) }) .then(response => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { const map = e.target._map; if (map && map.mapsController && map.mapsController.markers) { const markers = map.mapsController.markers; if (markers[pointIndex]) { markers[pointIndex][0] = parseFloat(data.latitude); markers[pointIndex][1] = parseFloat(data.longitude); } } // Create updated marker data array const updatedMarkerData = [ parseFloat(data.latitude), parseFloat(data.longitude), originalMarkerData[MARKER_DATA_INDICES.BATTERY], originalMarkerData[MARKER_DATA_INDICES.ALTITUDE], originalMarkerData[MARKER_DATA_INDICES.TIMESTAMP], originalMarkerData[MARKER_DATA_INDICES.VELOCITY], data.id, originalMarkerData[MARKER_DATA_INDICES.COUNTRY] ]; // Update the marker's stored data e.target.options.markerData = updatedMarkerData; // Update the popup content if (this._popup) { const updatedPopupContent = createPopupContent( updatedMarkerData, userSettings.timezone, userSettings.distanceUnit ); this.setPopupContent(updatedPopupContent); } }) .catch(error => { console.error('Error updating point:', error); this.setLatLng([e.target.options.originalLat, e.target.options.originalLng]); alert('Failed to update point position. Please try again.'); }); }); }