From 43e4e8d81a80fd2dcc96994b806452b7169d865d Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Fri, 10 Jan 2025 23:03:07 +0100 Subject: [PATCH 01/11] Color polylines based on speed --- app/javascript/controllers/maps_controller.js | 18 +- app/javascript/maps/polylines.js | 223 +++++++++++++----- 2 files changed, 174 insertions(+), 67 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 40893763..028240b2 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -758,10 +758,9 @@ export default class extends Controller { this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50; this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6; - // Preserve existing layer instances if they exist + // Preserve existing layers except polylines which need to be recreated const preserveLayers = { Points: this.markersLayer, - Polylines: this.polylinesLayer, Heatmap: this.heatmapLayer, "Fog of War": this.fogOverlay, Areas: this.areasLayer, @@ -774,12 +773,15 @@ export default class extends Controller { } }); - // Recreate layers only if they don't exist - this.markersLayer = preserveLayers.Points || L.layerGroup(createMarkersArray(this.markers, newSettings)); - this.polylinesLayer = preserveLayers.Polylines || createPolylinesLayer(this.markers, this.map, this.timezone, this.routeOpacity, this.userSettings, this.distanceUnit); - this.heatmapLayer = preserveLayers.Heatmap || L.heatLayer(this.markers.map((element) => [element[0], element[1], 0.2]), { radius: 20 }); - this.fogOverlay = preserveLayers["Fog of War"] || L.layerGroup(); - this.areasLayer = preserveLayers.Areas || L.layerGroup(); + // Recreate polylines layer with new settings + this.polylinesLayer = createPolylinesLayer( + this.markers, + this.map, + this.timezone, + this.routeOpacity, + newSettings, + this.distanceUnit + ); // Redraw areas fetchAndDrawAreas(this.areasLayer, this.apiKey); diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 2c09022d..bd0a1595 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -4,11 +4,50 @@ import { getUrlParameter } from "../maps/helpers"; import { minutesToDaysHoursMinutes } from "../maps/helpers"; import { haversineDistance } from "../maps/helpers"; -export function addHighlightOnHover(polyline, map, polylineCoordinates, userSettings, distanceUnit) { - const originalStyle = { color: "blue", opacity: userSettings.routeOpacity, weight: 3 }; - const highlightStyle = { color: "yellow", opacity: 1, weight: 5 }; +function getSpeedColor(speedKmh) { + console.log('Speed to color:', speedKmh + ' km/h'); - polyline.setStyle(originalStyle); + if (speedKmh > 100) { + console.log('Red - Very fast'); + return '#FF0000'; + } + if (speedKmh > 70) { + console.log('Orange - Fast'); + return '#FFA500'; + } + if (speedKmh > 40) { + console.log('Yellow - Moderate'); + return '#FFFF00'; + } + if (speedKmh > 20) { + console.log('Light green - Normal'); + return '#90EE90'; + } + console.log('Green - Slow'); + return '#008000'; +} + +function calculateSpeed(point1, point2) { + const distanceKm = haversineDistance(point1[0], point1[1], point2[0], point2[1]); // in kilometers + const timeDiffSeconds = point2[4] - point1[4]; + + // Convert to km/h: (kilometers / seconds) * (3600 seconds / hour) + const speed = (distanceKm / timeDiffSeconds) * 3600; + + console.log('Speed calculation:', { + distance: distanceKm + ' km', + timeDiff: timeDiffSeconds + ' seconds', + speed: speed + ' km/h', + point1: point1, + point2: point2 + }); + + return speed; +} + +export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, userSettings, distanceUnit) { + const highlightStyle = { opacity: 1, weight: 5 }; + const normalStyle = { opacity: userSettings.routeOpacity, weight: 3 }; const startPoint = polylineCoordinates[0]; const endPoint = polylineCoordinates[polylineCoordinates.length - 1]; @@ -28,66 +67,112 @@ export function addHighlightOnHover(polyline, map, polylineCoordinates, userSett const startIcon = L.divIcon({ html: "🚥", className: "emoji-icon" }); const finishIcon = L.divIcon({ html: "🏁", className: "emoji-icon" }); - const isDebugMode = getUrlParameter("debug") === "true"; - - let popupContent = ` - Start: ${firstTimestamp}
- End: ${lastTimestamp}
- Duration: ${timeOnRoute}
- Total Distance: ${formatDistance(totalDistance, distanceUnit)}
- `; - - if (isDebugMode) { - const prevPoint = polylineCoordinates[0]; - const nextPoint = polylineCoordinates[polylineCoordinates.length - 1]; - const distanceToPrev = haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]); - const distanceToNext = haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]); - - const timeBetweenPrev = Math.round((startPoint[4] - prevPoint[4]) / 60); - const timeBetweenNext = Math.round((endPoint[4] - nextPoint[4]) / 60); - const pointsNumber = polylineCoordinates.length; - - popupContent += ` - Prev Route: ${Math.round(distanceToPrev)}m and ${minutesToDaysHoursMinutes(timeBetweenPrev)} away
- Next Route: ${Math.round(distanceToNext)}m and ${minutesToDaysHoursMinutes(timeBetweenNext)} away
- Points: ${pointsNumber}
- `; - } - - const startMarker = L.marker([startPoint[0], startPoint[1]], { icon: startIcon }).bindPopup(`Start: ${firstTimestamp}`); - const endMarker = L.marker([endPoint[0], endPoint[1]], { icon: finishIcon }).bindPopup(popupContent); + const startMarker = L.marker([startPoint[0], startPoint[1]], { icon: startIcon }); + const endMarker = L.marker([endPoint[0], endPoint[1]], { icon: finishIcon }); let hoverPopup = null; - polyline.on("mouseover", function (e) { - polyline.setStyle(highlightStyle); + polylineGroup.on("mouseover", function (e) { + // Find the closest segment and its speed + let closestSegment = null; + let minDistance = Infinity; + let currentSpeed = 0; + + polylineGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + const layerLatLngs = layer.getLatLngs(); + const distance = L.LineUtil.pointToSegmentDistance( + e.latlng, + layerLatLngs[0], + layerLatLngs[1] + ); + + if (distance < minDistance) { + minDistance = distance; + closestSegment = layer; + + // Get the coordinates of the segment + const startPoint = layerLatLngs[0]; + const endPoint = layerLatLngs[1]; + + console.log('Closest segment found:', { + startPoint, + endPoint, + distance + }); + + // Find matching points in polylineCoordinates + const startIdx = polylineCoordinates.findIndex(p => { + const latMatch = Math.abs(p[0] - startPoint.lat) < 0.0000001; + const lngMatch = Math.abs(p[1] - startPoint.lng) < 0.0000001; + return latMatch && lngMatch; + }); + + console.log('Start point index:', startIdx); + console.log('Original point:', startIdx !== -1 ? polylineCoordinates[startIdx] : 'not found'); + + if (startIdx !== -1 && startIdx < polylineCoordinates.length - 1) { + currentSpeed = calculateSpeed( + polylineCoordinates[startIdx], + polylineCoordinates[startIdx + 1] + ); + console.log('Speed calculated:', currentSpeed); + } + } + } + }); + + // Highlight all segments in the group + polylineGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + layer.setStyle({ + ...highlightStyle, + color: layer.options.originalColor + }); + } + }); + startMarker.addTo(map); endMarker.addTo(map); - const latLng = e.latlng; + const popupContent = ` + Start: ${firstTimestamp}
+ End: ${lastTimestamp}
+ Duration: ${timeOnRoute}
+ Total Distance: ${formatDistance(totalDistance, distanceUnit)}
+ Current Speed: ${Math.round(currentSpeed)} km/h + `; + if (hoverPopup) { map.closePopup(hoverPopup); } + hoverPopup = L.popup() - .setLatLng(latLng) + .setLatLng(e.latlng) .setContent(popupContent) .openOn(map); }); - polyline.on("mouseout", function () { - polyline.setStyle(originalStyle); - map.closePopup(hoverPopup); + polylineGroup.on("mouseout", function () { + // Restore original styles for all segments + polylineGroup.eachLayer((layer) => { + if (layer instanceof L.Polyline) { + layer.setStyle({ + ...normalStyle, + color: layer.options.originalColor + }); + } + }); + + if (hoverPopup) { + map.closePopup(hoverPopup); + } map.removeLayer(startMarker); map.removeLayer(endMarker); }); - polyline.on("click", function () { - map.fitBounds(polyline.getBounds()); - }); - - // Close the popup when clicking elsewhere on the map - map.on("click", function () { - map.closePopup(hoverPopup); + polylineGroup.on("click", function () { + map.fitBounds(polylineGroup.getBounds()); }); } @@ -97,6 +182,7 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS const distanceThresholdMeters = parseInt(userSettings.meters_between_routes) || 500; const timeThresholdMinutes = parseInt(userSettings.minutes_between_routes) || 60; + // Split into separate polylines based on distance/time thresholds for (let i = 0, len = markers.length; i < len; i++) { if (currentPolyline.length === 0) { currentPolyline.push(markers[i]); @@ -121,26 +207,45 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS return L.layerGroup( splitPolylines.map((polylineCoordinates) => { - const latLngs = polylineCoordinates.map((point) => [point[0], point[1]]); - const polyline = L.polyline(latLngs, { - color: "blue", - opacity: 0.6, - weight: 3, - zIndexOffset: 400, - pane: 'overlayPane' - }); + const segmentGroup = L.featureGroup(); - addHighlightOnHover(polyline, map, polylineCoordinates, userSettings, distanceUnit); + // Create segments with different colors based on speed + for (let i = 0; i < polylineCoordinates.length - 1; i++) { + const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]); + const color = getSpeedColor(speed); - return polyline; + const segment = L.polyline( + [ + [polylineCoordinates[i][0], polylineCoordinates[i][1]], + [polylineCoordinates[i + 1][0], polylineCoordinates[i + 1][1]] + ], + { + color: color, + originalColor: color, + opacity: routeOpacity, + weight: 3 + } + ); + + segmentGroup.addLayer(segment); + } + + // Add hover effect to the entire group of segments + addHighlightOnHover(segmentGroup, map, polylineCoordinates, userSettings, distanceUnit); + + return segmentGroup; }) ).addTo(map); } export function updatePolylinesOpacity(polylinesLayer, opacity) { - polylinesLayer.eachLayer((layer) => { - if (layer instanceof L.Polyline) { - layer.setStyle({ opacity: opacity }); + polylinesLayer.eachLayer((groupLayer) => { + if (groupLayer instanceof L.LayerGroup) { + groupLayer.eachLayer((segment) => { + if (segment instanceof L.Polyline) { + segment.setStyle({ opacity: opacity }); + } + }); } }); } From 2e18b35e3cd30b712dad1c5bbfd0effe0a2b2a10 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Sat, 11 Jan 2025 00:42:44 +0100 Subject: [PATCH 02/11] Add settings for speed-colored polylines --- app/controllers/api/v1/settings_controller.rb | 3 +- app/javascript/controllers/maps_controller.js | 192 ++++++++------ app/javascript/maps/polylines.js | 238 +++++++++++++----- 3 files changed, 294 insertions(+), 139 deletions(-) diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb index f87d9df7..b15bad16 100644 --- a/app/controllers/api/v1/settings_controller.rb +++ b/app/controllers/api/v1/settings_controller.rb @@ -27,7 +27,8 @@ class Api::V1::SettingsController < ApiController :meters_between_routes, :minutes_between_routes, :fog_of_war_meters, :time_threshold_minutes, :merge_threshold_minutes, :route_opacity, :preferred_map_layer, :points_rendering_mode, :live_map_enabled, - :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key + :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key, + :speed_colored_polylines ) end end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 028240b2..fc60bf75 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -5,8 +5,13 @@ import consumer from "../channels/consumer"; import { createMarkersArray } from "../maps/markers"; -import { createPolylinesLayer } from "../maps/polylines"; -import { updatePolylinesOpacity } from "../maps/polylines"; +import { + createPolylinesLayer, + updatePolylinesOpacity, + updatePolylinesColors, + calculateSpeed, + getSpeedColor +} from "../maps/polylines"; import { fetchAndDrawAreas } from "../maps/areas"; import { handleAreaCreated } from "../maps/areas"; @@ -27,6 +32,18 @@ import { countryCodesMap } from "../maps/country_codes"; import "leaflet-draw"; +function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; +} + export default class extends Controller { static targets = ["container"]; @@ -48,6 +65,7 @@ export default class extends Controller { this.pointsRenderingMode = this.userSettings.points_rendering_mode || "raw"; this.liveMapEnabled = this.userSettings.live_map_enabled || false; this.countryCodesMap = countryCodesMap(); + this.speedColoredPolylines = this.userSettings.speed_colored_polylines || false; this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111]; @@ -677,6 +695,12 @@ export default class extends Controller { + + `; @@ -717,8 +741,13 @@ export default class extends Controller { } } + speedColoredPolylinesChecked() { + return this.userSettings.speed_colored_polylines ? 'checked' : ''; + } + updateSettings(event) { event.preventDefault(); + console.log('Form submitted'); fetch(`/api/v1/settings?api_key=${this.apiKey}`, { method: 'PATCH', @@ -732,12 +761,14 @@ export default class extends Controller { time_threshold_minutes: event.target.time_threshold_minutes.value, merge_threshold_minutes: event.target.merge_threshold_minutes.value, points_rendering_mode: event.target.points_rendering_mode.value, - live_map_enabled: event.target.live_map_enabled.checked + live_map_enabled: event.target.live_map_enabled.checked, + speed_colored_polylines: event.target.speed_colored_polylines.checked }, }), }) .then((response) => response.json()) .then((data) => { + console.log('Settings update response:', data); if (data.status === 'success') { showFlashMessage('notice', data.message); this.updateMapWithNewSettings(data.settings); @@ -748,86 +779,101 @@ export default class extends Controller { } else { showFlashMessage('error', data.message); } + }) + .catch(error => { + console.error('Settings update error:', error); + showFlashMessage('error', 'Failed to update settings'); }); } updateMapWithNewSettings(newSettings) { + console.log('Updating map settings:', { + newSettings, + currentSettings: this.userSettings, + hasPolylines: !!this.polylinesLayer, + isVisible: this.polylinesLayer && this.map.hasLayer(this.polylinesLayer) + }); + + // Store current visibility state + const wasPolylinesVisible = this.polylinesLayer && this.map.hasLayer(this.polylinesLayer); const currentLayerStates = this.getLayerControlStates(); - // Update local state with new settings - this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50; - this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6; + // Show loading indicator + const loadingDiv = document.createElement('div'); + loadingDiv.className = 'map-loading-overlay'; + loadingDiv.innerHTML = '
Updating map...
'; + document.body.appendChild(loadingDiv); - // Preserve existing layers except polylines which need to be recreated - const preserveLayers = { - Points: this.markersLayer, - Heatmap: this.heatmapLayer, - "Fog of War": this.fogOverlay, - Areas: this.areasLayer, - }; + // Debounce the heavy operations + const updateLayers = debounce(() => { + try { + // Check if speed_colored_polylines setting has changed + if (newSettings.speed_colored_polylines !== this.userSettings.speed_colored_polylines) { + console.log('Speed colored polylines setting changed:', { + old: this.userSettings.speed_colored_polylines, + new: newSettings.speed_colored_polylines + }); - // Clear all layers except base layers - this.map.eachLayer((layer) => { - if (!(layer instanceof L.TileLayer)) { - this.map.removeLayer(layer); + if (this.polylinesLayer) { + console.log('Starting polylines color update'); + + // Update colors without removing the layer + this.polylinesLayer.eachLayer(groupLayer => { + if (groupLayer instanceof L.LayerGroup || groupLayer instanceof L.FeatureGroup) { + groupLayer.eachLayer(segment => { + if (segment instanceof L.Polyline) { + const latLngs = segment.getLatLngs(); + const point1 = [latLngs[0].lat, latLngs[0].lng]; + const point2 = [latLngs[1].lat, latLngs[1].lng]; + + const speed = calculateSpeed( + [...point1, 0, segment.options.startTime], + [...point2, 0, segment.options.endTime] + ); + + const newColor = newSettings.speed_colored_polylines ? + getSpeedColor(speed, true) : + '#0000ff'; + + segment.setStyle({ + color: newColor, + originalColor: newColor + }); + } + }); + } + }); + + console.log('Finished polylines color update'); + } + } + + // Check if route opacity has 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; + + // Reapply layer states + this.applyLayerControlStates(currentLayerStates); + + } catch (error) { + console.error('Error updating map settings:', error); + console.error(error.stack); + } finally { + // Remove loading indicator + document.body.removeChild(loadingDiv); } - }); + }, 250); - // Recreate polylines layer with new settings - this.polylinesLayer = createPolylinesLayer( - this.markers, - this.map, - this.timezone, - this.routeOpacity, - newSettings, - this.distanceUnit - ); - - // Redraw areas - fetchAndDrawAreas(this.areasLayer, this.apiKey); - - let fogEnabled = false; - document.getElementById('fog').style.display = 'none'; - - this.map.on('overlayadd', (e) => { - if (e.name === 'Fog of War') { - fogEnabled = true; - document.getElementById('fog').style.display = 'block'; - this.updateFog(this.markers, this.clearFogRadius); - } - }); - - this.map.on('overlayremove', (e) => { - if (e.name === 'Fog of War') { - fogEnabled = false; - document.getElementById('fog').style.display = 'none'; - } - }); - - this.map.on('zoomend moveend', () => { - if (fogEnabled) { - this.updateFog(this.markers, this.clearFogRadius); - } - }); - - this.addLastMarker(this.map, this.markers); - this.addEventListeners(); - this.initializeDrawControl(); - updatePolylinesOpacity(this.polylinesLayer, this.routeOpacity); - - this.map.on('overlayadd', (e) => { - if (e.name === 'Areas') { - this.map.addControl(this.drawControl); - } - }); - - this.map.on('overlayremove', (e) => { - if (e.name === 'Areas') { - this.map.removeControl(this.drawControl); - } - }); - - this.applyLayerControlStates(currentLayerStates); + updateLayers(); } getLayerControlStates() { diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index bd0a1595..eb9ca95f 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -4,50 +4,137 @@ import { getUrlParameter } from "../maps/helpers"; import { minutesToDaysHoursMinutes } from "../maps/helpers"; import { haversineDistance } from "../maps/helpers"; -function getSpeedColor(speedKmh) { - console.log('Speed to color:', speedKmh + ' km/h'); +function pointToLineDistance(point, lineStart, lineEnd) { + const x = point.lat; + const y = point.lng; + const x1 = lineStart.lat; + const y1 = lineStart.lng; + const x2 = lineEnd.lat; + const y2 = lineEnd.lng; - if (speedKmh > 100) { - console.log('Red - Very fast'); - return '#FF0000'; + const A = x - x1; + const B = y - y1; + const C = x2 - x1; + const D = y2 - y1; + + const dot = A * C + B * D; + const lenSq = C * C + D * D; + let param = -1; + + if (lenSq !== 0) { + param = dot / lenSq; } - if (speedKmh > 70) { - console.log('Orange - Fast'); - return '#FFA500'; + + let xx, yy; + + if (param < 0) { + xx = x1; + yy = y1; + } else if (param > 1) { + xx = x2; + yy = y2; + } else { + xx = x1 + param * C; + yy = y1 + param * D; } - if (speedKmh > 40) { - console.log('Yellow - Moderate'); - return '#FFFF00'; - } - if (speedKmh > 20) { - console.log('Light green - Normal'); - return '#90EE90'; - } - console.log('Green - Slow'); - return '#008000'; + + const dx = x - xx; + const dy = y - yy; + + return Math.sqrt(dx * dx + dy * dy); } -function calculateSpeed(point1, point2) { +export function calculateSpeed(point1, point2) { const distanceKm = haversineDistance(point1[0], point1[1], point2[0], point2[1]); // in kilometers const timeDiffSeconds = point2[4] - point1[4]; - // Convert to km/h: (kilometers / seconds) * (3600 seconds / hour) - const speed = (distanceKm / timeDiffSeconds) * 3600; + // Handle edge cases + if (timeDiffSeconds <= 0 || distanceKm <= 0) { + return 0; + } - console.log('Speed calculation:', { - distance: distanceKm + ' km', - timeDiff: timeDiffSeconds + ' seconds', - speed: speed + ' km/h', - point1: point1, - point2: point2 - }); + const speedKmh = (distanceKm / timeDiffSeconds) * 3600; // Convert to km/h - return speed; + // Cap speed at reasonable maximum (e.g., 150 km/h) + const MAX_SPEED = 150; + return Math.min(speedKmh, MAX_SPEED); +} + +export function getSpeedColor(speedKmh, useSpeedColors) { + if (!useSpeedColors) { + return '#0000ff'; // Default blue color + } + + // Existing speed-based color logic + const colorStops = [ + { speed: 0, color: '#00ff00' }, // Stationary/very slow (neon green) + { speed: 15, color: '#00ffff' }, // Walking/jogging (neon cyan) + { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (neon magenta) + { speed: 50, color: '#ff3300' }, // Urban driving (neon orange-red) + { speed: 100, color: '#ffff00' } // Highway driving (neon yellow) + ]; + + // Find the appropriate color segment + for (let i = 1; i < colorStops.length; i++) { + if (speedKmh <= colorStops[i].speed) { + // Calculate how far we are between the two speeds (0-1) + const ratio = (speedKmh - colorStops[i-1].speed) / (colorStops[i].speed - colorStops[i-1].speed); + + // Convert hex to RGB for interpolation + const color1 = hexToRGB(colorStops[i-1].color); + const color2 = hexToRGB(colorStops[i].color); + + // Interpolate between the two colors + const r = Math.round(color1.r + (color2.r - color1.r) * ratio); + const g = Math.round(color1.g + (color2.g - color1.g) * ratio); + const b = Math.round(color1.b + (color2.b - color1.b) * ratio); + + return `rgb(${r}, ${g}, ${b})`; + } + } + + // If speed is higher than our highest threshold, return the last color + return colorStops[colorStops.length - 1].color; +} + +// Helper function to convert hex to RGB +function hexToRGB(hex) { + const r = parseInt(hex.slice(1, 3), 16); + const g = parseInt(hex.slice(3, 5), 16); + const b = parseInt(hex.slice(5, 7), 16); + return { r, g, b }; +} + +// Add new function for batch processing +function processInBatches(items, batchSize, processFn) { + let index = 0; + + function processNextBatch() { + const batch = items.slice(index, index + batchSize); + batch.forEach(processFn); + + index += batchSize; + + if (index < items.length) { + // Schedule next batch using requestAnimationFrame + window.requestAnimationFrame(processNextBatch); + } + } + + processNextBatch(); } export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, userSettings, distanceUnit) { - const highlightStyle = { opacity: 1, weight: 5 }; - const normalStyle = { opacity: userSettings.routeOpacity, weight: 3 }; + const highlightStyle = { + opacity: 1, + weight: 5, + color: userSettings.speed_colored_polylines ? null : '#ffff00' // Yellow highlight if not using speed colors + }; + const normalStyle = { + opacity: userSettings.routeOpacity, + weight: 3, + color: userSettings.speed_colored_polylines ? null : '#0000ff' // Blue normal if not using speed colors + }; const startPoint = polylineCoordinates[0]; const endPoint = polylineCoordinates[polylineCoordinates.length - 1]; @@ -73,7 +160,6 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use let hoverPopup = null; polylineGroup.on("mouseover", function (e) { - // Find the closest segment and its speed let closestSegment = null; let minDistance = Infinity; let currentSpeed = 0; @@ -81,53 +167,33 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { const layerLatLngs = layer.getLatLngs(); - const distance = L.LineUtil.pointToSegmentDistance( - e.latlng, - layerLatLngs[0], - layerLatLngs[1] - ); + const distance = pointToLineDistance(e.latlng, layerLatLngs[0], layerLatLngs[1]); if (distance < minDistance) { minDistance = distance; closestSegment = layer; - // Get the coordinates of the segment - const startPoint = layerLatLngs[0]; - const endPoint = layerLatLngs[1]; - - console.log('Closest segment found:', { - startPoint, - endPoint, - distance - }); - - // Find matching points in polylineCoordinates const startIdx = polylineCoordinates.findIndex(p => { - const latMatch = Math.abs(p[0] - startPoint.lat) < 0.0000001; - const lngMatch = Math.abs(p[1] - startPoint.lng) < 0.0000001; + 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; }); - console.log('Start point index:', startIdx); - console.log('Original point:', startIdx !== -1 ? polylineCoordinates[startIdx] : 'not found'); - if (startIdx !== -1 && startIdx < polylineCoordinates.length - 1) { currentSpeed = calculateSpeed( polylineCoordinates[startIdx], polylineCoordinates[startIdx + 1] ); - console.log('Speed calculated:', currentSpeed); } } } }); - // Highlight all segments in the group polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { layer.setStyle({ ...highlightStyle, - color: layer.options.originalColor + color: userSettings.speed_colored_polylines ? layer.options.originalColor : highlightStyle.color }); } }); @@ -154,12 +220,11 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use }); polylineGroup.on("mouseout", function () { - // Restore original styles for all segments polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { layer.setStyle({ ...normalStyle, - color: layer.options.originalColor + color: userSettings.speed_colored_polylines ? layer.options.originalColor : normalStyle.color }); } }); @@ -182,7 +247,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS const distanceThresholdMeters = parseInt(userSettings.meters_between_routes) || 500; const timeThresholdMinutes = parseInt(userSettings.minutes_between_routes) || 60; - // Split into separate polylines based on distance/time thresholds for (let i = 0, len = markers.length; i < len; i++) { if (currentPolyline.length === 0) { currentPolyline.push(markers[i]); @@ -209,10 +273,9 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS splitPolylines.map((polylineCoordinates) => { const segmentGroup = L.featureGroup(); - // Create segments with different colors based on speed for (let i = 0; i < polylineCoordinates.length - 1; i++) { const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]); - const color = getSpeedColor(speed); + const color = getSpeedColor(speed, userSettings.speed_colored_polylines); const segment = L.polyline( [ @@ -223,14 +286,15 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS color: color, originalColor: color, opacity: routeOpacity, - weight: 3 + weight: 3, + startTime: polylineCoordinates[i][4], + endTime: polylineCoordinates[i + 1][4] } ); segmentGroup.addLayer(segment); } - // Add hover effect to the entire group of segments addHighlightOnHover(segmentGroup, map, polylineCoordinates, userSettings, distanceUnit); return segmentGroup; @@ -238,14 +302,58 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS ).addTo(map); } -export function updatePolylinesOpacity(polylinesLayer, opacity) { +export function updatePolylinesColors(polylinesLayer, useSpeedColors) { + const segments = []; + + // Collect all segments first polylinesLayer.eachLayer((groupLayer) => { if (groupLayer instanceof L.LayerGroup) { groupLayer.eachLayer((segment) => { if (segment instanceof L.Polyline) { - segment.setStyle({ opacity: opacity }); + segments.push(segment); } }); } }); + + // Process segments in batches of 50 + processInBatches(segments, 50, (segment) => { + const latLngs = segment.getLatLngs(); + const point1 = [latLngs[0].lat, latLngs[0].lng]; + const point2 = [latLngs[1].lat, latLngs[1].lng]; + + const speed = calculateSpeed( + [...point1, 0, segment.options.startTime], + [...point2, 0, segment.options.endTime] + ); + + const newColor = useSpeedColors ? + getSpeedColor(speed, useSpeedColors) : + '#0000ff'; + + segment.setStyle({ + color: newColor, + originalColor: newColor + }); + }); +} + +export function updatePolylinesOpacity(polylinesLayer, opacity) { + const segments = []; + + // Collect all segments first + polylinesLayer.eachLayer((groupLayer) => { + if (groupLayer instanceof L.LayerGroup) { + groupLayer.eachLayer((segment) => { + if (segment instanceof L.Polyline) { + segments.push(segment); + } + }); + } + }); + + // Process segments in batches of 50 + processInBatches(segments, 50, (segment) => { + segment.setStyle({ opacity: opacity }); + }); } From badeff3d0a4591425c8779524130c6e5a95a7207 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 20:34:57 +0100 Subject: [PATCH 03/11] Enable or disable speed colored polylines --- app/javascript/controllers/maps_controller.js | 79 +++++++++---------- app/javascript/maps/polylines.js | 43 +++++----- 2 files changed, 63 insertions(+), 59 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index fc60bf75..f6b2c749 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -809,46 +809,28 @@ export default class extends Controller { try { // Check if speed_colored_polylines setting has changed if (newSettings.speed_colored_polylines !== this.userSettings.speed_colored_polylines) { - console.log('Speed colored polylines setting changed:', { - old: this.userSettings.speed_colored_polylines, - new: newSettings.speed_colored_polylines - }); - if (this.polylinesLayer) { - console.log('Starting polylines color update'); + // Remove existing polylines layer + this.map.removeLayer(this.polylinesLayer); - // Update colors without removing the layer - this.polylinesLayer.eachLayer(groupLayer => { - if (groupLayer instanceof L.LayerGroup || groupLayer instanceof L.FeatureGroup) { - groupLayer.eachLayer(segment => { - if (segment instanceof L.Polyline) { - const latLngs = segment.getLatLngs(); - const point1 = [latLngs[0].lat, latLngs[0].lng]; - const point2 = [latLngs[1].lat, latLngs[1].lng]; + // Create new polylines layer with updated settings + this.polylinesLayer = createPolylinesLayer( + this.markers, + this.map, + this.timezone, + this.routeOpacity, + { ...this.userSettings, speed_colored_polylines: newSettings.speed_colored_polylines }, + this.distanceUnit + ); - const speed = calculateSpeed( - [...point1, 0, segment.options.startTime], - [...point2, 0, segment.options.endTime] - ); - - const newColor = newSettings.speed_colored_polylines ? - getSpeedColor(speed, true) : - '#0000ff'; - - segment.setStyle({ - color: newColor, - originalColor: newColor - }); - } - }); - } - }); - - console.log('Finished polylines color update'); + // Add the layer back if it was visible + if (wasPolylinesVisible) { + this.polylinesLayer.addTo(this.map); + } } } - // Check if route opacity has changed + // Update opacity if changed if (newSettings.route_opacity !== this.userSettings.route_opacity) { const newOpacity = parseFloat(newSettings.route_opacity) || 0.6; if (this.polylinesLayer) { @@ -861,8 +843,18 @@ export default class extends Controller { this.routeOpacity = parseFloat(newSettings.route_opacity) || 0.6; this.clearFogRadius = parseInt(newSettings.fog_of_war_meters) || 50; - // Reapply layer states - this.applyLayerControlStates(currentLayerStates); + // Update layer control + this.map.removeControl(this.layerControl); + const controlsLayer = { + Points: this.markersLayer, + Polylines: this.polylinesLayer, + Heatmap: this.heatmapLayer, + "Fog of War": this.fogOverlay, + "Scratch map": this.scratchLayer, + Areas: this.areasLayer, + Photos: this.photoMarkers + }; + this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map); } catch (error) { console.error('Error updating map settings:', error); @@ -913,6 +905,8 @@ export default class extends Controller { } applyLayerControlStates(states) { + console.log('Applying layer states:', states); + const layerControl = { Points: this.markersLayer, Polylines: this.polylinesLayer, @@ -923,11 +917,16 @@ export default class extends Controller { for (const [name, isVisible] of Object.entries(states)) { const layer = layerControl[name]; + console.log(`Processing layer ${name}:`, { layer, isVisible }); - if (isVisible && !this.map.hasLayer(layer)) { - this.map.addLayer(layer); - } else if (this.map.hasLayer(layer)) { - this.map.removeLayer(layer); + 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`); + this.map.removeLayer(layer); + } } } diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index eb9ca95f..c13226d1 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -125,17 +125,6 @@ function processInBatches(items, batchSize, processFn) { } export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, userSettings, distanceUnit) { - const highlightStyle = { - opacity: 1, - weight: 5, - color: userSettings.speed_colored_polylines ? null : '#ffff00' // Yellow highlight if not using speed colors - }; - const normalStyle = { - opacity: userSettings.routeOpacity, - weight: 3, - color: userSettings.speed_colored_polylines ? null : '#0000ff' // Blue normal if not using speed colors - }; - const startPoint = polylineCoordinates[0]; const endPoint = polylineCoordinates[polylineCoordinates.length - 1]; @@ -189,12 +178,20 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use } }); + // Apply highlight style to all segments polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { - layer.setStyle({ - ...highlightStyle, - color: userSettings.speed_colored_polylines ? layer.options.originalColor : highlightStyle.color - }); + const highlightStyle = { + weight: 5, + opacity: 1 + }; + + // Change color to yellow only for non-speed-colored (blue) polylines + if (!userSettings.speed_colored_polylines) { + highlightStyle.color = '#ffff00'; // Yellow + } + + layer.setStyle(highlightStyle); } }); @@ -220,12 +217,20 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use }); polylineGroup.on("mouseout", function () { + // Restore original style polylineGroup.eachLayer((layer) => { if (layer instanceof L.Polyline) { - layer.setStyle({ - ...normalStyle, - color: userSettings.speed_colored_polylines ? layer.options.originalColor : normalStyle.color - }); + const originalStyle = { + weight: 3, + opacity: userSettings.route_opacity + }; + + // Restore original blue color for non-speed-colored polylines + if (!userSettings.speed_colored_polylines) { + originalStyle.color = '#0000ff'; + } + + layer.setStyle(originalStyle); } }); From 216727b9e71d8b92163b27d61d24a6bd2e732299 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 21:04:18 +0100 Subject: [PATCH 04/11] Fix polylines color update when settings updated --- app/javascript/controllers/maps_controller.js | 26 ++---- app/javascript/maps/polylines.js | 83 +++++++++++-------- app/javascript/maps/popups.js | 2 +- 3 files changed, 60 insertions(+), 51 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index f6b2c749..5e3a88b0 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -810,23 +810,13 @@ export default class extends Controller { // Check if speed_colored_polylines setting has changed if (newSettings.speed_colored_polylines !== this.userSettings.speed_colored_polylines) { if (this.polylinesLayer) { - // Remove existing polylines layer - this.map.removeLayer(this.polylinesLayer); + console.log('Starting gradual polyline color update'); - // Create new polylines layer with updated settings - this.polylinesLayer = createPolylinesLayer( - this.markers, - this.map, - this.timezone, - this.routeOpacity, - { ...this.userSettings, speed_colored_polylines: newSettings.speed_colored_polylines }, - this.distanceUnit + // Use the batch processing approach instead of recreating the layer + updatePolylinesColors( + this.polylinesLayer, + newSettings.speed_colored_polylines ); - - // Add the layer back if it was visible - if (wasPolylinesVisible) { - this.polylinesLayer.addTo(this.map); - } } } @@ -860,8 +850,10 @@ export default class extends Controller { console.error('Error updating map settings:', error); console.error(error.stack); } finally { - // Remove loading indicator - document.body.removeChild(loadingDiv); + // 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); diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index c13226d1..4442a8ee 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -45,9 +45,21 @@ function pointToLineDistance(point, lineStart, lineEnd) { } export function calculateSpeed(point1, point2) { + if (!point1 || !point2 || !point1[4] || !point2[4]) { + console.warn('Invalid points for speed calculation:', { point1, point2 }); + return 0; + } + const distanceKm = haversineDistance(point1[0], point1[1], point2[0], point2[1]); // in kilometers const timeDiffSeconds = point2[4] - point1[4]; + console.log('Speed calculation:', { + distance: distanceKm, + timeDiff: timeDiffSeconds, + point1Time: point1[4], + point2Time: point2[4] + }); + // Handle edge cases if (timeDiffSeconds <= 0 || distanceKm <= 0) { return 0; @@ -65,26 +77,22 @@ export function getSpeedColor(speedKmh, useSpeedColors) { return '#0000ff'; // Default blue color } - // Existing speed-based color logic + // Speed-based color logic const colorStops = [ - { speed: 0, color: '#00ff00' }, // Stationary/very slow (neon green) - { speed: 15, color: '#00ffff' }, // Walking/jogging (neon cyan) - { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (neon magenta) - { speed: 50, color: '#ff3300' }, // Urban driving (neon orange-red) - { speed: 100, color: '#ffff00' } // Highway driving (neon yellow) + { speed: 0, color: '#00ff00' }, // Stationary/very slow (green) + { speed: 15, color: '#00ffff' }, // Walking/jogging (cyan) + { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta) + { speed: 50, color: '#ff3300' }, // Urban driving (orange-red) + { speed: 100, color: '#ffff00' } // Highway driving (yellow) ]; // Find the appropriate color segment for (let i = 1; i < colorStops.length; i++) { if (speedKmh <= colorStops[i].speed) { - // Calculate how far we are between the two speeds (0-1) const ratio = (speedKmh - colorStops[i-1].speed) / (colorStops[i].speed - colorStops[i-1].speed); - - // Convert hex to RGB for interpolation const color1 = hexToRGB(colorStops[i-1].color); const color2 = hexToRGB(colorStops[i].color); - // Interpolate between the two colors const r = Math.round(color1.r + (color2.r - color1.r) * ratio); const g = Math.round(color1.g + (color2.g - color1.g) * ratio); const b = Math.round(color1.b + (color2.b - color1.b) * ratio); @@ -93,7 +101,6 @@ export function getSpeedColor(speedKmh, useSpeedColors) { } } - // If speed is higher than our highest threshold, return the last color return colorStops[colorStops.length - 1].color; } @@ -116,8 +123,10 @@ function processInBatches(items, batchSize, processFn) { index += batchSize; if (index < items.length) { - // Schedule next batch using requestAnimationFrame - window.requestAnimationFrame(processNextBatch); + // Add a small delay between batches + setTimeout(() => { + window.requestAnimationFrame(processNextBatch); + }, 10); // 10ms delay between batches } } @@ -186,9 +195,9 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use opacity: 1 }; - // Change color to yellow only for non-speed-colored (blue) polylines + // Only change color to yellow if speed colors are disabled if (!userSettings.speed_colored_polylines) { - highlightStyle.color = '#ffff00'; // Yellow + highlightStyle.color = '#ffff00'; } layer.setStyle(highlightStyle); @@ -222,14 +231,10 @@ export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, use if (layer instanceof L.Polyline) { const originalStyle = { weight: 3, - opacity: userSettings.route_opacity + opacity: userSettings.route_opacity, + color: layer.options.originalColor // Use the stored original color }; - // Restore original blue color for non-speed-colored polylines - if (!userSettings.speed_colored_polylines) { - originalStyle.color = '#0000ff'; - } - layer.setStyle(originalStyle); } }); @@ -280,6 +285,11 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS for (let i = 0; i < polylineCoordinates.length - 1; i++) { const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]); + console.log('Creating segment with speed:', speed, 'from points:', { + point1: polylineCoordinates[i], + point2: polylineCoordinates[i + 1] + }); + const color = getSpeedColor(speed, userSettings.speed_colored_polylines); const segment = L.polyline( @@ -292,6 +302,7 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS originalColor: color, opacity: routeOpacity, weight: 3, + speed: speed, // Store the calculated speed startTime: polylineCoordinates[i][4], endTime: polylineCoordinates[i + 1][4] } @@ -308,6 +319,7 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS } export function updatePolylinesColors(polylinesLayer, useSpeedColors) { + console.log('Starting color update with useSpeedColors:', useSpeedColors); const segments = []; // Collect all segments first @@ -321,20 +333,25 @@ export function updatePolylinesColors(polylinesLayer, useSpeedColors) { } }); - // Process segments in batches of 50 - processInBatches(segments, 50, (segment) => { - const latLngs = segment.getLatLngs(); - const point1 = [latLngs[0].lat, latLngs[0].lng]; - const point2 = [latLngs[1].lat, latLngs[1].lng]; + console.log(`Found ${segments.length} segments to update`); - const speed = calculateSpeed( - [...point1, 0, segment.options.startTime], - [...point2, 0, segment.options.endTime] - ); + // Process segments in smaller batches of 20 + processInBatches(segments, 20, (segment) => { + if (!useSpeedColors) { + segment.setStyle({ + color: '#0000ff', + originalColor: '#0000ff' + }); + return; + } - const newColor = useSpeedColors ? - getSpeedColor(speed, useSpeedColors) : - '#0000ff'; + // Get the original speed from the segment options + const speed = segment.options.speed; + console.log('Segment options:', segment.options); + console.log('Retrieved speed:', speed); + + const newColor = getSpeedColor(speed, true); + console.log('Calculated color for speed:', {speed, newColor}); segment.setStyle({ color: newColor, diff --git a/app/javascript/maps/popups.js b/app/javascript/maps/popups.js index 34a71224..dee74dc5 100644 --- a/app/javascript/maps/popups.js +++ b/app/javascript/maps/popups.js @@ -13,7 +13,7 @@ export function createPopupContent(marker, timezone, distanceUnit) { Latitude: ${marker[0]}
Longitude: ${marker[1]}
Altitude: ${marker[3]}m
- Velocity: ${marker[5]}km/h
+ Speed: ${marker[5]}km/h
Battery: ${marker[2]}%
Id: ${marker[6]}
[Delete] From 7a83afd857c3fc7bd00df68cf7e12253498ecaed Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 21:10:49 +0100 Subject: [PATCH 05/11] Speed up polylines coloring --- app/javascript/maps/polylines.js | 64 ++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 27 deletions(-) diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 4442a8ee..03a458df 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -53,13 +53,6 @@ export function calculateSpeed(point1, point2) { const distanceKm = haversineDistance(point1[0], point1[1], point2[0], point2[1]); // in kilometers const timeDiffSeconds = point2[4] - point1[4]; - console.log('Speed calculation:', { - distance: distanceKm, - timeDiff: timeDiffSeconds, - point1Time: point1[4], - point2Time: point2[4] - }); - // Handle edge cases if (timeDiffSeconds <= 0 || distanceKm <= 0) { return 0; @@ -115,22 +108,43 @@ function hexToRGB(hex) { // Add new function for batch processing function processInBatches(items, batchSize, processFn) { let index = 0; + const totalBatches = Math.ceil(items.length / batchSize); + let batchCount = 0; + const startTime = performance.now(); function processNextBatch() { - const batch = items.slice(index, index + batchSize); - batch.forEach(processFn); + const batchStartTime = performance.now(); + let processedInThisFrame = 0; - index += batchSize; + // Process multiple batches in one frame if they're taking very little time + while (index < items.length && processedInThisFrame < 100) { + const batch = items.slice(index, index + batchSize); + batch.forEach(processFn); + + index += batchSize; + batchCount++; + processedInThisFrame += batch.length; + + // If we've been processing for more than 16ms (targeting 60fps), + // break and schedule the next frame + if (performance.now() - batchStartTime > 16) { + break; + } + } + + const batchEndTime = performance.now(); + console.log(`Processed ${processedInThisFrame} items in ${batchEndTime - batchStartTime}ms`); if (index < items.length) { - // Add a small delay between batches - setTimeout(() => { - window.requestAnimationFrame(processNextBatch); - }, 10); // 10ms delay between batches + window.requestAnimationFrame(processNextBatch); + } else { + const endTime = performance.now(); + console.log(`All items completed in ${endTime - startTime}ms`); } } - processNextBatch(); + console.log(`Starting processing of ${items.length} items`); + window.requestAnimationFrame(processNextBatch); } export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, userSettings, distanceUnit) { @@ -285,10 +299,6 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS for (let i = 0; i < polylineCoordinates.length - 1; i++) { const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]); - console.log('Creating segment with speed:', speed, 'from points:', { - point1: polylineCoordinates[i], - point2: polylineCoordinates[i + 1] - }); const color = getSpeedColor(speed, userSettings.speed_colored_polylines); @@ -321,6 +331,7 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS export function updatePolylinesColors(polylinesLayer, useSpeedColors) { console.log('Starting color update with useSpeedColors:', useSpeedColors); const segments = []; + const startCollectTime = performance.now(); // Collect all segments first polylinesLayer.eachLayer((groupLayer) => { @@ -333,10 +344,14 @@ export function updatePolylinesColors(polylinesLayer, useSpeedColors) { } }); - console.log(`Found ${segments.length} segments to update`); + const endCollectTime = performance.now(); + console.log(`Collected ${segments.length} segments in ${endCollectTime - startCollectTime}ms`); - // Process segments in smaller batches of 20 - processInBatches(segments, 20, (segment) => { + // Increased batch size since individual operations are very fast + const BATCH_SIZE = 50; + + // Process segments in batches + processInBatches(segments, BATCH_SIZE, (segment) => { if (!useSpeedColors) { segment.setStyle({ color: '#0000ff', @@ -345,13 +360,8 @@ export function updatePolylinesColors(polylinesLayer, useSpeedColors) { return; } - // Get the original speed from the segment options const speed = segment.options.speed; - console.log('Segment options:', segment.options); - console.log('Retrieved speed:', speed); - const newColor = getSpeedColor(speed, true); - console.log('Calculated color for speed:', {speed, newColor}); segment.setStyle({ color: newColor, From 1c9667d218873f11def27af738e1f0146b428061 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 21:21:04 +0100 Subject: [PATCH 06/11] Optimize polylines color update --- app/javascript/controllers/maps_controller.js | 3 - app/javascript/maps/polylines.js | 121 +++++++++--------- 2 files changed, 60 insertions(+), 64 deletions(-) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 5e3a88b0..d7221d1f 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -810,9 +810,6 @@ export default class extends Controller { // Check if speed_colored_polylines setting has changed if (newSettings.speed_colored_polylines !== this.userSettings.speed_colored_polylines) { if (this.polylinesLayer) { - console.log('Starting gradual polyline color update'); - - // Use the batch processing approach instead of recreating the layer updatePolylinesColors( this.polylinesLayer, newSettings.speed_colored_polylines diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 03a458df..3ce93c33 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -1,6 +1,5 @@ import { formatDate } from "../maps/helpers"; import { formatDistance } from "../maps/helpers"; -import { getUrlParameter } from "../maps/helpers"; import { minutesToDaysHoursMinutes } from "../maps/helpers"; import { haversineDistance } from "../maps/helpers"; @@ -65,26 +64,29 @@ export function calculateSpeed(point1, point2) { return Math.min(speedKmh, MAX_SPEED); } +// Optimize getSpeedColor by pre-calculating color stops +const colorStops = [ + { speed: 0, color: '#00ff00' }, // Stationary/very slow (green) + { speed: 15, color: '#00ffff' }, // Walking/jogging (cyan) + { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta) + { speed: 50, color: '#ff3300' }, // Urban driving (orange-red) + { speed: 100, color: '#ffff00' } // Highway driving (yellow) +].map(stop => ({ + ...stop, + rgb: hexToRGB(stop.color) +})); + export function getSpeedColor(speedKmh, useSpeedColors) { if (!useSpeedColors) { - return '#0000ff'; // Default blue color + return '#0000ff'; } - // Speed-based color logic - const colorStops = [ - { speed: 0, color: '#00ff00' }, // Stationary/very slow (green) - { speed: 15, color: '#00ffff' }, // Walking/jogging (cyan) - { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta) - { speed: 50, color: '#ff3300' }, // Urban driving (orange-red) - { speed: 100, color: '#ffff00' } // Highway driving (yellow) - ]; - // Find the appropriate color segment for (let i = 1; i < colorStops.length; i++) { if (speedKmh <= colorStops[i].speed) { const ratio = (speedKmh - colorStops[i-1].speed) / (colorStops[i].speed - colorStops[i-1].speed); - const color1 = hexToRGB(colorStops[i-1].color); - const color2 = hexToRGB(colorStops[i].color); + const color1 = colorStops[i-1].rgb; + const color2 = colorStops[i].rgb; const r = Math.round(color1.r + (color2.r - color1.r) * ratio); const g = Math.round(color1.g + (color2.g - color1.g) * ratio); @@ -108,43 +110,40 @@ function hexToRGB(hex) { // Add new function for batch processing function processInBatches(items, batchSize, processFn) { let index = 0; - const totalBatches = Math.ceil(items.length / batchSize); - let batchCount = 0; - const startTime = performance.now(); + const totalItems = items.length; function processNextBatch() { const batchStartTime = performance.now(); let processedInThisFrame = 0; - // Process multiple batches in one frame if they're taking very little time - while (index < items.length && processedInThisFrame < 100) { - const batch = items.slice(index, index + batchSize); - batch.forEach(processFn); + // Process as many items as possible within our time budget + while (index < totalItems && processedInThisFrame < 500) { + const end = Math.min(index + batchSize, totalItems); - index += batchSize; - batchCount++; - processedInThisFrame += batch.length; + // Ensure we're within bounds + for (let i = index; i < end; i++) { + if (items[i]) { // Add null check + processFn(items[i]); + } + } - // If we've been processing for more than 16ms (targeting 60fps), - // break and schedule the next frame - if (performance.now() - batchStartTime > 16) { + processedInThisFrame += (end - index); + index = end; + + if (performance.now() - batchStartTime > 32) { break; } } - const batchEndTime = performance.now(); - console.log(`Processed ${processedInThisFrame} items in ${batchEndTime - batchStartTime}ms`); - - if (index < items.length) { - window.requestAnimationFrame(processNextBatch); + if (index < totalItems) { + setTimeout(processNextBatch, 0); } else { - const endTime = performance.now(); - console.log(`All items completed in ${endTime - startTime}ms`); + // Only clear the array after all processing is complete + items.length = 0; } } - console.log(`Starting processing of ${items.length} items`); - window.requestAnimationFrame(processNextBatch); + processNextBatch(); } export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, userSettings, distanceUnit) { @@ -329,14 +328,16 @@ export function createPolylinesLayer(markers, map, timezone, routeOpacity, userS } export function updatePolylinesColors(polylinesLayer, useSpeedColors) { - console.log('Starting color update with useSpeedColors:', useSpeedColors); - const segments = []; - const startCollectTime = performance.now(); + const defaultStyle = { + color: '#0000ff', + originalColor: '#0000ff' + }; - // Collect all segments first - polylinesLayer.eachLayer((groupLayer) => { + // More efficient segment collection + const segments = new Array(); + polylinesLayer.eachLayer(groupLayer => { if (groupLayer instanceof L.LayerGroup) { - groupLayer.eachLayer((segment) => { + groupLayer.eachLayer(segment => { if (segment instanceof L.Polyline) { segments.push(segment); } @@ -344,29 +345,27 @@ export function updatePolylinesColors(polylinesLayer, useSpeedColors) { } }); - const endCollectTime = performance.now(); - console.log(`Collected ${segments.length} segments in ${endCollectTime - startCollectTime}ms`); + // Reuse style object to reduce garbage collection + const styleObj = {}; - // Increased batch size since individual operations are very fast - const BATCH_SIZE = 50; + // Process segments in larger batches + processInBatches(segments, 200, (segment) => { + try { + if (!useSpeedColors) { + segment.setStyle(defaultStyle); + return; + } - // Process segments in batches - processInBatches(segments, BATCH_SIZE, (segment) => { - if (!useSpeedColors) { - segment.setStyle({ - color: '#0000ff', - originalColor: '#0000ff' - }); - return; + const speed = segment.options.speed || 0; + const newColor = getSpeedColor(speed, true); + + // Reuse style object + styleObj.color = newColor; + styleObj.originalColor = newColor; + segment.setStyle(styleObj); + } catch (error) { + console.error('Error processing segment:', error); } - - const speed = segment.options.speed; - const newColor = getSpeedColor(speed, true); - - segment.setStyle({ - color: newColor, - originalColor: newColor - }); }); } From cd7cf8c4bbb0e3e20a6f81d7ed819762fc232fc7 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 21:30:08 +0100 Subject: [PATCH 07/11] Return distance and points number in the custom control to the map --- app/controllers/map_controller.rb | 3 ++- app/javascript/controllers/maps_controller.js | 21 ++++++++++++++++++- app/views/map/index.html.erb | 2 ++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index ac960928..7a7246c5 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -14,6 +14,7 @@ class MapController < ApplicationController @start_at = Time.zone.at(start_at) @end_at = Time.zone.at(end_at) @years = (@start_at.year..@end_at.year).to_a + @points_number = @coordinates.count end private @@ -36,7 +37,7 @@ class MapController < ApplicationController @distance ||= 0 @coordinates.each_cons(2) do - @distance += Geocoder::Calculations.distance_between([_1[0], _1[1]], [_2[0], _2[1]]) + @distance += Geocoder::Calculations.distance_between([_1[0], _1[1]], [_2[0], _2[1]], units: DISTANCE_UNIT) end @distance.round(1) diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index d7221d1f..7a12d565 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -104,7 +104,26 @@ export default class extends Controller { Photos: this.photoMarkers }; - // Add scale control to bottom right + // 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; + } + }); + + // 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', diff --git a/app/views/map/index.html.erb b/app/views/map/index.html.erb index 7e36c225..d3c39f80 100644 --- a/app/views/map/index.html.erb +++ b/app/views/map/index.html.erb @@ -51,6 +51,8 @@ data-api_key="<%= current_user.api_key %>" data-user_settings=<%= current_user.settings.to_json %> data-coordinates="<%= @coordinates %>" + data-distance="<%= @distance %>" + data-points_number="<%= @points_number %>" data-timezone="<%= Rails.configuration.time_zone %>">
From a1adc9875a00eb09cfa0b003d89daae65e1270a6 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 21:37:22 +0100 Subject: [PATCH 08/11] Update changelog and app version --- .app_version | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.app_version b/.app_version index a723ece7..faa5fb26 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.22.1 +0.22.2 diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f7be29..641a050c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ 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.2 - 2025-01-13 + +✨ The Fancy Routes release ✨ + +### Added + +- In the Map Settings (coggle in the top left corner of the map), you can now enable/disable the Fancy Routes feature. Simply said, it will color your routes based on the speed of each segment. +- Hovering over a polyline now shows the speed of the segment. Move cursor over a polyline to see the speed of different segments. +- Distance and points number in the custom control to the map. + +⚠️ Important note on the Prometheus monitoring ⚠️ + +In the previous release, `bin/dev` command in the default `docker-compose.yml` file was replaced with `bin/rails server -p 3000 -b ::`, but this way Dawarich won't be able to start Prometheus Exporter. If you want to use Prometheus monitoring, you need to use `bin/dev` command instead. + +Example: + +```diff + dawarich_app: + image: freikin/dawarich:latest +... +- command: ['bin/rails', 'server', '-p', '3000', '-b', '::'] ++ command: ['bin/dev'] +``` + # 0.22.1 - 2025-01-09 ### Removed From cebc4950e61fc92365bcb8d74d886aad3823f7b9 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 21:57:19 +0100 Subject: [PATCH 09/11] Add info modal for speed colored polylines --- app/javascript/maps/polylines.js | 4 ++-- app/views/map/_settings_modals.html.erb | 29 +++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/javascript/maps/polylines.js b/app/javascript/maps/polylines.js index 3ce93c33..b2f1e94a 100644 --- a/app/javascript/maps/polylines.js +++ b/app/javascript/maps/polylines.js @@ -69,8 +69,8 @@ const colorStops = [ { speed: 0, color: '#00ff00' }, // Stationary/very slow (green) { speed: 15, color: '#00ffff' }, // Walking/jogging (cyan) { speed: 30, color: '#ff00ff' }, // Cycling/slow driving (magenta) - { speed: 50, color: '#ff3300' }, // Urban driving (orange-red) - { speed: 100, color: '#ffff00' } // Highway driving (yellow) + { speed: 50, color: '#ffff00' }, // Urban driving (yellow) + { speed: 100, color: '#ff3300' } // Highway driving (red) ].map(stop => ({ ...stop, rgb: hexToRGB(stop.color) diff --git a/app/views/map/_settings_modals.html.erb b/app/views/map/_settings_modals.html.erb index 09ddd165..5376a585 100644 --- a/app/views/map/_settings_modals.html.erb +++ b/app/views/map/_settings_modals.html.erb @@ -112,3 +112,32 @@
+ + + From 4fc8992f731cba83a4295082e0c2fc40ebf6d0ab Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Mon, 13 Jan 2025 22:05:25 +0100 Subject: [PATCH 10/11] Rename Polylines to Routes in the interface --- app/controllers/api/v1/settings_controller.rb | 2 +- app/javascript/controllers/maps_controller.js | 34 ++++++++----------- app/javascript/maps/polylines.js | 4 +-- app/views/map/_settings_modals.html.erb | 4 +-- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb index b15bad16..316c201e 100644 --- a/app/controllers/api/v1/settings_controller.rb +++ b/app/controllers/api/v1/settings_controller.rb @@ -28,7 +28,7 @@ class Api::V1::SettingsController < ApiController :time_threshold_minutes, :merge_threshold_minutes, :route_opacity, :preferred_map_layer, :points_rendering_mode, :live_map_enabled, :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key, - :speed_colored_polylines + :speed_colored_routes ) end end diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js index 7a12d565..fa2ef5e1 100644 --- a/app/javascript/controllers/maps_controller.js +++ b/app/javascript/controllers/maps_controller.js @@ -65,7 +65,7 @@ export default class extends Controller { this.pointsRenderingMode = this.userSettings.points_rendering_mode || "raw"; this.liveMapEnabled = this.userSettings.live_map_enabled || false; this.countryCodesMap = countryCodesMap(); - this.speedColoredPolylines = this.userSettings.speed_colored_polylines || false; + this.speedColoredPolylines = this.userSettings.speed_colored_routes || false; this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111]; @@ -96,7 +96,7 @@ export default class extends Controller { const controlsLayer = { Points: this.markersLayer, - Polylines: this.polylinesLayer, + Routes: this.polylinesLayer, Heatmap: this.heatmapLayer, "Fog of War": this.fogOverlay, "Scratch map": this.scratchLayer, @@ -476,7 +476,7 @@ export default class extends Controller { this.map.removeControl(this.layerControl); const controlsLayer = { Points: this.markersLayer, - Polylines: this.polylinesLayer, + Routes: this.polylinesLayer, Heatmap: this.heatmapLayer, "Fog of War": this.fogOverlay, "Scratch map": this.scratchLayer, @@ -714,10 +714,10 @@ export default class extends Controller { -