From 5a031dad6924a22d37686af0d05e15e8e52821b7 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 19 Jan 2025 11:26:38 +0100 Subject: [PATCH 1/9] Implement drag and drop for points --- app/javascript/maps/markers.js | 54 ++++++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/app/javascript/maps/markers.js b/app/javascript/maps/markers.js index d55ee7fb..cb319cea 100644 --- a/app/javascript/maps/markers.js +++ b/app/javascript/maps/markers.js @@ -12,13 +12,25 @@ export function createMarkersArray(markersData, userSettings) { const popupContent = createPopupContent(marker, userSettings.timezone, userSettings.distanceUnit); let markerColor = marker[5] < 0 ? "orange" : "blue"; - return L.circleMarker([lat, lon], { - renderer: renderer, // Use canvas renderer - radius: 4, - color: markerColor, - zIndexOffset: 1000, - pane: 'markerPane' - }).bindPopup(popupContent, { autoClose: false }); + // Use L.marker instead of L.circleMarker for better drag support + return L.marker([lat, lon], { + icon: L.divIcon({ + className: 'custom-div-icon', + html: `
`, + iconSize: [8, 8], + iconAnchor: [4, 4] + }), + draggable: true, + autoPan: true + }).bindPopup(popupContent, { autoClose: false }) + .on('dragstart', function(e) { + this.closePopup(); + }) + .on('dragend', function(e) { + const newLatLng = e.target.getLatLng(); + this.setLatLng(newLatLng); + this.openPopup(); + }); }); } } @@ -53,14 +65,24 @@ export function createSimplifiedMarkers(markersData, renderer) { const popupContent = createPopupContent(marker); let markerColor = marker[5] < 0 ? "orange" : "blue"; - return L.circleMarker( - [lat, lon], - { - renderer: renderer, // Use canvas renderer - radius: 4, - color: markerColor, - zIndexOffset: 1000 - } - ).bindPopup(popupContent); + // Use L.marker instead of L.circleMarker for better drag support + return L.marker([lat, lon], { + icon: L.divIcon({ + className: 'custom-div-icon', + html: `
`, + iconSize: [8, 8], + iconAnchor: [4, 4] + }), + draggable: true, + autoPan: true + }).bindPopup(popupContent) + .on('dragstart', function(e) { + this.closePopup(); + }) + .on('dragend', function(e) { + const newLatLng = e.target.getLatLng(); + this.setLatLng(newLatLng); + this.openPopup(); + }); }); } From f85fd9e4d0d8d2da5c60f2019a6edbe10a45544d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 19 Jan 2025 12:59:12 +0100 Subject: [PATCH 2/9] Implement polylines redraw on point drag --- app/controllers/api/v1/points_controller.rb | 12 ++ app/javascript/controllers/maps_controller.js | 2 +- app/javascript/maps/markers.js | 172 +++++++++++++++++- config/routes.rb | 2 +- 4 files changed, 179 insertions(+), 9 deletions(-) diff --git a/app/controllers/api/v1/points_controller.rb b/app/controllers/api/v1/points_controller.rb index a70dabdc..7905ca68 100644 --- a/app/controllers/api/v1/points_controller.rb +++ b/app/controllers/api/v1/points_controller.rb @@ -21,6 +21,14 @@ class Api::V1::PointsController < ApiController render json: serialized_points end + def update + point = current_api_user.tracked_points.find(params[:id]) + + point.update(point_params) + + render json: point_serializer.new(point).call + end + def destroy point = current_api_user.tracked_points.find(params[:id]) point.destroy @@ -30,6 +38,10 @@ class Api::V1::PointsController < ApiController private + def point_params + params.require(:point).permit(:latitude, :longitude) + end + def point_serializer params[:slim] == 'true' ? Api::SlimPointSerializer : Api::PointSerializer end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 01fa6ad7..8a0afdf0 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -68,7 +68,7 @@ export default class extends Controller { this.map.setMaxBounds(bounds); - this.markersArray = createMarkersArray(this.markers, this.userSettings); + this.markersArray = createMarkersArray(this.markers, this.userSettings, this.apiKey); this.markersLayer = L.layerGroup(this.markersArray); this.heatmapMarkers = this.markersArray.map((element) => [element._latlng.lat, element._latlng.lng, 0.2]); diff --git a/app/javascript/maps/markers.js b/app/javascript/maps/markers.js index cb319cea..d4e67380 100644 --- a/app/javascript/maps/markers.js +++ b/app/javascript/maps/markers.js @@ -1,18 +1,18 @@ import { createPopupContent } from "./popups"; -export function createMarkersArray(markersData, userSettings) { +export function createMarkersArray(markersData, userSettings, apiKey) { // Create a canvas renderer const renderer = L.canvas({ padding: 0.5 }); if (userSettings.pointsRenderingMode === "simplified") { return createSimplifiedMarkers(markersData, renderer); } else { - return markersData.map((marker) => { + return markersData.map((marker, index) => { const [lat, lon] = marker; + const pointId = marker[2]; const popupContent = createPopupContent(marker, userSettings.timezone, userSettings.distanceUnit); let markerColor = marker[5] < 0 ? "orange" : "blue"; - // Use L.marker instead of L.circleMarker for better drag support return L.marker([lat, lon], { icon: L.divIcon({ className: 'custom-div-icon', @@ -21,20 +21,179 @@ export function createMarkersArray(markersData, userSettings) { iconAnchor: [4, 4] }), draggable: true, - autoPan: true - }).bindPopup(popupContent, { autoClose: false }) + autoPan: true, + pointIndex: index, + pointId: pointId, + originalLat: lat, + originalLng: lon, + renderer: renderer + }).bindPopup(popupContent) .on('dragstart', function(e) { + console.log('Drag started', { index: this.options.pointIndex }); this.closePopup(); }) - .on('dragend', function(e) { + .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; + + console.log('Dragging point', { + pointIndex, + newPosition: newLatLng, + originalPosition: { lat: originalLat, lng: 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) { + console.log('Updating start point of segment', { + from: coords[0], + to: newLatLng + }); + 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) { + console.log('Updating end point of segment', { + from: coords[1], + to: newLatLng + }); + 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; + }) + .on('dragend', function(e) { + console.log('Drag ended', { + finalPosition: e.target.getLatLng(), + pointIndex: e.target.options.pointIndex + }); + const newLatLng = e.target.getLatLng(); + const pointId = e.target.options.pointId; + const pointIndex = e.target.options.pointIndex; + + // Update the marker's position this.setLatLng(newLatLng); this.openPopup(); + + // Send API request to update point position + 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, + longitude: newLatLng.lng + } + }) + }) + .then(response => { + if (!response.ok) { + throw new Error('Failed to update point position'); + } + return response.json(); + }) + .then(data => { + // Update the markers array in the controller + const map = e.target._map; + const mapsController = map.mapsController; + if (mapsController && mapsController.markers) { + mapsController.markers[pointIndex][0] = newLatLng.lat; + mapsController.markers[pointIndex][1] = newLatLng.lng; + + // Store current polylines visibility state + const wasPolyLayerVisible = map.hasLayer(mapsController.polylinesLayer); + + // Remove old polylines layer + if (mapsController.polylinesLayer) { + map.removeLayer(mapsController.polylinesLayer); + } + + // Create new polylines layer with updated coordinates + mapsController.polylinesLayer = createPolylinesLayer( + mapsController.markers, + map, + mapsController.timezone, + mapsController.routeOpacity, + mapsController.userSettings, + mapsController.distanceUnit + ); + + // Restore polylines visibility if it was visible before + if (wasPolyLayerVisible) { + mapsController.polylinesLayer.addTo(map); + } + } + + // Update popup content with new data + const updatedPopupContent = createPopupContent( + [ + data.latitude, + data.longitude, + data.id, + data.altitude, + data.timestamp, + data.velocity || 0 + ], + userSettings.timezone, + userSettings.distanceUnit + ); + this.setPopupContent(updatedPopupContent); + }) + .catch(error => { + console.error('Error updating point position:', error); + // Revert the marker position on error + this.setLatLng([lat, lon]); + }); }); }); } } +// Helper function to check if a point is connected to a polyline endpoint +function isConnectedToPoint(latLng, originalPoint, tolerance) { + // originalPoint is [lat, lng] array + const latMatch = Math.abs(latLng.lat - originalPoint[0]) < tolerance; + const lngMatch = Math.abs(latLng.lng - originalPoint[1]) < tolerance; + return latMatch && lngMatch; +} + export function createSimplifiedMarkers(markersData, renderer) { const distanceThreshold = 50; // meters const timeThreshold = 20000; // milliseconds (3 seconds) @@ -47,7 +206,6 @@ export function createSimplifiedMarkers(markersData, renderer) { if (index === 0) return; // Skip the first marker const [prevLat, prevLon, prevTimestamp] = previousMarker; - const [currLat, currLon, currTimestamp] = currentMarker; const timeDiff = currTimestamp - prevTimestamp; const distance = haversineDistance(prevLat, prevLon, currLat, currLon, 'km') * 1000; // Convert km to meters diff --git a/config/routes.rb b/config/routes.rb index 8d28efde..0befcca4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,7 +67,7 @@ Rails.application.routes.draw do get 'settings', to: 'settings#index' resources :areas, only: %i[index create update destroy] - resources :points, only: %i[index destroy] + resources :points, only: %i[index destroy update] resources :visits, only: %i[update] resources :stats, only: :index From 94e08d56e112dcc91728d41f589fe4745fae5034 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 19 Jan 2025 17:14:09 +0100 Subject: [PATCH 3/9] Fix point updating in the database --- app/javascript/maps/markers.js | 116 ++++++++++++--------------------- 1 file changed, 40 insertions(+), 76 deletions(-) diff --git a/app/javascript/maps/markers.js b/app/javascript/maps/markers.js index d4e67380..610a81dc 100644 --- a/app/javascript/maps/markers.js +++ b/app/javascript/maps/markers.js @@ -9,9 +9,8 @@ export function createMarkersArray(markersData, userSettings, apiKey) { } else { return markersData.map((marker, index) => { const [lat, lon] = marker; - const pointId = marker[2]; - const popupContent = createPopupContent(marker, userSettings.timezone, userSettings.distanceUnit); - let markerColor = marker[5] < 0 ? "orange" : "blue"; + const pointId = marker[6]; // ID is at index 6 + const markerColor = marker[5] < 0 ? "orange" : "blue"; return L.marker([lat, lon], { icon: L.divIcon({ @@ -26,10 +25,10 @@ export function createMarkersArray(markersData, userSettings, apiKey) { pointId: pointId, originalLat: lat, originalLng: lon, + markerData: marker, // Store the complete marker data renderer: renderer - }).bindPopup(popupContent) + }).bindPopup(createPopupContent(marker, userSettings.timezone, userSettings.distanceUnit)) .on('dragstart', function(e) { - console.log('Drag started', { index: this.options.pointIndex }); this.closePopup(); }) .on('drag', function(e) { @@ -38,13 +37,6 @@ export function createMarkersArray(markersData, userSettings, apiKey) { const pointIndex = e.target.options.pointIndex; const originalLat = e.target.options.originalLat; const originalLng = e.target.options.originalLng; - - console.log('Dragging point', { - pointIndex, - newPosition: newLatLng, - originalPosition: { lat: originalLat, lng: originalLng } - }); - // Find polylines by iterating through all map layers map.eachLayer((layer) => { // Check if this is a LayerGroup containing polylines @@ -60,10 +52,6 @@ export function createMarkersArray(markersData, userSettings, apiKey) { // Check and update start point if (Math.abs(coords[0].lat - originalLat) < tolerance && Math.abs(coords[0].lng - originalLng) < tolerance) { - console.log('Updating start point of segment', { - from: coords[0], - to: newLatLng - }); coords[0] = newLatLng; updated = true; } @@ -71,10 +59,6 @@ export function createMarkersArray(markersData, userSettings, apiKey) { // Check and update end point if (Math.abs(coords[1].lat - originalLat) < tolerance && Math.abs(coords[1].lng - originalLng) < tolerance) { - console.log('Updating end point of segment', { - from: coords[1], - to: newLatLng - }); coords[1] = newLatLng; updated = true; } @@ -96,19 +80,11 @@ export function createMarkersArray(markersData, userSettings, apiKey) { e.target.options.originalLng = newLatLng.lng; }) .on('dragend', function(e) { - console.log('Drag ended', { - finalPosition: e.target.getLatLng(), - pointIndex: e.target.options.pointIndex - }); const newLatLng = e.target.getLatLng(); const pointId = e.target.options.pointId; const pointIndex = e.target.options.pointIndex; + const originalMarkerData = e.target.options.markerData; - // Update the marker's position - this.setLatLng(newLatLng); - this.openPopup(); - - // Send API request to update point position fetch(`/api/v1/points/${pointId}`, { method: 'PATCH', headers: { @@ -118,68 +94,56 @@ export function createMarkersArray(markersData, userSettings, apiKey) { }, body: JSON.stringify({ point: { - latitude: newLatLng.lat, - longitude: newLatLng.lng + latitude: newLatLng.lat.toString(), + longitude: newLatLng.lng.toString() } }) }) .then(response => { if (!response.ok) { - throw new Error('Failed to update point position'); + throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then(data => { - // Update the markers array in the controller const map = e.target._map; - const mapsController = map.mapsController; - if (mapsController && mapsController.markers) { - mapsController.markers[pointIndex][0] = newLatLng.lat; - mapsController.markers[pointIndex][1] = newLatLng.lng; - - // Store current polylines visibility state - const wasPolyLayerVisible = map.hasLayer(mapsController.polylinesLayer); - - // Remove old polylines layer - if (mapsController.polylinesLayer) { - map.removeLayer(mapsController.polylinesLayer); - } - - // Create new polylines layer with updated coordinates - mapsController.polylinesLayer = createPolylinesLayer( - mapsController.markers, - map, - mapsController.timezone, - mapsController.routeOpacity, - mapsController.userSettings, - mapsController.distanceUnit - ); - - // Restore polylines visibility if it was visible before - if (wasPolyLayerVisible) { - mapsController.polylinesLayer.addTo(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); } } - // Update popup content with new data - const updatedPopupContent = createPopupContent( - [ - data.latitude, - data.longitude, - data.id, - data.altitude, - data.timestamp, - data.velocity || 0 - ], - userSettings.timezone, - userSettings.distanceUnit - ); - this.setPopupContent(updatedPopupContent); + // Create updated marker data array + const updatedMarkerData = [ + parseFloat(data.latitude), + parseFloat(data.longitude), + originalMarkerData[2], // battery + originalMarkerData[3], // altitude + originalMarkerData[4], // timestamp + originalMarkerData[5], // velocity + data.id, // id + originalMarkerData[7] // 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 position:', error); - // Revert the marker position on error - this.setLatLng([lat, lon]); + 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.'); }); }); }); From 64b141fa24e3cc0ede78baa24b6720f44ff7f2fa Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 19 Jan 2025 17:49:11 +0100 Subject: [PATCH 4/9] Highlight routes again --- app/javascript/maps/polylines.js | 179 ++++++++++++++++++++++--------- app/javascript/maps/popups.js | 3 + 2 files changed, 130 insertions(+), 52 deletions(-) diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index ba7e15cf..50abecc9 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -170,51 +170,54 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use let hoverPopup = null; - polylineGroup.on("mouseover", function (e) { - let closestSegment = null; - let minDistance = Infinity; - let currentSpeed = 0; + // Add events to both group and individual polylines + polylineGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + layer.on("mouseover", function (e) { + console.log("Individual polyline mouseover", e); + handleMouseOver(e); + }); - polylineGroup.eachLayer((layer) => { - if (layer instanceof L.Polyline) { - const layerLatLngs = layer.getLatLngs(); - const distance = pointToLineDistance(e.latlng, layerLatLngs[0], layerLatLngs[1]); + layer.on("mouseout", function (e) { + console.log("Individual polyline mouseout", e); + handleMouseOut(e); + }); + } + }); - if (distance < minDistance) { - minDistance = distance; - closestSegment = layer; + function handleMouseOver(e) { + console.log('Individual polyline mouseover', e); - const startIdx = polylineCoordinates.findIndex(p => { - const latMatch = Math.abs(p[0] - layerLatLngs[0].lat) < 0.0000001; - const lngMatch = Math.abs(p[1] - layerLatLngs[0].lng) < 0.0000001; - return latMatch && lngMatch; - }); + // Handle both direct layer events and group propagated events + const layer = e.layer || e.target; + let speed = 0; - if (startIdx !== -1 && startIdx < polylineCoordinates.length - 1) { - currentSpeed = calculateSpeed( - polylineCoordinates[startIdx], - polylineCoordinates[startIdx + 1] - ); - } + if (layer instanceof L.Polyline) { + // Get the coordinates array from the layer + const coords = layer.getLatLngs(); + if (coords && coords.length >= 2) { + const startPoint = coords[0]; + const endPoint = coords[coords.length - 1]; + + // Find the corresponding markers for these coordinates + const startMarkerData = polylineCoordinates.find(m => + m[0] === startPoint.lat && m[1] === startPoint.lng + ); + const endMarkerData = polylineCoordinates.find(m => + m[0] === endPoint.lat && m[1] === endPoint.lng + ); + + // Calculate speed if we have both markers + if (startMarkerData && endMarkerData) { + speed = startMarkerData[5] || endMarkerData[5] || 0; } } - }); + } - // Apply highlight style to all segments - polylineGroup.eachLayer((layer) => { - if (layer instanceof L.Polyline) { - const highlightStyle = { - weight: 5, - opacity: 1 - }; - - // Only change color to yellow if speed colors are disabled - if (!userSettings.speed_colored_routes) { - highlightStyle.color = '#ffff00'; - } - - layer.setStyle(highlightStyle); - } + layer.setStyle({ + weight: 8, + opacity: 0.8, + color: '#FFD700' }); startMarker.addTo(map); @@ -225,7 +228,7 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use End: ${lastTimestamp}
Duration: ${timeOnRoute}
Total Distance: ${formatDistance(totalDistance, distanceUnit)}
- Current Speed: ${Math.round(currentSpeed)} km/h + Current Speed: ${Math.round(speed)} km/h `; if (hoverPopup) { @@ -236,16 +239,15 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use .setLatLng(e.latlng) .setContent(popupContent) .openOn(map); - }); + } - polylineGroup.on("mouseout", function () { - // Restore original style + function handleMouseOut(e) { polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { const originalStyle = { weight: 3, opacity: userSettings.route_opacity, - color: layer.options.originalColor // Use the stored original color + color: layer.options.originalColor }; layer.setStyle(originalStyle); @@ -257,16 +259,31 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use } map.removeLayer(startMarker); map.removeLayer(endMarker); - }); + } + // Keep the original group events as a fallback + polylineGroup.on("mouseover", handleMouseOver); + polylineGroup.on("mouseout", handleMouseOut); + + // Keep the click event polylineGroup.on("click", function () { map.fitBounds(polylineGroup.getBounds()); }); } export function createPolylinesLayer(markers, map, timezone, routeOpacity, userSettings, distanceUnit) { - // Create a canvas renderer - const renderer = L.canvas({ padding: 0.5 }); + // Create a custom pane for our polylines with higher z-index + if (!map.getPane('polylinesPane')) { + map.createPane('polylinesPane'); + map.getPane('polylinesPane').style.zIndex = 450; // Above the default overlay pane (400) + } + + const renderer = L.canvas({ + padding: 0.5, + pane: 'polylinesPane' + }); + + console.log("Creating polylines layer with markers:", markers.length); const splitPolylines = []; let currentPolyline = []; @@ -295,9 +312,15 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS splitPolylines.push(currentPolyline); } - return L.layerGroup( - splitPolylines.map((polylineCoordinates) => { + console.log("Split into polyline groups:", splitPolylines.length); + + // Create the layer group with the polylines + const layerGroup = L.layerGroup( + splitPolylines.map((polylineCoordinates, groupIndex) => { + console.log(`Creating group ${groupIndex} with coordinates:`, polylineCoordinates.length); + const segmentGroup = L.featureGroup(); + const segments = []; for (let i = 0; i < polylineCoordinates.length - 1; i++) { const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]); @@ -309,25 +332,77 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS [polylineCoordinates[i + 1][0], polylineCoordinates[i + 1][1]] ], { - renderer: renderer, // Use canvas renderer + renderer: renderer, color: color, originalColor: color, opacity: routeOpacity, weight: 3, speed: speed, - startTime: polylineCoordinates[i][4], - endTime: polylineCoordinates[i + 1][4] + interactive: true, + pane: 'polylinesPane', + bubblingMouseEvents: false } ); + segments.push(segment); segmentGroup.addLayer(segment); } + // Add mouseover/mouseout to the entire group + segmentGroup.on('mouseover', function(e) { + console.log("Group mouseover", groupIndex); + L.DomEvent.stopPropagation(e); + segments.forEach(segment => { + segment.setStyle({ + weight: 8, + opacity: 1 + }); + if (map.hasLayer(segment)) { + segment.bringToFront(); + } + }); + }); + + segmentGroup.on('mouseout', function(e) { + console.log("Group mouseout", groupIndex); + L.DomEvent.stopPropagation(e); + segments.forEach(segment => { + segment.setStyle({ + weight: 3, + opacity: routeOpacity, + color: segment.options.originalColor + }); + }); + }); + + // Make the group interactive + segmentGroup.options.interactive = true; + segmentGroup.options.bubblingMouseEvents = false; + + // Add the hover functionality to the group addHighlightOnHover(segmentGroup, map, polylineCoordinates, userSettings, distanceUnit); return segmentGroup; }) - ).addTo(map); + ); + + // Add CSS to ensure our pane receives mouse events + const style = document.createElement('style'); + style.textContent = ` + .leaflet-polylinesPane-pane { + pointer-events: auto !important; + } + .leaflet-polylinesPane-pane canvas { + pointer-events: auto !important; + } + `; + document.head.appendChild(style); + + // Add to map and return + layerGroup.addTo(map); + console.log("Layer group added to map"); + + return layerGroup; } export function updatePolylinesColors(polylinesLayer, useSpeedColors) { diff --git a/app/javascript/maps/popups.js b/app/javascript/maps/popups.js index dee74dc5..cba49a22 100644 --- a/app/javascript/maps/popups.js +++ b/app/javascript/maps/popups.js @@ -8,6 +8,9 @@ export function createPopupContent(marker, timezone, distanceUnit) { marker[3] = marker[3] * 3.28084; } + // convert marker[5] from m/s to km/h and round to nearest integer + marker[5] = Math.round(marker[5] * 3.6); + return ` Timestamp: ${formatDate(marker[4], timezone)}
Latitude: ${marker[0]}
From 3ba42dceaf7e794ca257ba3e9d6ab877d5418c03 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 19 Jan 2025 18:05:21 +0100 Subject: [PATCH 5/9] Highlight on hover --- app/javascript/maps/polylines.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 50abecc9..2780dc1e 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -214,10 +214,22 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use } } - layer.setStyle({ - weight: 8, - opacity: 0.8, - color: '#FFD700' + // Apply style to all segments in the group + polylineGroup.eachLayer((segment) => { + if (segment instanceof L.Polyline) { + const newStyle = { + weight: 8, + opacity: 0.8 + }; + + // Only change color if speed-colored routes are not enabled + console.log("speed_colored_routes", userSettings.speed_colored_routes); + if (!userSettings.speed_colored_routes) { + newStyle.color = "yellow" + } + + segment.setStyle(newStyle); + } }); startMarker.addTo(map); From 70caaef9b5a6b2ba0f6c20463f7117d5cb328b21 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sun, 19 Jan 2025 18:06:56 +0100 Subject: [PATCH 6/9] Remove console logs --- app/javascript/maps/polylines.js | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 2780dc1e..4b548264 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -174,20 +174,16 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { layer.on("mouseover", function (e) { - console.log("Individual polyline mouseover", e); handleMouseOver(e); }); layer.on("mouseout", function (e) { - console.log("Individual polyline mouseout", e); handleMouseOut(e); }); } }); function handleMouseOver(e) { - console.log('Individual polyline mouseover', e); - // Handle both direct layer events and group propagated events const layer = e.layer || e.target; let speed = 0; @@ -219,11 +215,10 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use if (segment instanceof L.Polyline) { const newStyle = { weight: 8, - opacity: 0.8 + opacity: 1 }; // Only change color if speed-colored routes are not enabled - console.log("speed_colored_routes", userSettings.speed_colored_routes); if (!userSettings.speed_colored_routes) { newStyle.color = "yellow" } @@ -295,8 +290,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS pane: 'polylinesPane' }); - console.log("Creating polylines layer with markers:", markers.length); - const splitPolylines = []; let currentPolyline = []; const distanceThresholdMeters = parseInt(userSettings.meters_between_routes) || 500; @@ -324,13 +317,9 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS splitPolylines.push(currentPolyline); } - console.log("Split into polyline groups:", splitPolylines.length); - // Create the layer group with the polylines const layerGroup = L.layerGroup( splitPolylines.map((polylineCoordinates, groupIndex) => { - console.log(`Creating group ${groupIndex} with coordinates:`, polylineCoordinates.length); - const segmentGroup = L.featureGroup(); const segments = []; @@ -362,7 +351,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS // Add mouseover/mouseout to the entire group segmentGroup.on('mouseover', function(e) { - console.log("Group mouseover", groupIndex); L.DomEvent.stopPropagation(e); segments.forEach(segment => { segment.setStyle({ @@ -376,7 +364,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS }); segmentGroup.on('mouseout', function(e) { - console.log("Group mouseout", groupIndex); L.DomEvent.stopPropagation(e); segments.forEach(segment => { segment.setStyle({ @@ -412,7 +399,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS // Add to map and return layerGroup.addTo(map); - console.log("Layer group added to map"); return layerGroup; } From a50e9f664add44f0aed9e60a3c325cae94f4c978 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 20 Jan 2025 11:26:52 +0100 Subject: [PATCH 7/9] Highlight route on click --- app/javascript/maps/polylines.js | 213 +++++++++++++++++++++++-------- 1 file changed, 158 insertions(+), 55 deletions(-) diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 4b548264..e48479d3 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -169,6 +169,7 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use const endMarker = L.marker([endPoint[0], endPoint[1]], { icon: finishIcon }); let hoverPopup = null; + let clickedLayer = null; // Add events to both group and individual polylines polylineGroup.eachLayer((layer) => { @@ -180,6 +181,10 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use layer.on("mouseout", function (e) { handleMouseOut(e); }); + + layer.on("click", function (e) { + handleClick(e); + }); } }); @@ -189,44 +194,140 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use let speed = 0; if (layer instanceof L.Polyline) { - // Get the coordinates array from the layer - const coords = layer.getLatLngs(); - if (coords && coords.length >= 2) { - const startPoint = coords[0]; - const endPoint = coords[coords.length - 1]; + // Get the coordinates array from the layer + const coords = layer.getLatLngs(); + if (coords && coords.length >= 2) { + const startPoint = coords[0]; + const endPoint = coords[coords.length - 1]; - // Find the corresponding markers for these coordinates - const startMarkerData = polylineCoordinates.find(m => - m[0] === startPoint.lat && m[1] === startPoint.lng - ); - const endMarkerData = polylineCoordinates.find(m => - m[0] === endPoint.lat && m[1] === endPoint.lng - ); + // Find the corresponding markers for these coordinates + const startMarkerData = polylineCoordinates.find(m => + m[0] === startPoint.lat && m[1] === startPoint.lng + ); + const endMarkerData = polylineCoordinates.find(m => + m[0] === endPoint.lat && m[1] === endPoint.lng + ); - // Calculate speed if we have both markers - if (startMarkerData && endMarkerData) { - speed = startMarkerData[5] || endMarkerData[5] || 0; + // Calculate speed if we have both markers + if (startMarkerData && endMarkerData) { + speed = startMarkerData[5] || endMarkerData[5] || 0; + } } - } } - // Apply style to all segments in the group - polylineGroup.eachLayer((segment) => { - if (segment instanceof L.Polyline) { - const newStyle = { - weight: 8, - opacity: 1 - }; + // Don't apply hover styles if this is the clicked layer + if (!clickedLayer) { + // Apply style to all segments in the group + polylineGroup.eachLayer((segment) => { + if (segment instanceof L.Polyline) { + const newStyle = { + weight: 8, + opacity: 1 + }; - // Only change color if speed-colored routes are not enabled - if (!userSettings.speed_colored_routes) { - newStyle.color = "yellow" + // Only change color if speed-colored routes are not enabled + if (!userSettings.speed_colored_routes) { + newStyle.color = 'yellow'; // Highlight color + } + + segment.setStyle(newStyle); + } + }); + + startMarker.addTo(map); + endMarker.addTo(map); + + const popupContent = ` + Start: ${firstTimestamp}
+ End: ${lastTimestamp}
+ Duration: ${timeOnRoute}
+ Total Distance: ${formatDistance(totalDistance, distanceUnit)}
+ Current Speed: ${Math.round(speed)} km/h + `; + + if (hoverPopup) { + map.closePopup(hoverPopup); } - segment.setStyle(newStyle); - } + hoverPopup = L.popup() + .setLatLng(e.latlng) + .setContent(popupContent) + .openOn(map); + } + } + + function handleMouseOut(e) { + // If there's a clicked state, maintain it + if (clickedLayer && polylineGroup.clickedState) { + polylineGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + if (layer === clickedLayer || layer.options.originalPath === clickedLayer.options.originalPath) { + layer.setStyle(polylineGroup.clickedState.style); + } + } + }); + return; + } + + // Apply normal style only if there's no clicked layer + polylineGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + const originalStyle = { + weight: 3, + opacity: userSettings.route_opacity, + color: layer.options.originalColor + }; + layer.setStyle(originalStyle); + } }); + if (hoverPopup && !clickedLayer) { + map.closePopup(hoverPopup); + map.removeLayer(startMarker); + map.removeLayer(endMarker); + } + } + + function handleClick(e) { + const newClickedLayer = e.target; + + // If clicking the same route that's already clicked, do nothing + if (clickedLayer === newClickedLayer) { + return; + } + + // Store reference to previous clicked layer before updating + const previousClickedLayer = clickedLayer; + + // Update clicked layer reference + clickedLayer = newClickedLayer; + + // Reset previous clicked layer if it exists + if (previousClickedLayer) { + previousClickedLayer.setStyle({ + weight: 3, + opacity: userSettings.route_opacity, + color: previousClickedLayer.options.originalColor + }); + } + + // Define style for clicked state + const clickedStyle = { + weight: 8, + opacity: 1, + color: userSettings.speed_colored_routes ? clickedLayer.options.originalColor : 'yellow' + }; + + // Apply style to new clicked layer + clickedLayer.setStyle(clickedStyle); + clickedLayer.bringToFront(); + + // Update clicked state + polylineGroup.clickedState = { + layer: clickedLayer, + style: clickedStyle + }; + startMarker.addTo(map); endMarker.addTo(map); @@ -235,7 +336,7 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use End: ${lastTimestamp}
Duration: ${timeOnRoute}
Total Distance: ${formatDistance(totalDistance, distanceUnit)}
- Current Speed: ${Math.round(speed)} km/h + Current Speed: ${Math.round(clickedLayer.options.speed || 0)} km/h `; if (hoverPopup) { @@ -243,39 +344,41 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use } hoverPopup = L.popup() - .setLatLng(e.latlng) - .setContent(popupContent) - .openOn(map); + .setLatLng(e.latlng) + .setContent(popupContent) + .openOn(map); + + // Prevent the click event from propagating to the map + L.DomEvent.stopPropagation(e); } - function handleMouseOut(e) { - polylineGroup.eachLayer((layer) => { - if (layer instanceof L.Polyline) { - const originalStyle = { - weight: 3, - opacity: userSettings.route_opacity, - color: layer.options.originalColor - }; - - layer.setStyle(originalStyle); - } - }); - - if (hoverPopup) { - map.closePopup(hoverPopup); + // Reset highlight when clicking elsewhere on the map + map.on('click', function () { + if (clickedLayer) { + const clickedGroup = clickedLayer.polylineGroup || polylineGroup; + clickedGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + layer.setStyle({ + weight: 3, + opacity: userSettings.route_opacity, + color: layer.options.originalColor + }); + } + }); + clickedLayer = null; + clickedGroup.clickedState = null; } - map.removeLayer(startMarker); - map.removeLayer(endMarker); - } + if (hoverPopup) { + map.closePopup(hoverPopup); + map.removeLayer(startMarker); + map.removeLayer(endMarker); + } + }); // Keep the original group events as a fallback polylineGroup.on("mouseover", handleMouseOver); polylineGroup.on("mouseout", handleMouseOut); - - // Keep the click event - polylineGroup.on("click", function () { - map.fitBounds(polylineGroup.getBounds()); - }); + polylineGroup.on("click", handleClick); } export function createPolylinesLayer(markers, map, timezone, routeOpacity, userSettings, distanceUnit) { From 0e5381c16f1ba83f3e618d2fae5db78cfb61d6de Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 20 Jan 2025 11:41:45 +0100 Subject: [PATCH 8/9] Fix fog of war console error --- app/javascript/controllers/maps_controller.js | 289 ++++++------------ app/javascript/maps/fog_of_war.js | 16 +- 2 files changed, 106 insertions(+), 199 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 8a0afdf0..0fd4502f 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -98,35 +98,41 @@ export default class extends Controller { Photos: this.photoMarkers }; - // Add this new custom control BEFORE the scale control - const TestControl = L.Control.extend({ - onAdd: (map) => { - const div = L.DomUtil.create('div', 'leaflet-control'); - const distance = this.element.dataset.distance || '0'; - const pointsNumber = this.element.dataset.points_number || '0'; - const unit = this.distanceUnit === 'mi' ? 'mi' : 'km'; - div.innerHTML = `${distance} ${unit} | ${pointsNumber} points`; - div.style.backgroundColor = 'white'; - div.style.padding = '0 5px'; - div.style.marginRight = '5px'; - div.style.display = 'inline-block'; - return div; + // Initialize layer control first + this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map); + + // Add the toggle panel button + this.addTogglePanelButton(); + + // Check if we should open the panel based on localStorage or URL params + const urlParams = new URLSearchParams(window.location.search); + const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; + const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); + + // Always create the panel first + this.toggleRightPanel(); + + // Then hide it if it shouldn't be open + if (!isPanelOpen && !hasDateParams) { + const panel = document.querySelector('.leaflet-right-panel'); + if (panel) { + panel.style.display = 'none'; + localStorage.setItem('mapPanelOpen', 'false'); + } + } + + // Update event handlers + this.map.on('moveend', () => { + if (document.getElementById('fog')) { + this.updateFog(this.markers, this.clearFogRadius); } }); - // Add the test control first - new TestControl({ position: 'bottomright' }).addTo(this.map); - - // Then add scale control - L.control.scale({ - position: 'bottomright', - imperial: this.distanceUnit === 'mi', - metric: this.distanceUnit === 'km', - maxWidth: 120 - }).addTo(this.map) - - // Initialize layer control - this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map); + this.map.on('zoomend', () => { + if (document.getElementById('fog')) { + this.updateFog(this.markers, this.clearFogRadius); + } + }); // Fetch and draw areas when the map is loaded fetchAndDrawAreas(this.areasLayer, this.apiKey); @@ -205,39 +211,6 @@ export default class extends Controller { if (this.liveMapEnabled) { this.setupSubscription(); } - - // Add the toggle panel button - this.addTogglePanelButton(); - - // Check if we should open the panel based on localStorage or URL params - const urlParams = new URLSearchParams(window.location.search); - const isPanelOpen = localStorage.getItem('mapPanelOpen') === 'true'; - const hasDateParams = urlParams.has('start_at') && urlParams.has('end_at'); - - // Always create the panel first - this.toggleRightPanel(); - - // Then hide it if it shouldn't be open - if (!isPanelOpen && !hasDateParams) { - const panel = document.querySelector('.leaflet-right-panel'); - if (panel) { - panel.style.display = 'none'; - localStorage.setItem('mapPanelOpen', 'false'); - } - } - - // Update event handlers - this.map.on('moveend', () => { - if (document.getElementById('fog')) { - this.updateFog(this.markers, this.clearFogRadius); - } - }); - - this.map.on('zoomend', () => { - if (document.getElementById('fog')) { - this.updateFog(this.markers, this.clearFogRadius); - } - }); } disconnect() { @@ -786,164 +759,84 @@ export default class extends Controller { } updateMapWithNewSettings(newSettings) { - console.log('Updating map settings:', { - newSettings, - currentSettings: this.userSettings, - hasPolylines: !!this.polylinesLayer, - isVisible: this.polylinesLayer && this.map.hasLayer(this.polylinesLayer) - }); - // Show loading indicator const loadingDiv = document.createElement('div'); loadingDiv.className = 'map-loading-overlay'; loadingDiv.innerHTML = '
Updating map...
'; document.body.appendChild(loadingDiv); - // Debounce the heavy operations - const updateLayers = debounce(() => { - try { - // Store current layer visibility states - const layerStates = { - Points: this.map.hasLayer(this.markersLayer), - Routes: this.map.hasLayer(this.polylinesLayer), - Heatmap: this.map.hasLayer(this.heatmapLayer), - "Fog of War": this.map.hasLayer(this.fogOverlay), - "Scratch map": this.map.hasLayer(this.scratchLayer), - Areas: this.map.hasLayer(this.areasLayer), - Photos: this.map.hasLayer(this.photoMarkers) - }; - - // Check if speed_colored_routes setting has changed - if (newSettings.speed_colored_routes !== this.userSettings.speed_colored_routes) { - if (this.polylinesLayer) { - updatePolylinesColors( - this.polylinesLayer, - newSettings.speed_colored_routes - ); - } + try { + // Update settings first + if (newSettings.speed_colored_routes !== this.userSettings.speed_colored_routes) { + if (this.polylinesLayer) { + updatePolylinesColors( + this.polylinesLayer, + newSettings.speed_colored_routes + ); } - - // Update opacity if changed - if (newSettings.route_opacity !== this.userSettings.route_opacity) { - const newOpacity = parseFloat(newSettings.route_opacity) || 0.6; - if (this.polylinesLayer) { - updatePolylinesOpacity(this.polylinesLayer, newOpacity); - } - } - - // Update the local settings - this.userSettings = { ...this.userSettings, ...newSettings }; - this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6; - this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50; - - // Remove existing layer control - if (this.layerControl) { - this.map.removeControl(this.layerControl); - } - - // Create new controls layer object with proper initialization - const controlsLayer = { - Points: this.markersLayer || L.layerGroup(), - Routes: this.polylinesLayer || L.layerGroup(), - Heatmap: this.heatmapLayer || L.heatLayer([]), - "Fog of War": new this.fogOverlay(), - "Scratch map": this.scratchLayer || L.layerGroup(), - Areas: this.areasLayer || L.layerGroup(), - Photos: this.photoMarkers || L.layerGroup() - }; - - // Add new layer control - this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map); - - // Restore layer visibility states - Object.entries(layerStates).forEach(([name, wasVisible]) => { - const layer = controlsLayer[name]; - if (wasVisible && layer) { - layer.addTo(this.map); - } else if (layer && this.map.hasLayer(layer)) { - this.map.removeLayer(layer); - } - }); - - } catch (error) { - console.error('Error updating map settings:', error); - console.error(error.stack); - } finally { - // Remove loading indicator after all updates are complete - setTimeout(() => { - document.body.removeChild(loadingDiv); - }, 500); // Give a small delay to ensure all batches are processed } - }, 250); - updateLayers(); - } - - getLayerControlStates() { - const controls = {}; - - this.map.eachLayer((layer) => { - const layerName = this.getLayerName(layer); - - if (layerName) { - controls[layerName] = this.map.hasLayer(layer); + if (newSettings.route_opacity !== this.userSettings.route_opacity) { + const newOpacity = parseFloat(newSettings.route_opacity) || 0.6; + if (this.polylinesLayer) { + updatePolylinesOpacity(this.polylinesLayer, newOpacity); + } } - }); - return controls; - } + // Update the local settings + this.userSettings = { ...this.userSettings, ...newSettings }; + this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6; + this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50; - getLayerName(layer) { - const controlLayers = { - Points: this.markersLayer, - Routes: this.polylinesLayer, - Heatmap: this.heatmapLayer, - "Fog of War": this.fogOverlay, - Areas: this.areasLayer, - }; + // Store current layer states + const layerStates = { + Points: this.map.hasLayer(this.markersLayer), + Routes: this.map.hasLayer(this.polylinesLayer), + Heatmap: this.map.hasLayer(this.heatmapLayer), + "Fog of War": this.map.hasLayer(this.fogOverlay), + "Scratch map": this.map.hasLayer(this.scratchLayer), + Areas: this.map.hasLayer(this.areasLayer), + Photos: this.map.hasLayer(this.photoMarkers) + }; - for (const [name, val] of Object.entries(controlLayers)) { - if (val && val.hasLayer && layer && val.hasLayer(layer)) // Check if the group layer contains the current layer - return name; - } + // Remove only the layer control + if (this.layerControl) { + this.map.removeControl(this.layerControl); + } - // Direct instance matching - for (const [name, val] of Object.entries(controlLayers)) { - if (val === layer) return name; - } + // Create new controls layer object + const controlsLayer = { + Points: this.markersLayer || L.layerGroup(), + Routes: this.polylinesLayer || L.layerGroup(), + Heatmap: this.heatmapLayer || L.heatLayer([]), + "Fog of War": new this.fogOverlay(), + "Scratch map": this.scratchLayer || L.layerGroup(), + Areas: this.areasLayer || L.layerGroup(), + Photos: this.photoMarkers || L.layerGroup() + }; - return undefined; // Indicate no matching layer name found - } + // Re-add the layer control in the same position + this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map); - applyLayerControlStates(states) { - console.log('Applying layer states:', states); - - const layerControl = { - Points: this.markersLayer, - Routes: this.polylinesLayer, - Heatmap: this.heatmapLayer, - "Fog of War": this.fogOverlay, - Areas: this.areasLayer, - }; - - for (const [name, isVisible] of Object.entries(states)) { - const layer = layerControl[name]; - console.log(`Processing layer ${name}:`, { layer, isVisible }); - - if (layer) { - if (isVisible && !this.map.hasLayer(layer)) { - console.log(`Adding layer ${name} to map`); - this.map.addLayer(layer); - } else if (!isVisible && this.map.hasLayer(layer)) { - console.log(`Removing layer ${name} from map`); + // Restore layer visibility states + Object.entries(layerStates).forEach(([name, wasVisible]) => { + const layer = controlsLayer[name]; + if (wasVisible && layer) { + layer.addTo(this.map); + } else if (layer && this.map.hasLayer(layer)) { this.map.removeLayer(layer); } - } - } + }); - // Ensure the layer control reflects the current state - this.map.removeControl(this.layerControl); - this.layerControl = L.control.layers(this.baseMaps(), layerControl).addTo(this.map); + } catch (error) { + console.error('Error updating map settings:', error); + console.error(error.stack); + } finally { + // Remove loading indicator + setTimeout(() => { + document.body.removeChild(loadingDiv); + }, 500); + } } createPhotoMarker(photo) { diff --git a/app/javascript/maps/fog_of_war.js b/app/javascript/maps/fog_of_war.js index 482a161e..8e910274 100644 --- a/app/javascript/maps/fog_of_war.js +++ b/app/javascript/maps/fog_of_war.js @@ -25,7 +25,8 @@ export function initializeFogCanvas(map) { export function drawFogCanvas(map, markers, clearFogRadius) { const fog = document.getElementById('fog'); - if (!fog) return; + // Return early if fog element doesn't exist or isn't a canvas + if (!fog || !(fog instanceof HTMLCanvasElement)) return; const ctx = fog.getContext('2d'); if (!ctx) return; @@ -83,12 +84,25 @@ export function createFogOverlay() { return L.Layer.extend({ onAdd: (map) => { initializeFogCanvas(map); + + // Add drag event handlers to update fog during marker movement + map.on('drag', () => { + const fog = document.getElementById('fog'); + if (fog) { + // Update fog canvas position to match map position + const mapPos = map.getContainer().getBoundingClientRect(); + fog.style.left = `${mapPos.left}px`; + fog.style.top = `${mapPos.top}px`; + } + }); }, onRemove: (map) => { const fog = document.getElementById('fog'); if (fog) { fog.remove(); } + // Clean up event listener + map.off('drag'); } }); } From 9970d6313401e0ab467c86b66b1223d2e72edc93 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 20 Jan 2025 11:46:57 +0100 Subject: [PATCH 9/9] Return scale and stats controls to map --- CHANGELOG.md | 10 ++++++- app/javascript/controllers/maps_controller.js | 29 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a34d9ea6..be1d0faa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -# 0.22.4 - 2025-01-15 +# 0.22.4 - 2025-01-20 + +### Added + +- You can now drag-n-drop a point on the map to update its position. Enable the "Points" layer on the map to see the points. ### Changed - Run seeds even in prod env so Unraid users could have default user. - Precompile assets in production env using dummy secret key base. +### Fixed + +- Fixed a bug where route wasn't highlighted when it was hovered or clicked. + # 0.22.3 - 2025-01-14 ### Changed diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 0fd4502f..313b477d 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -61,6 +61,35 @@ export default class extends Controller { this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 14); + // Add scale control + L.control.scale({ + position: 'bottomright', + imperial: this.distanceUnit === 'mi', + metric: this.distanceUnit === 'km', + maxWidth: 120 + }).addTo(this.map) + + // Add stats control + const StatsControl = L.Control.extend({ + options: { + position: 'bottomright' + }, + onAdd: (map) => { + const div = L.DomUtil.create('div', 'leaflet-control-stats'); + const distance = this.element.dataset.distance || '0'; + const pointsNumber = this.element.dataset.points_number || '0'; + const unit = this.distanceUnit === 'mi' ? 'mi' : 'km'; + div.innerHTML = `${distance} ${unit} | ${pointsNumber} points`; + div.style.backgroundColor = 'white'; + div.style.padding = '0 5px'; + div.style.marginRight = '5px'; + div.style.display = 'inline-block'; + return div; + } + }); + + new StatsControl().addTo(this.map); + // Set the maximum bounds to prevent infinite scroll var southWest = L.latLng(-120, -210); var northEast = L.latLng(120, 210);