2024-03-15 20:07:20 -04:00
|
|
|
import { Controller } from "@hotwired/stimulus"
|
2024-03-21 18:24:47 -04:00
|
|
|
import L, { circleMarker } from "leaflet"
|
2024-05-25 16:14:55 -04:00
|
|
|
import "leaflet.heat"
|
2024-03-15 20:07:20 -04:00
|
|
|
|
|
|
|
|
// Connects to data-controller="maps"
|
|
|
|
|
export default class extends Controller {
|
|
|
|
|
static targets = ["container"]
|
|
|
|
|
|
|
|
|
|
connect() {
|
2024-03-16 16:31:07 -04:00
|
|
|
console.log("Map controller connected")
|
2024-03-15 20:07:20 -04:00
|
|
|
var markers = JSON.parse(this.element.dataset.coordinates)
|
2024-04-04 11:31:15 -04:00
|
|
|
var center = markers[markers.length - 1] || JSON.parse(this.element.dataset.center)
|
2024-03-16 16:31:07 -04:00
|
|
|
var center = (center === undefined) ? [52.516667, 13.383333] : center;
|
2024-05-31 14:10:22 -04:00
|
|
|
var timezone = this.element.dataset.timezone;
|
2024-04-17 15:54:04 -04:00
|
|
|
|
|
|
|
|
var map = L.map(this.containerTarget, {
|
2024-04-17 16:00:23 -04:00
|
|
|
layers: [this.osmMapLayer(), this.osmHotMapLayer()]
|
2024-04-17 15:54:04 -04:00
|
|
|
}).setView([center[0], center[1]], 14);
|
|
|
|
|
|
2024-05-29 17:00:35 -04:00
|
|
|
var markersArray = this.markersArray(markers);
|
|
|
|
|
var markersLayer = L.layerGroup(markersArray);
|
|
|
|
|
var heatmapMarkers = markers.map(element => [element[0], element[1], 0.3]); // lat, lon, intensity
|
2024-04-17 15:54:04 -04:00
|
|
|
|
2024-05-29 17:00:35 -04:00
|
|
|
// Function to calculate distance between two lat-lng points using Haversine formula
|
|
|
|
|
function haversineDistance(lat1, lon1, lat2, lon2) {
|
|
|
|
|
const toRad = x => x * Math.PI / 180;
|
|
|
|
|
const R = 6371; // Radius of the Earth in kilometers
|
|
|
|
|
const dLat = toRad(lat2 - lat1);
|
|
|
|
|
const dLon = toRad(lon2 - lon1);
|
|
|
|
|
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
2024-05-30 05:50:12 -04:00
|
|
|
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
|
|
|
|
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
2024-05-29 17:00:35 -04:00
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
|
return R * c * 1000; // Distance in meters
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
function getURLParameter(name) {
|
|
|
|
|
return new URLSearchParams(window.location.search).get(name);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 17:25:04 -04:00
|
|
|
function minutesToDaysHoursMinutes(minutes) {
|
|
|
|
|
var days = Math.floor(minutes / (24 * 60));
|
|
|
|
|
var hours = Math.floor((minutes % (24 * 60)) / 60);
|
|
|
|
|
var minutes = minutes % 60;
|
|
|
|
|
var result = '';
|
|
|
|
|
|
|
|
|
|
if (days > 0) {
|
|
|
|
|
result += days + 'd ';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (hours > 0) {
|
|
|
|
|
result += hours + 'h ';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (minutes > 0) {
|
|
|
|
|
result += minutes + 'min';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
function addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone) {
|
2024-05-30 05:50:12 -04:00
|
|
|
// Define the original and highlight styles
|
|
|
|
|
const originalStyle = { color: 'blue', opacity: 0.6, weight: 3 };
|
|
|
|
|
const highlightStyle = { color: 'yellow', opacity: 1, weight: 5 };
|
|
|
|
|
|
|
|
|
|
// Apply original style to the polyline initially
|
|
|
|
|
polyline.setStyle(originalStyle);
|
|
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
// Create the popup content for the route
|
|
|
|
|
var firstTimestamp = new Date(startPoint[4] * 1000).toLocaleString('en-GB', { timeZone: timezone });
|
|
|
|
|
var lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString('en-GB', { timeZone: timezone });
|
2024-05-31 17:25:04 -04:00
|
|
|
// Make timeOnRoute look nice with split to days, hours and minutes
|
|
|
|
|
|
|
|
|
|
var minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
|
|
|
|
|
var timeOnRoute = minutesToDaysHoursMinutes(minutes);
|
2024-05-31 14:10:22 -04:00
|
|
|
|
|
|
|
|
// Calculate distances to previous and next points
|
|
|
|
|
var distanceToPrev = prevPoint ? haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : 'N/A';
|
|
|
|
|
var distanceToNext = nextPoint ? haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : 'N/A';
|
|
|
|
|
|
|
|
|
|
// Calculate time between routes
|
|
|
|
|
var timeBetweenPrev = prevPoint ? Math.round((startPoint[4] - prevPoint[4]) / 60) : 'N/A';
|
|
|
|
|
var timeBetweenNext = nextPoint ? Math.round((nextPoint[4] - endPoint[4]) / 60) : 'N/A';
|
|
|
|
|
|
|
|
|
|
// Create custom emoji icons
|
|
|
|
|
const startIcon = L.divIcon({ html: '🚥', className: 'emoji-icon' });
|
|
|
|
|
const finishIcon = L.divIcon({ html: '🏁', className: 'emoji-icon' });
|
|
|
|
|
|
|
|
|
|
// Create markers for the start and end points
|
|
|
|
|
const startMarker = L.marker([startPoint[0], startPoint[1]], { icon: startIcon }).bindPopup(`Start: ${firstTimestamp}`);
|
|
|
|
|
const endMarker = L.marker([endPoint[0], endPoint[1]], { icon: finishIcon }).bindPopup(`
|
2024-05-31 14:17:14 -04:00
|
|
|
<b>Start:</b> ${firstTimestamp}<br>
|
|
|
|
|
<b>End:</b> ${lastTimestamp}<br>
|
2024-05-31 17:25:04 -04:00
|
|
|
<b>Duration:</b> ${timeOnRoute}<br>
|
2024-06-07 13:22:39 -04:00
|
|
|
<b>Prev Route:</b> ${Math.round(distanceToPrev)}m and ${minutesToDaysHoursMinutes(timeBetweenPrev)} away<br>
|
|
|
|
|
<b>Next Route:</b> ${Math.round(distanceToNext)}m and ${minutesToDaysHoursMinutes(timeBetweenNext)} away<br>
|
2024-05-31 14:10:22 -04:00
|
|
|
`);
|
|
|
|
|
|
|
|
|
|
// Add mouseover event to highlight the polyline and show the start and end markers
|
2024-05-30 05:50:12 -04:00
|
|
|
polyline.on('mouseover', function(e) {
|
2024-05-30 10:22:28 -04:00
|
|
|
polyline.setStyle(highlightStyle);
|
2024-05-31 14:10:22 -04:00
|
|
|
startMarker.addTo(map);
|
|
|
|
|
endMarker.addTo(map).openPopup();
|
2024-05-30 05:50:12 -04:00
|
|
|
});
|
|
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
// Add mouseout event to revert the polyline style and remove the start and end markers
|
2024-05-30 05:50:12 -04:00
|
|
|
polyline.on('mouseout', function(e) {
|
2024-05-31 14:17:14 -04:00
|
|
|
polyline.setStyle(originalStyle);
|
|
|
|
|
map.closePopup();
|
|
|
|
|
map.removeLayer(startMarker);
|
|
|
|
|
map.removeLayer(endMarker);
|
2024-05-30 05:50:12 -04:00
|
|
|
});
|
2024-05-31 14:17:14 -04:00
|
|
|
}
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-05-29 17:00:35 -04:00
|
|
|
var splitPolylines = [];
|
|
|
|
|
var currentPolyline = [];
|
2024-05-31 14:10:22 -04:00
|
|
|
var distanceThresholdMeters = parseInt(getURLParameter('meters_between_routes')) || 500;
|
|
|
|
|
var timeThresholdMinutes = parseInt(getURLParameter('minutes_between_routes')) || 60;
|
2024-05-29 17:00:35 -04:00
|
|
|
|
2024-05-30 10:22:28 -04:00
|
|
|
// Process markers and split polylines based on the distance and time
|
2024-05-29 17:00:35 -04:00
|
|
|
for (let i = 0, len = markers.length; i < len; i++) {
|
|
|
|
|
if (currentPolyline.length === 0) {
|
2024-05-30 05:50:12 -04:00
|
|
|
currentPolyline.push(markers[i]);
|
2024-05-29 17:00:35 -04:00
|
|
|
} else {
|
|
|
|
|
var lastPoint = currentPolyline[currentPolyline.length - 1];
|
2024-05-30 05:50:12 -04:00
|
|
|
var currentPoint = markers[i];
|
2024-05-29 17:00:35 -04:00
|
|
|
var distance = haversineDistance(lastPoint[0], lastPoint[1], currentPoint[0], currentPoint[1]);
|
2024-05-30 10:22:28 -04:00
|
|
|
var timeDifference = (currentPoint[4] - lastPoint[4]) / 60; // Time difference in minutes
|
2024-05-29 17:00:35 -04:00
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
if (distance > distanceThresholdMeters || timeDifference > timeThresholdMinutes) {
|
2024-05-29 17:00:35 -04:00
|
|
|
splitPolylines.push([...currentPolyline]); // Use spread operator to clone the array
|
|
|
|
|
currentPolyline = [currentPoint];
|
|
|
|
|
} else {
|
|
|
|
|
currentPolyline.push(currentPoint);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Add the last polyline if it exists
|
|
|
|
|
if (currentPolyline.length > 0) {
|
|
|
|
|
splitPolylines.push(currentPolyline);
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-30 05:50:12 -04:00
|
|
|
// Assuming each polylineCoordinates is an array of objects with lat, lng, and timestamp properties
|
2024-05-31 14:10:22 -04:00
|
|
|
var polylineLayers = splitPolylines.map((polylineCoordinates, index) => {
|
2024-05-30 05:50:12 -04:00
|
|
|
// Extract lat-lng pairs for the polyline
|
|
|
|
|
var latLngs = polylineCoordinates.map(point => [point[0], point[1]]);
|
|
|
|
|
|
|
|
|
|
// Create a polyline with the given coordinates
|
|
|
|
|
var polyline = L.polyline(latLngs, { color: 'blue', opacity: 0.6, weight: 3 });
|
|
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
// Get the start and end points
|
|
|
|
|
var startPoint = polylineCoordinates[0];
|
|
|
|
|
var endPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
// Get the previous and next points
|
|
|
|
|
var prevPoint = index > 0 ? splitPolylines[index - 1][splitPolylines[index - 1].length - 1] : null;
|
|
|
|
|
var nextPoint = index < splitPolylines.length - 1 ? splitPolylines[index + 1][0] : null;
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
// Add highlighting and popups on hover
|
|
|
|
|
addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone);
|
2024-05-30 05:50:12 -04:00
|
|
|
|
|
|
|
|
return polyline;
|
|
|
|
|
});
|
|
|
|
|
|
2024-05-29 17:00:35 -04:00
|
|
|
var polylinesLayer = L.layerGroup(polylineLayers).addTo(map);
|
|
|
|
|
var heatmapLayer = L.heatLayer(heatmapMarkers, { radius: 20 }).addTo(map);
|
2024-04-17 15:54:04 -04:00
|
|
|
|
|
|
|
|
var controlsLayer = {
|
|
|
|
|
"Points": markersLayer,
|
2024-05-30 10:22:28 -04:00
|
|
|
"Polylines": polylinesLayer,
|
2024-05-25 16:14:55 -04:00
|
|
|
"Heatmap": heatmapLayer
|
2024-05-29 17:00:35 -04:00
|
|
|
};
|
2024-03-15 20:07:20 -04:00
|
|
|
|
2024-05-31 14:10:22 -04:00
|
|
|
L.control.scale({
|
|
|
|
|
position: 'bottomright', // The default position is 'bottomleft'
|
|
|
|
|
metric: true, // Display metric scale
|
|
|
|
|
imperial: false, // Display imperial scale
|
|
|
|
|
maxWidth: 120 // Maximum width of the scale control in pixels
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
|
2024-05-29 17:00:35 -04:00
|
|
|
L.control.layers(this.baseMaps(), controlsLayer).addTo(map);
|
2024-05-25 16:14:55 -04:00
|
|
|
|
2024-03-21 18:54:19 -04:00
|
|
|
this.addTileLayer(map);
|
|
|
|
|
this.addLastMarker(map, markers);
|
2024-03-15 20:07:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
|
this.map.remove();
|
|
|
|
|
}
|
2024-03-21 18:24:47 -04:00
|
|
|
|
2024-04-17 16:00:23 -04:00
|
|
|
osmMapLayer() {
|
|
|
|
|
return L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
|
|
|
maxZoom: 19,
|
|
|
|
|
attribution: '© OpenStreetMap'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
osmHotMapLayer() {
|
|
|
|
|
return L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
|
|
|
|
|
maxZoom: 19,
|
|
|
|
|
attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseMaps() {
|
|
|
|
|
return {
|
|
|
|
|
"OpenStreetMap": this.osmMapLayer(),
|
|
|
|
|
"OpenStreetMap.HOT": this.osmHotMapLayer()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
controlsLayer() {
|
|
|
|
|
return {
|
|
|
|
|
"Points": this.markersLayer,
|
|
|
|
|
"Polyline": this.polylineLayer
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
markersArray(markers_data) {
|
|
|
|
|
var markersArray = []
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < markers_data.length; i++) {
|
|
|
|
|
var lat = markers_data[i][0];
|
|
|
|
|
var lon = markers_data[i][1];
|
|
|
|
|
|
|
|
|
|
var popupContent = this.popupContent(markers_data[i]);
|
|
|
|
|
var circleMarker = L.circleMarker([lat, lon], {radius: 4})
|
|
|
|
|
|
|
|
|
|
markersArray.push(circleMarker.bindPopup(popupContent).openPopup())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return markersArray
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-21 18:24:47 -04:00
|
|
|
popupContent(marker) {
|
|
|
|
|
return `
|
|
|
|
|
<b>Timestamp:</b> ${this.formatDate(marker[4])}<br>
|
|
|
|
|
<b>Latitude:</b> ${marker[0]}<br>
|
|
|
|
|
<b>Longitude:</b> ${marker[1]}<br>
|
2024-03-22 17:57:53 -04:00
|
|
|
<b>Altitude:</b> ${marker[3]}m<br>
|
|
|
|
|
<b>Velocity:</b> ${marker[5]}km/h<br>
|
2024-03-28 10:11:59 -04:00
|
|
|
<b>Battery:</b> ${marker[2]}%
|
2024-03-21 18:24:47 -04:00
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
formatDate(timestamp) {
|
|
|
|
|
let date = new Date(timestamp * 1000); // Multiply by 1000 because JavaScript works with milliseconds
|
|
|
|
|
|
2024-05-25 14:23:33 -04:00
|
|
|
let timezone = this.element.dataset.timezone;
|
|
|
|
|
|
|
|
|
|
return date.toLocaleString('en-GB', { timeZone: timezone });
|
2024-03-21 18:24:47 -04:00
|
|
|
}
|
2024-03-21 18:54:19 -04:00
|
|
|
|
|
|
|
|
addTileLayer(map) {
|
|
|
|
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
|
|
|
maxZoom: 19,
|
|
|
|
|
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
|
|
|
|
}).addTo(map);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
addPolyline(map, markers) {
|
|
|
|
|
var coordinates = markers.map(element => element.slice(0, 2));
|
|
|
|
|
L.polyline(coordinates).addTo(map);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addLastMarker(map, markers) {
|
|
|
|
|
if (markers.length > 0) {
|
|
|
|
|
var lastMarker = markers[markers.length - 1].slice(0, 2)
|
|
|
|
|
L.marker(lastMarker).addTo(map);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-03-15 20:07:20 -04:00
|
|
|
}
|