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..93f71881 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,34 @@ 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.
+
+### Changed
+
+- The name of the "Polylines" feature is now "Routes".
+
+⚠️ 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
diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb
index f87d9df7..316c201e 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_routes
)
end
end
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 40893763..fa2ef5e1 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_routes || false;
this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111];
@@ -78,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,
@@ -86,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',
@@ -439,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,
@@ -677,6 +714,12 @@ export default class extends Controller {
+
+ Speed-colored routes
+ ?
+
+
+
Update
`;
@@ -717,8 +760,13 @@ export default class extends Controller {
}
}
+ speedColoredRoutesChecked() {
+ return this.userSettings.speed_colored_routes ? 'checked' : '';
+ }
+
updateSettings(event) {
event.preventDefault();
+ console.log('Form submitted');
fetch(`/api/v1/settings?api_key=${this.apiKey}`, {
method: 'PATCH',
@@ -732,12 +780,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_routes: event.target.speed_colored_routes.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,84 +798,78 @@ 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) {
- 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;
-
- // Preserve existing layer instances if they exist
- const preserveLayers = {
- Points: this.markersLayer,
- Polylines: this.polylinesLayer,
- Heatmap: this.heatmapLayer,
- "Fog of War": this.fogOverlay,
- Areas: this.areasLayer,
- };
-
- // Clear all layers except base layers
- this.map.eachLayer((layer) => {
- if (!(layer instanceof L.TileLayer)) {
- this.map.removeLayer(layer);
- }
+ console.log('Updating map settings:', {
+ newSettings,
+ currentSettings: this.userSettings,
+ hasPolylines: !!this.polylinesLayer,
+ isVisible: this.polylinesLayer && this.map.hasLayer(this.polylinesLayer)
});
- // 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();
+ // Show loading indicator
+ const loadingDiv = document.createElement('div');
+ loadingDiv.className = 'map-loading-overlay';
+ loadingDiv.innerHTML = '
Updating map...
';
+ document.body.appendChild(loadingDiv);
- // Redraw areas
- fetchAndDrawAreas(this.areasLayer, this.apiKey);
+ // Debounce the heavy operations
+ const updateLayers = debounce(() => {
+ try {
+ // 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
+ );
+ }
+ }
- let fogEnabled = false;
- document.getElementById('fog').style.display = 'none';
+ // 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);
+ }
+ }
- 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);
+ // 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;
+
+ // Update layer control
+ this.map.removeControl(this.layerControl);
+ const controlsLayer = {
+ Points: this.markersLayer,
+ Routes: 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);
+ 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);
- 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() {
@@ -845,7 +889,7 @@ export default class extends Controller {
getLayerName(layer) {
const controlLayers = {
Points: this.markersLayer,
- Polylines: this.polylinesLayer,
+ Routes: this.polylinesLayer,
Heatmap: this.heatmapLayer,
"Fog of War": this.fogOverlay,
Areas: this.areasLayer,
@@ -865,9 +909,11 @@ export default class extends Controller {
}
applyLayerControlStates(states) {
+ console.log('Applying layer states:', states);
+
const layerControl = {
Points: this.markersLayer,
- Polylines: this.polylinesLayer,
+ Routes: this.polylinesLayer,
Heatmap: this.heatmapLayer,
"Fog of War": this.fogOverlay,
Areas: this.areasLayer,
@@ -875,11 +921,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 2c09022d..e78f0223 100644
--- a/app/javascript/maps/polylines.js
+++ b/app/javascript/maps/polylines.js
@@ -1,15 +1,152 @@
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";
-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 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;
- polyline.setStyle(originalStyle);
+ 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;
+ }
+
+ 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;
+ }
+
+ const dx = x - xx;
+ const dy = y - yy;
+
+ return Math.sqrt(dx * dx + dy * dy);
+}
+
+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];
+
+ // Handle edge cases
+ if (timeDiffSeconds <= 0 || distanceKm <= 0) {
+ return 0;
+ }
+
+ const speedKmh = (distanceKm / timeDiffSeconds) * 3600; // Convert to km/h
+
+ // Cap speed at reasonable maximum (e.g., 150 km/h)
+ const MAX_SPEED = 150;
+ 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: '#ffff00' }, // Urban driving (yellow)
+ { speed: 100, color: '#ff3300' } // Highway driving (red)
+].map(stop => ({
+ ...stop,
+ rgb: hexToRGB(stop.color)
+}));
+
+export function getSpeedColor(speedKmh, useSpeedColors) {
+ if (!useSpeedColors) {
+ return '#0000ff';
+ }
+
+ // 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 = 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);
+ const b = Math.round(color1.b + (color2.b - color1.b) * ratio);
+
+ return `rgb(${r}, ${g}, ${b})`;
+ }
+ }
+
+ 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;
+ const totalItems = items.length;
+
+ function processNextBatch() {
+ const batchStartTime = performance.now();
+ let processedInThisFrame = 0;
+
+ // Process as many items as possible within our time budget
+ while (index < totalItems && processedInThisFrame < 500) {
+ const end = Math.min(index + batchSize, totalItems);
+
+ // Ensure we're within bounds
+ for (let i = index; i < end; i++) {
+ if (items[i]) { // Add null check
+ processFn(items[i]);
+ }
+ }
+
+ processedInThisFrame += (end - index);
+ index = end;
+
+ if (performance.now() - batchStartTime > 32) {
+ break;
+ }
+ }
+
+ if (index < totalItems) {
+ setTimeout(processNextBatch, 0);
+ } else {
+ // Only clear the array after all processing is complete
+ items.length = 0;
+ }
+ }
+
+ processNextBatch();
+}
+
+export function addHighlightOnHover(polylineGroup, map, polylineCoordinates, userSettings, distanceUnit) {
const startPoint = polylineCoordinates[0];
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
@@ -28,66 +165,102 @@ 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) {
+ let closestSegment = null;
+ let minDistance = Infinity;
+ let currentSpeed = 0;
+
+ polylineGroup.eachLayer((layer) => {
+ if (layer instanceof L.Polyline) {
+ const layerLatLngs = layer.getLatLngs();
+ const distance = pointToLineDistance(e.latlng, layerLatLngs[0], layerLatLngs[1]);
+
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestSegment = layer;
+
+ 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;
+ });
+
+ if (startIdx !== -1 && startIdx < polylineCoordinates.length - 1) {
+ currentSpeed = calculateSpeed(
+ polylineCoordinates[startIdx],
+ polylineCoordinates[startIdx + 1]
+ );
+ }
+ }
+ }
+ });
+
+ // 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);
+ }
+ });
+
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 style
+ 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
+ };
+
+ layer.setStyle(originalStyle);
+ }
+ });
+
+ 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());
});
}
@@ -121,26 +294,97 @@ 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);
+ for (let i = 0; i < polylineCoordinates.length - 1; i++) {
+ const speed = calculateSpeed(polylineCoordinates[i], polylineCoordinates[i + 1]);
- return polyline;
+ const color = getSpeedColor(speed, userSettings.speed_colored_routes);
+
+ 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,
+ speed: speed, // Store the calculated speed
+ startTime: polylineCoordinates[i][4],
+ endTime: polylineCoordinates[i + 1][4]
+ }
+ );
+
+ segmentGroup.addLayer(segment);
+ }
+
+ 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 });
+export function updatePolylinesColors(polylinesLayer, useSpeedColors) {
+ const defaultStyle = {
+ color: '#0000ff',
+ originalColor: '#0000ff'
+ };
+
+ // More efficient segment collection
+ const segments = new Array();
+ polylinesLayer.eachLayer(groupLayer => {
+ if (groupLayer instanceof L.LayerGroup) {
+ groupLayer.eachLayer(segment => {
+ if (segment instanceof L.Polyline) {
+ segments.push(segment);
+ }
+ });
+ }
+ });
+
+ // Reuse style object to reduce garbage collection
+ const styleObj = {};
+
+ // Process segments in larger batches
+ processInBatches(segments, 200, (segment) => {
+ try {
+ if (!useSpeedColors) {
+ segment.setStyle(defaultStyle);
+ 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);
}
});
}
+
+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 });
+ });
+}
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]
diff --git a/app/views/map/_settings_modals.html.erb b/app/views/map/_settings_modals.html.erb
index 09ddd165..5a36b807 100644
--- a/app/views/map/_settings_modals.html.erb
+++ b/app/views/map/_settings_modals.html.erb
@@ -112,3 +112,32 @@
Close
+
+
+
+
+
Speed-colored routes
+
+ This checkbox will color the routes based on the speed of each segment.
+
+
+ Uncheck this checkbox if you want to disable the speed-colored routes.
+
+
+ Speed coloring is based on the following color stops:
+
+
+ 0 km/h — green, stationary or walking
+
+ 15 km/h — cyan, jogging
+
+ 30 km/h — magenta, cycling
+
+ 50 km/h — yellow, urban driving
+
+ 100 km/h — orange-red, highway driving
+
+
+
+
Close
+
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 %>">