diff --git a/.app_version b/.app_version
index 965065db..a602fc9e 100644
--- a/.app_version
+++ b/.app_version
@@ -1 +1 @@
-0.9.3
+0.9.4
diff --git a/.gitignore b/.gitignore
index 591f7232..909855de 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,8 +26,12 @@
!/tmp/storage/.keep
/public/assets
-/public/exports
-/public/imports
+
+# We need directories for import and export files, but not the files themselves.
+/public/exports/*
+!/public/exports/.keep
+/public/imports/*
+!/public/imports/.keep
# Ignore master key for decrypting credentials and more.
/config/master.key
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a953daa..ad234c41 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [0.9.4] — 2024-07-21
+
+### Fixed
+
+- Added `public/imports` and `public/exports` folders to git to prevent errors when exporting data
+
+### Changed
+
+- Some code from `maps_controller.js` was extracted into separate files
+
+---
+
## [0.9.3] — 2024-07-19
diff --git a/app/javascript/controllers/maps_controller.js b/app/javascript/controllers/maps_controller.js
index 9cc55816..cc6bac1d 100644
--- a/app/javascript/controllers/maps_controller.js
+++ b/app/javascript/controllers/maps_controller.js
@@ -1,6 +1,14 @@
import { Controller } from "@hotwired/stimulus";
import L from "leaflet";
import "leaflet.heat";
+import { formatDistance } from "../maps/helpers";
+import { getUrlParameter } from "../maps/helpers";
+import { minutesToDaysHoursMinutes } from "../maps/helpers";
+import { formatDate } from "../maps/helpers";
+import { haversineDistance } from "../maps/helpers";
+import { osmMapLayer } from "../maps/layers";
+import { osmHotMapLayer } from "../maps/layers";
+import { addTileLayer } from "../maps/layers";
export default class extends Controller {
static targets = ["container"];
@@ -15,7 +23,7 @@ export default class extends Controller {
const clearFogRadius = this.element.dataset.fog_of_war_meters;
const map = L.map(this.containerTarget, {
- layers: [this.osmMapLayer(), this.osmHotMapLayer()],
+ layers: [osmMapLayer(), osmHotMapLayer()],
}).setView([center[0], center[1]], 14);
const markersArray = this.createMarkersArray(markers);
@@ -41,7 +49,7 @@ export default class extends Controller {
})
.addTo(map);
- const layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(map);
+ L.control.layers(this.baseMaps(), controlsLayer).addTo(map);
let fogEnabled = false;
@@ -110,7 +118,7 @@ export default class extends Controller {
fog.appendChild(circle);
}
- this.addTileLayer(map);
+ addTileLayer(map);
this.addLastMarker(map, markers);
}
@@ -118,24 +126,10 @@ export default class extends Controller {
this.map.remove();
}
- 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(),
+ OpenStreetMap: osmMapLayer(),
+ "OpenStreetMap.HOT": osmHotMapLayer(),
};
}
@@ -148,8 +142,9 @@ export default class extends Controller {
}
createPopupContent(marker) {
+ const timezone = this.element.dataset.timezone;
return `
- Timestamp: ${this.formatDate(marker[4])}
+ Timestamp: ${formatDate(marker[4], timezone)}
Latitude: ${marker[0]}
Longitude: ${marker[1]}
Altitude: ${marker[3]}m
@@ -158,19 +153,6 @@ export default class extends Controller {
`;
}
- formatDate(timestamp) {
- const date = new Date(timestamp * 1000);
- const timezone = this.element.dataset.timezone;
- return date.toLocaleString("en-GB", { timeZone: timezone });
- }
-
- addTileLayer(map) {
- L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
- maxZoom: 19,
- attribution: "© OpenStreetMap",
- }).addTo(map);
- }
-
addLastMarker(map, markers) {
if (markers.length > 0) {
const lastMarker = markers[markers.length - 1].slice(0, 2);
@@ -178,44 +160,6 @@ export default class extends Controller {
}
}
- 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) +
- Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
- Math.sin(dLon / 2) * Math.sin(dLon / 2);
- const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
- return R * c * 1000; // Distance in meters
- }
-
- minutesToDaysHoursMinutes(minutes) {
- const days = Math.floor(minutes / (24 * 60));
- const hours = Math.floor((minutes % (24 * 60)) / 60);
- minutes = minutes % 60;
- let result = "";
-
- if (days > 0) {
- result += `${days}d `;
- }
-
- if (hours > 0) {
- result += `${hours}h `;
- }
-
- if (minutes > 0) {
- result += `${minutes}min`;
- }
-
- return result;
- }
-
- getUrlParameter(name) {
- return new URLSearchParams(window.location.search).get(name);
- }
-
addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone) {
const originalStyle = { color: "blue", opacity: 0.6, weight: 3 };
const highlightStyle = { color: "yellow", opacity: 1, weight: 5 };
@@ -226,11 +170,11 @@ export default class extends Controller {
const lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
const minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
- const timeOnRoute = this.minutesToDaysHoursMinutes(minutes);
- const distance = this.haversineDistance(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
+ const timeOnRoute = minutesToDaysHoursMinutes(minutes);
+ const distance = haversineDistance(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
- const distanceToPrev = prevPoint ? this.haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : "N/A";
- const distanceToNext = nextPoint ? this.haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : "N/A";
+ const distanceToPrev = prevPoint ? haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : "N/A";
+ const distanceToNext = nextPoint ? haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : "N/A";
const timeBetweenPrev = prevPoint ? Math.round((startPoint[4] - prevPoint[4]) / 60) : "N/A";
const timeBetweenNext = nextPoint ? Math.round((nextPoint[4] - endPoint[4]) / 60) : "N/A";
@@ -238,19 +182,19 @@ export default class extends Controller {
const startIcon = L.divIcon({ html: "🚥", className: "emoji-icon" });
const finishIcon = L.divIcon({ html: "🏁", className: "emoji-icon" });
- const isDebugMode = this.getUrlParameter("debug") === "true";
+ const isDebugMode = getUrlParameter("debug") === "true";
let popupContent = `
Start: ${firstTimestamp}
End: ${lastTimestamp}
Duration: ${timeOnRoute}
- Distance: ${this.formatDistance(distance)}
+ Distance: ${formatDistance(distance)}
`;
if (isDebugMode) {
popupContent += `
- Prev Route: ${Math.round(distanceToPrev)}m and ${this.minutesToDaysHoursMinutes(timeBetweenPrev)} away
- Next Route: ${Math.round(distanceToNext)}m and ${this.minutesToDaysHoursMinutes(timeBetweenNext)} away
+ Prev Route: ${Math.round(distanceToPrev)}m and ${minutesToDaysHoursMinutes(timeBetweenPrev)} away
+ Next Route: ${Math.round(distanceToNext)}m and ${minutesToDaysHoursMinutes(timeBetweenNext)} away
`;
}
@@ -303,7 +247,7 @@ export default class extends Controller {
} else {
const lastPoint = currentPolyline[currentPolyline.length - 1];
const currentPoint = markers[i];
- const distance = this.haversineDistance(lastPoint[0], lastPoint[1], currentPoint[0], currentPoint[1]);
+ const distance = haversineDistance(lastPoint[0], lastPoint[1], currentPoint[0], currentPoint[1]);
const timeDifference = (currentPoint[4] - lastPoint[4]) / 60;
if (distance > distanceThresholdMeters || timeDifference > timeThresholdMinutes) {
@@ -335,12 +279,4 @@ export default class extends Controller {
})
).addTo(map);
}
-
- formatDistance(distance) {
- if (distance >= 1000) {
- return (distance / 1000).toFixed(2) + ' km';
- } else {
- return distance.toFixed(0) + ' meters';
- }
- }
}
diff --git a/app/javascript/maps/content.js b/app/javascript/maps/content.js
new file mode 100644
index 00000000..899e5de8
--- /dev/null
+++ b/app/javascript/maps/content.js
@@ -0,0 +1,46 @@
+// Markers, polylines, and popups
+
+export function createPolylinesLayer(markers, map, timezone) {
+ const splitPolylines = [];
+ let currentPolyline = [];
+ const distanceThresholdMeters = parseInt(this.element.dataset.meters_between_routes) || 500;
+ const timeThresholdMinutes = parseInt(this.element.dataset.minutes_between_routes) || 60;
+
+ for (let i = 0, len = markers.length; i < len; i++) {
+ if (currentPolyline.length === 0) {
+ currentPolyline.push(markers[i]);
+ } else {
+ const lastPoint = currentPolyline[currentPolyline.length - 1];
+ const currentPoint = markers[i];
+ const distance = haversineDistance(lastPoint[0], lastPoint[1], currentPoint[0], currentPoint[1]);
+ const timeDifference = (currentPoint[4] - lastPoint[4]) / 60;
+
+ if (distance > distanceThresholdMeters || timeDifference > timeThresholdMinutes) {
+ splitPolylines.push([...currentPolyline]);
+ currentPolyline = [currentPoint];
+ } else {
+ currentPolyline.push(currentPoint);
+ }
+ }
+ }
+
+ if (currentPolyline.length > 0) {
+ splitPolylines.push(currentPolyline);
+ }
+
+ return L.layerGroup(
+ splitPolylines.map((polylineCoordinates, index) => {
+ const latLngs = polylineCoordinates.map((point) => [point[0], point[1]]);
+ const polyline = L.polyline(latLngs, { color: "blue", opacity: 0.6, weight: 3 });
+
+ const startPoint = polylineCoordinates[0];
+ const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
+ const prevPoint = index > 0 ? splitPolylines[index - 1][splitPolylines[index - 1].length - 1] : null;
+ const nextPoint = index < splitPolylines.length - 1 ? splitPolylines[index + 1][0] : null;
+
+ this.addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone);
+
+ return polyline;
+ })
+ ).addTo(map);
+}
diff --git a/app/javascript/maps/helpers.js b/app/javascript/maps/helpers.js
new file mode 100644
index 00000000..4a7d6817
--- /dev/null
+++ b/app/javascript/maps/helpers.js
@@ -0,0 +1,51 @@
+// javascript/maps/helpers.js
+export function formatDistance(distance) {
+ if (distance < 1000) {
+ return `${distance.toFixed(2)} meters`;
+ } else {
+ return `${(distance / 1000).toFixed(2)} km`;
+ }
+}
+
+export function getUrlParameter(name) {
+ return new URLSearchParams(window.location.search).get(name);
+}
+
+export function minutesToDaysHoursMinutes(minutes) {
+ const days = Math.floor(minutes / (24 * 60));
+ const hours = Math.floor((minutes % (24 * 60)) / 60);
+ minutes = minutes % 60;
+ let result = "";
+
+ if (days > 0) {
+ result += `${days}d `;
+ }
+
+ if (hours > 0) {
+ result += `${hours}h `;
+ }
+
+ if (minutes > 0) {
+ result += `${minutes}min`;
+ }
+
+ return result;
+}
+
+export function formatDate(timestamp, timezone) {
+ const date = new Date(timestamp * 1000);
+ return date.toLocaleString("en-GB", { timeZone: timezone });
+}
+
+export 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) +
+ Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
+ Math.sin(dLon / 2) * Math.sin(dLon / 2);
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
+ return R * c * 1000; // Distance in meters
+}
diff --git a/app/javascript/maps/layers.js b/app/javascript/maps/layers.js
new file mode 100644
index 00000000..a5b2196b
--- /dev/null
+++ b/app/javascript/maps/layers.js
@@ -0,0 +1,21 @@
+
+export function osmMapLayer() {
+ return L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ maxZoom: 19,
+ attribution: "© OpenStreetMap",
+ });
+}
+
+export function 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",
+ });
+}
+
+export function addTileLayer(map) {
+ L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
+ maxZoom: 19,
+ attribution: "© OpenStreetMap",
+ }).addTo(map);
+}