2024-06-19 15:16:06 -04:00
|
|
|
import { Controller } from "@hotwired/stimulus";
|
|
|
|
|
import L from "leaflet";
|
|
|
|
|
import "leaflet.heat";
|
2024-11-07 13:00:11 -05:00
|
|
|
import consumer from "../channels/consumer";
|
2024-10-20 14:23:58 -04:00
|
|
|
|
|
|
|
|
import { createMarkersArray } from "../maps/markers";
|
|
|
|
|
|
|
|
|
|
import { createPolylinesLayer } from "../maps/polylines";
|
|
|
|
|
import { updatePolylinesOpacity } from "../maps/polylines";
|
|
|
|
|
|
|
|
|
|
import { fetchAndDrawAreas } from "../maps/areas";
|
|
|
|
|
import { handleAreaCreated } from "../maps/areas";
|
|
|
|
|
|
2024-10-20 14:32:51 -04:00
|
|
|
import { showFlashMessage } from "../maps/helpers";
|
|
|
|
|
|
2024-07-21 09:13:16 -04:00
|
|
|
import { osmMapLayer } from "../maps/layers";
|
|
|
|
|
import { osmHotMapLayer } from "../maps/layers";
|
2024-09-15 15:04:13 -04:00
|
|
|
import { OPNVMapLayer } from "../maps/layers";
|
|
|
|
|
import { openTopoMapLayer } from "../maps/layers";
|
|
|
|
|
import { cyclOsmMapLayer } from "../maps/layers";
|
|
|
|
|
import { esriWorldStreetMapLayer } from "../maps/layers";
|
|
|
|
|
import { esriWorldTopoMapLayer } from "../maps/layers";
|
|
|
|
|
import { esriWorldImageryMapLayer } from "../maps/layers";
|
|
|
|
|
import { esriWorldGrayCanvasMapLayer } from "../maps/layers";
|
2024-11-01 08:40:37 -04:00
|
|
|
import { countryCodesMap } from "../maps/country_codes";
|
2024-10-20 14:32:51 -04:00
|
|
|
|
2024-07-21 14:09:42 -04:00
|
|
|
import "leaflet-draw";
|
2024-03-15 20:07:20 -04:00
|
|
|
|
|
|
|
|
export default class extends Controller {
|
2024-06-19 15:16:06 -04:00
|
|
|
static targets = ["container"];
|
2024-03-15 20:07:20 -04:00
|
|
|
|
2024-08-28 15:34:26 -04:00
|
|
|
settingsButtonAdded = false;
|
|
|
|
|
layerControl = null;
|
|
|
|
|
|
2024-03-15 20:07:20 -04:00
|
|
|
connect() {
|
2024-06-19 15:16:06 -04:00
|
|
|
console.log("Map controller connected");
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-07-27 06:22:56 -04:00
|
|
|
this.apiKey = this.element.dataset.api_key;
|
2024-07-21 10:45:29 -04:00
|
|
|
this.markers = JSON.parse(this.element.dataset.coordinates);
|
|
|
|
|
this.timezone = this.element.dataset.timezone;
|
2024-08-28 14:24:35 -04:00
|
|
|
this.userSettings = JSON.parse(this.element.dataset.user_settings);
|
|
|
|
|
this.clearFogRadius = parseInt(this.userSettings.fog_of_war_meters) || 50;
|
|
|
|
|
this.routeOpacity = parseFloat(this.userSettings.route_opacity) || 0.6;
|
2024-08-28 18:17:51 -04:00
|
|
|
this.distanceUnit = this.element.dataset.distance_unit || "km";
|
2024-10-20 14:23:58 -04:00
|
|
|
this.pointsRenderingMode = this.userSettings.points_rendering_mode || "raw";
|
2024-11-07 07:30:58 -05:00
|
|
|
this.liveMapEnabled = this.userSettings.live_map_enabled || false;
|
2024-11-01 08:40:37 -04:00
|
|
|
this.countryCodesMap = countryCodesMap();
|
2024-11-01 08:29:24 -04:00
|
|
|
|
2024-07-21 10:45:29 -04:00
|
|
|
this.center = this.markers[this.markers.length - 1] || [52.514568, 13.350111];
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-07-31 13:35:35 -04:00
|
|
|
this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 14);
|
2024-07-21 10:45:29 -04:00
|
|
|
|
2024-10-16 09:25:22 -04:00
|
|
|
// Set the maximum bounds to prevent infinite scroll
|
|
|
|
|
var southWest = L.latLng(-90, -180);
|
|
|
|
|
var northEast = L.latLng(90, 180);
|
|
|
|
|
var bounds = L.latLngBounds(southWest, northEast);
|
|
|
|
|
|
|
|
|
|
this.map.setMaxBounds(bounds);
|
|
|
|
|
|
2024-10-20 14:23:58 -04:00
|
|
|
this.markersArray = createMarkersArray(this.markers, this.userSettings);
|
2024-07-21 10:45:29 -04:00
|
|
|
this.markersLayer = L.layerGroup(this.markersArray);
|
2024-10-20 14:23:58 -04:00
|
|
|
this.heatmapMarkers = this.markersArray.map((element) => [element._latlng.lat, element._latlng.lng, 0.2]);
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-10-20 14:23:58 -04:00
|
|
|
this.polylinesLayer = createPolylinesLayer(this.markers, this.map, this.timezone, this.routeOpacity, this.userSettings);
|
2024-07-21 10:45:29 -04:00
|
|
|
this.heatmapLayer = L.heatLayer(this.heatmapMarkers, { radius: 20 }).addTo(this.map);
|
|
|
|
|
this.fogOverlay = L.layerGroup(); // Initialize fog layer
|
2024-07-21 14:09:42 -04:00
|
|
|
this.areasLayer = L.layerGroup(); // Initialize areas layer
|
2024-11-26 08:46:26 -05:00
|
|
|
this.photoMarkers = L.layerGroup();
|
|
|
|
|
|
2024-11-01 08:40:37 -04:00
|
|
|
this.setupScratchLayer(this.countryCodesMap);
|
2024-05-30 05:50:12 -04:00
|
|
|
|
2024-08-28 15:34:26 -04:00
|
|
|
if (!this.settingsButtonAdded) {
|
|
|
|
|
this.addSettingsButton();
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 15:16:06 -04:00
|
|
|
const controlsLayer = {
|
2024-07-21 10:45:29 -04:00
|
|
|
Points: this.markersLayer,
|
|
|
|
|
Polylines: this.polylinesLayer,
|
|
|
|
|
Heatmap: this.heatmapLayer,
|
|
|
|
|
"Fog of War": this.fogOverlay,
|
2024-11-01 08:29:24 -04:00
|
|
|
"Scratch map": this.scratchLayer,
|
2024-11-26 08:46:26 -05:00
|
|
|
Areas: this.areasLayer,
|
|
|
|
|
Photos: this.photoMarkers
|
2024-05-29 17:00:35 -04:00
|
|
|
};
|
2024-03-15 20:07:20 -04:00
|
|
|
|
2024-07-21 14:26:45 -04:00
|
|
|
L.control
|
|
|
|
|
.scale({
|
|
|
|
|
position: "bottomright",
|
|
|
|
|
metric: true,
|
2024-11-07 07:30:58 -05:00
|
|
|
imperial: true,
|
2024-07-21 14:26:45 -04:00
|
|
|
maxWidth: 120,
|
|
|
|
|
})
|
|
|
|
|
.addTo(this.map);
|
|
|
|
|
|
2024-08-28 15:34:26 -04:00
|
|
|
this.layerControl = L.control.layers(this.baseMaps(), controlsLayer).addTo(this.map);
|
2024-05-25 16:14:55 -04:00
|
|
|
|
2024-07-21 14:09:42 -04:00
|
|
|
// Fetch and draw areas when the map is loaded
|
2024-10-20 14:23:58 -04:00
|
|
|
fetchAndDrawAreas(this.areasLayer, this.apiKey);
|
2024-07-21 14:09:42 -04:00
|
|
|
|
2024-06-25 15:57:22 -04:00
|
|
|
let fogEnabled = false;
|
|
|
|
|
|
2024-06-25 16:30:11 -04:00
|
|
|
// Hide fog by default
|
|
|
|
|
document.getElementById('fog').style.display = 'none';
|
|
|
|
|
|
2024-06-25 15:57:22 -04:00
|
|
|
// Toggle fog layer visibility
|
2024-07-21 10:45:29 -04:00
|
|
|
this.map.on('overlayadd', (e) => {
|
2024-06-25 15:57:22 -04:00
|
|
|
if (e.name === 'Fog of War') {
|
|
|
|
|
fogEnabled = true;
|
|
|
|
|
document.getElementById('fog').style.display = 'block';
|
2024-07-21 10:45:29 -04:00
|
|
|
this.updateFog(this.markers, this.clearFogRadius);
|
2024-06-25 15:57:22 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-07-21 10:45:29 -04:00
|
|
|
this.map.on('overlayremove', (e) => {
|
2024-06-25 15:57:22 -04:00
|
|
|
if (e.name === 'Fog of War') {
|
|
|
|
|
fogEnabled = false;
|
|
|
|
|
document.getElementById('fog').style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update fog circles on zoom and move
|
2024-07-21 10:45:29 -04:00
|
|
|
this.map.on('zoomend moveend', () => {
|
2024-06-25 15:57:22 -04:00
|
|
|
if (fogEnabled) {
|
2024-07-21 10:45:29 -04:00
|
|
|
this.updateFog(this.markers, this.clearFogRadius);
|
2024-06-25 15:57:22 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2024-07-21 10:45:29 -04:00
|
|
|
this.addLastMarker(this.map, this.markers);
|
|
|
|
|
this.addEventListeners();
|
2024-07-21 14:09:42 -04:00
|
|
|
|
|
|
|
|
// Initialize Leaflet.draw
|
|
|
|
|
this.initializeDrawControl();
|
|
|
|
|
|
|
|
|
|
// Add event listeners to toggle draw controls
|
|
|
|
|
this.map.on('overlayadd', (e) => {
|
|
|
|
|
if (e.name === 'Areas') {
|
|
|
|
|
this.map.addControl(this.drawControl);
|
|
|
|
|
}
|
2024-11-26 08:46:26 -05:00
|
|
|
if (e.name === 'Photos') {
|
|
|
|
|
// Extract dates from URL parameters
|
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const startDate = urlParams.get('start_at')?.split('T')[0] || new Date().toISOString().split('T')[0];
|
|
|
|
|
const endDate = urlParams.get('end_at')?.split('T')[0] || new Date().toISOString().split('T')[0];
|
|
|
|
|
this.fetchAndDisplayPhotos(startDate, endDate);
|
|
|
|
|
}
|
2024-07-21 14:09:42 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.map.on('overlayremove', (e) => {
|
|
|
|
|
if (e.name === 'Areas') {
|
|
|
|
|
this.map.removeControl(this.drawControl);
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-11-03 10:48:43 -05:00
|
|
|
|
2024-11-07 07:30:58 -05:00
|
|
|
if (this.liveMapEnabled) {
|
|
|
|
|
this.setupSubscription();
|
|
|
|
|
}
|
2024-03-15 20:07:20 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
|
this.map.remove();
|
|
|
|
|
}
|
2024-03-21 18:24:47 -04:00
|
|
|
|
2024-11-03 10:48:43 -05:00
|
|
|
setupSubscription() {
|
|
|
|
|
consumer.subscriptions.create("PointsChannel", {
|
|
|
|
|
received: (data) => {
|
2024-11-03 13:28:33 -05:00
|
|
|
// TODO:
|
|
|
|
|
// Only append the point if its timestamp is within current
|
|
|
|
|
// timespan
|
2024-11-07 07:30:58 -05:00
|
|
|
if (this.map && this.map._loaded) {
|
|
|
|
|
this.appendPoint(data);
|
|
|
|
|
}
|
2024-11-03 10:48:43 -05:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendPoint(data) {
|
|
|
|
|
// Parse the received point data
|
|
|
|
|
const newPoint = data;
|
|
|
|
|
|
|
|
|
|
// Add the new point to the markers array
|
|
|
|
|
this.markers.push(newPoint);
|
|
|
|
|
|
2024-11-03 14:09:53 -05:00
|
|
|
const newMarker = L.marker([newPoint[0], newPoint[1]])
|
2024-11-03 10:48:43 -05:00
|
|
|
this.markersArray.push(newMarker);
|
|
|
|
|
|
|
|
|
|
// Update the markers layer
|
|
|
|
|
this.markersLayer.clearLayers();
|
|
|
|
|
this.markersLayer.addLayer(L.layerGroup(this.markersArray));
|
|
|
|
|
|
|
|
|
|
// Update heatmap
|
|
|
|
|
this.heatmapMarkers.push([newPoint[0], newPoint[1], 0.2]);
|
|
|
|
|
this.heatmapLayer.setLatLngs(this.heatmapMarkers);
|
|
|
|
|
|
|
|
|
|
// Update polylines
|
|
|
|
|
this.polylinesLayer.clearLayers();
|
|
|
|
|
this.polylinesLayer = createPolylinesLayer(
|
|
|
|
|
this.markers,
|
|
|
|
|
this.map,
|
|
|
|
|
this.timezone,
|
|
|
|
|
this.routeOpacity,
|
|
|
|
|
this.userSettings
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Pan map to new location
|
2024-11-03 14:09:53 -05:00
|
|
|
this.map.setView([newPoint[0], newPoint[1]], 16);
|
2024-11-03 10:48:43 -05:00
|
|
|
|
|
|
|
|
// Update fog of war if enabled
|
|
|
|
|
if (this.map.hasLayer(this.fogOverlay)) {
|
|
|
|
|
this.updateFog(this.markers, this.clearFogRadius);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update the last marker
|
|
|
|
|
this.map.eachLayer((layer) => {
|
|
|
|
|
if (layer instanceof L.Marker && !layer._popup) {
|
|
|
|
|
this.map.removeLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-11-03 13:28:33 -05:00
|
|
|
|
2024-11-03 10:48:43 -05:00
|
|
|
this.addLastMarker(this.map, this.markers);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-01 08:40:37 -04:00
|
|
|
async setupScratchLayer(countryCodesMap) {
|
2024-11-01 08:29:24 -04:00
|
|
|
this.scratchLayer = L.geoJSON(null, {
|
|
|
|
|
style: {
|
|
|
|
|
fillColor: '#FFD700',
|
|
|
|
|
fillOpacity: 0.3,
|
|
|
|
|
color: '#FFA500',
|
|
|
|
|
weight: 1
|
|
|
|
|
}
|
2024-11-01 08:43:21 -04:00
|
|
|
})
|
2024-11-01 08:29:24 -04:00
|
|
|
|
2024-11-01 09:05:16 -04:00
|
|
|
try {
|
|
|
|
|
// Up-to-date version can be found on Github:
|
|
|
|
|
// https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson
|
2024-11-01 15:49:59 -04:00
|
|
|
const response = await fetch('/api/v1/countries/borders.json', {
|
2024-11-01 09:05:16 -04:00
|
|
|
headers: {
|
|
|
|
|
'Accept': 'application/geo+json,application/json'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const worldData = await response.json();
|
|
|
|
|
|
|
|
|
|
const visitedCountries = this.getVisitedCountries(countryCodesMap)
|
|
|
|
|
const filteredFeatures = worldData.features.filter(feature =>
|
|
|
|
|
visitedCountries.includes(feature.properties.ISO_A2)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
this.scratchLayer.addData({
|
|
|
|
|
type: 'FeatureCollection',
|
|
|
|
|
features: filteredFeatures
|
|
|
|
|
})
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading GeoJSON:', error);
|
|
|
|
|
}
|
2024-11-01 08:29:24 -04:00
|
|
|
}
|
|
|
|
|
|
2024-11-01 08:40:37 -04:00
|
|
|
getVisitedCountries(countryCodesMap) {
|
2024-11-01 08:29:24 -04:00
|
|
|
if (!this.markers) return [];
|
|
|
|
|
|
|
|
|
|
return [...new Set(
|
|
|
|
|
this.markers
|
|
|
|
|
.filter(marker => marker[7]) // Ensure country exists
|
|
|
|
|
.map(marker => {
|
|
|
|
|
// Convert country name to ISO code, or return the original if not found
|
2024-11-01 08:40:37 -04:00
|
|
|
return countryCodesMap[marker[7]] || marker[7];
|
2024-11-01 08:29:24 -04:00
|
|
|
})
|
|
|
|
|
)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Optional: Add methods to handle user interactions
|
|
|
|
|
toggleScratchLayer() {
|
|
|
|
|
if (this.map.hasLayer(this.scratchLayer)) {
|
|
|
|
|
this.map.removeLayer(this.scratchLayer)
|
|
|
|
|
} else {
|
|
|
|
|
this.scratchLayer.addTo(this.map)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-17 16:00:23 -04:00
|
|
|
baseMaps() {
|
2024-09-15 15:04:13 -04:00
|
|
|
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
2024-10-20 14:23:58 -04:00
|
|
|
|
2024-04-17 16:00:23 -04:00
|
|
|
return {
|
2024-09-15 15:04:13 -04:00
|
|
|
OpenStreetMap: osmMapLayer(this.map, selectedLayerName),
|
|
|
|
|
"OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName),
|
|
|
|
|
OPNV: OPNVMapLayer(this.map, selectedLayerName),
|
|
|
|
|
openTopo: openTopoMapLayer(this.map, selectedLayerName),
|
|
|
|
|
cyclOsm: cyclOsmMapLayer(this.map, selectedLayerName),
|
|
|
|
|
esriWorldStreet: esriWorldStreetMapLayer(this.map, selectedLayerName),
|
|
|
|
|
esriWorldTopo: esriWorldTopoMapLayer(this.map, selectedLayerName),
|
|
|
|
|
esriWorldImagery: esriWorldImageryMapLayer(this.map, selectedLayerName),
|
|
|
|
|
esriWorldGrayCanvas: esriWorldGrayCanvasMapLayer(this.map, selectedLayerName)
|
2024-06-19 15:16:06 -04:00
|
|
|
};
|
2024-04-17 16:00:23 -04:00
|
|
|
}
|
|
|
|
|
|
2024-09-02 17:29:37 -04:00
|
|
|
removeEventListeners() {
|
|
|
|
|
document.removeEventListener('click', this.handleDeleteClick);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-21 10:45:29 -04:00
|
|
|
addEventListeners() {
|
2024-09-02 17:29:37 -04:00
|
|
|
this.handleDeleteClick = (event) => {
|
2024-07-21 10:45:29 -04:00
|
|
|
if (event.target && event.target.classList.contains('delete-point')) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const pointId = event.target.getAttribute('data-id');
|
|
|
|
|
|
|
|
|
|
if (confirm('Are you sure you want to delete this point?')) {
|
2024-07-31 13:35:35 -04:00
|
|
|
this.deletePoint(pointId, this.apiKey);
|
2024-07-21 10:45:29 -04:00
|
|
|
}
|
|
|
|
|
}
|
2024-09-02 17:29:37 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Ensure only one listener is attached by removing any existing ones first
|
|
|
|
|
this.removeEventListeners();
|
|
|
|
|
document.addEventListener('click', this.handleDeleteClick);
|
2024-09-15 15:04:13 -04:00
|
|
|
|
|
|
|
|
// Add an event listener for base layer change in Leaflet
|
|
|
|
|
this.map.on('baselayerchange', (event) => {
|
|
|
|
|
const selectedLayerName = event.name;
|
|
|
|
|
this.updatePreferredBaseLayer(selectedLayerName);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updatePreferredBaseLayer(selectedLayerName) {
|
|
|
|
|
fetch(`/api/v1/settings?api_key=${this.apiKey}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
settings: {
|
|
|
|
|
preferred_map_layer: selectedLayerName
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
.then((response) => response.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
if (data.status === 'success') {
|
2024-10-20 14:32:51 -04:00
|
|
|
showFlashMessage('notice', `Preferred map layer updated to: ${selectedLayerName}`);
|
2024-09-15 15:04:13 -04:00
|
|
|
} else {
|
2024-10-20 14:32:51 -04:00
|
|
|
showFlashMessage('error', data.message);
|
2024-09-15 15:04:13 -04:00
|
|
|
}
|
|
|
|
|
});
|
2024-07-21 10:45:29 -04:00
|
|
|
}
|
|
|
|
|
|
2024-07-31 13:35:35 -04:00
|
|
|
deletePoint(id, apiKey) {
|
|
|
|
|
fetch(`/api/v1/points/${id}?api_key=${apiKey}`, {
|
2024-07-21 10:45:29 -04:00
|
|
|
method: 'DELETE',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.then(response => {
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error('Network response was not ok');
|
|
|
|
|
}
|
|
|
|
|
return response.json();
|
|
|
|
|
})
|
|
|
|
|
.then(data => {
|
|
|
|
|
this.removeMarker(id);
|
|
|
|
|
})
|
|
|
|
|
.catch(error => {
|
|
|
|
|
console.error('There was a problem with the delete request:', error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
removeMarker(id) {
|
|
|
|
|
const markerIndex = this.markersArray.findIndex(marker => marker.getPopup().getContent().includes(`data-id="${id}"`));
|
|
|
|
|
if (markerIndex !== -1) {
|
|
|
|
|
this.markersArray[markerIndex].remove(); // Assuming your marker object has a remove method
|
|
|
|
|
this.markersArray.splice(markerIndex, 1);
|
|
|
|
|
this.markersLayer.clearLayers();
|
|
|
|
|
this.markersLayer.addLayer(L.layerGroup(this.markersArray));
|
|
|
|
|
|
|
|
|
|
// Remove from the markers data array
|
|
|
|
|
this.markers = this.markers.filter(marker => marker[6] !== parseInt(id));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-21 18:54:19 -04:00
|
|
|
addLastMarker(map, markers) {
|
|
|
|
|
if (markers.length > 0) {
|
2024-06-19 15:16:06 -04:00
|
|
|
const lastMarker = markers[markers.length - 1].slice(0, 2);
|
2024-03-21 18:54:19 -04:00
|
|
|
L.marker(lastMarker).addTo(map);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-19 15:16:06 -04:00
|
|
|
|
2024-07-21 10:45:29 -04:00
|
|
|
updateFog(markers, clearFogRadius) {
|
|
|
|
|
var fog = document.getElementById('fog');
|
|
|
|
|
fog.innerHTML = ''; // Clear previous circles
|
|
|
|
|
markers.forEach((point) => {
|
|
|
|
|
const radiusInPixels = this.metersToPixels(this.map, clearFogRadius);
|
|
|
|
|
this.clearFog(point[0], point[1], radiusInPixels);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
metersToPixels(map, meters) {
|
|
|
|
|
const zoom = map.getZoom();
|
|
|
|
|
const latLng = map.getCenter(); // Get map center for correct projection
|
|
|
|
|
const metersPerPixel = this.getMetersPerPixel(latLng.lat, zoom);
|
|
|
|
|
return meters / metersPerPixel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getMetersPerPixel(latitude, zoom) {
|
|
|
|
|
const earthCircumference = 40075016.686; // Earth's circumference in meters
|
|
|
|
|
const metersPerPixel = earthCircumference * Math.cos(latitude * Math.PI / 180) / Math.pow(2, zoom + 8);
|
|
|
|
|
return metersPerPixel;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearFog(lat, lng, radius) {
|
|
|
|
|
var fog = document.getElementById('fog');
|
|
|
|
|
var point = this.map.latLngToContainerPoint([lat, lng]);
|
|
|
|
|
var size = radius * 2;
|
|
|
|
|
var circle = document.createElement('div');
|
|
|
|
|
circle.className = 'unfogged-circle';
|
|
|
|
|
circle.style.width = size + 'px';
|
|
|
|
|
circle.style.height = size + 'px';
|
|
|
|
|
circle.style.left = (point.x - radius) + 'px';
|
|
|
|
|
circle.style.top = (point.y - radius) + 'px';
|
|
|
|
|
circle.style.backdropFilter = 'blur(0px)'; // Remove blur for the circles
|
|
|
|
|
fog.appendChild(circle);
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-21 14:09:42 -04:00
|
|
|
initializeDrawControl() {
|
|
|
|
|
// Initialize the FeatureGroup to store editable layers
|
|
|
|
|
this.drawnItems = new L.FeatureGroup();
|
|
|
|
|
this.map.addLayer(this.drawnItems);
|
|
|
|
|
|
|
|
|
|
// Initialize the draw control and pass it the FeatureGroup of editable layers
|
|
|
|
|
this.drawControl = new L.Control.Draw({
|
|
|
|
|
draw: {
|
|
|
|
|
polyline: false,
|
|
|
|
|
polygon: false,
|
|
|
|
|
rectangle: false,
|
|
|
|
|
marker: false,
|
|
|
|
|
circlemarker: false,
|
|
|
|
|
circle: {
|
|
|
|
|
shapeOptions: {
|
|
|
|
|
color: 'red',
|
|
|
|
|
fillColor: '#f03',
|
|
|
|
|
fillOpacity: 0.5,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle circle creation
|
|
|
|
|
this.map.on(L.Draw.Event.CREATED, (event) => {
|
|
|
|
|
const layer = event.layer;
|
|
|
|
|
|
|
|
|
|
if (event.layerType === 'circle') {
|
2024-10-20 14:23:58 -04:00
|
|
|
handleAreaCreated(this.areasLayer, layer, this.apiKey);
|
2024-07-21 14:09:42 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.drawnItems.addLayer(layer);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-28 14:24:35 -04:00
|
|
|
addSettingsButton() {
|
2024-08-28 15:34:26 -04:00
|
|
|
if (this.settingsButtonAdded) return;
|
|
|
|
|
|
2024-08-28 14:24:35 -04:00
|
|
|
// Define the custom control
|
|
|
|
|
const SettingsControl = L.Control.extend({
|
|
|
|
|
onAdd: (map) => {
|
|
|
|
|
const button = L.DomUtil.create('button', 'map-settings-button');
|
|
|
|
|
button.innerHTML = '⚙️'; // Gear icon
|
|
|
|
|
|
|
|
|
|
// Style the button
|
|
|
|
|
button.style.backgroundColor = 'white';
|
2024-08-28 15:34:26 -04:00
|
|
|
button.style.width = '32px';
|
|
|
|
|
button.style.height = '32px';
|
2024-08-28 14:24:35 -04:00
|
|
|
button.style.border = 'none';
|
|
|
|
|
button.style.cursor = 'pointer';
|
|
|
|
|
button.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
|
|
|
|
|
|
|
|
|
// Disable map interactions when clicking the button
|
|
|
|
|
L.DomEvent.disableClickPropagation(button);
|
|
|
|
|
|
|
|
|
|
// Toggle settings menu on button click
|
|
|
|
|
L.DomEvent.on(button, 'click', () => {
|
|
|
|
|
this.toggleSettingsMenu();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return button;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add the control to the map
|
2024-08-28 15:34:26 -04:00
|
|
|
this.map.addControl(new SettingsControl({ position: 'topleft' }));
|
|
|
|
|
this.settingsButtonAdded = true;
|
2024-08-28 14:24:35 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
toggleSettingsMenu() {
|
|
|
|
|
// If the settings panel already exists, just show/hide it
|
|
|
|
|
if (this.settingsPanel) {
|
|
|
|
|
if (this.settingsPanel._map) {
|
|
|
|
|
this.map.removeControl(this.settingsPanel);
|
|
|
|
|
} else {
|
|
|
|
|
this.map.addControl(this.settingsPanel);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Create the settings panel for the first time
|
2024-08-28 15:34:26 -04:00
|
|
|
this.settingsPanel = L.control({ position: 'topleft' });
|
2024-08-28 14:24:35 -04:00
|
|
|
|
|
|
|
|
this.settingsPanel.onAdd = () => {
|
|
|
|
|
const div = L.DomUtil.create('div', 'leaflet-settings-panel');
|
|
|
|
|
|
|
|
|
|
// Form HTML
|
|
|
|
|
div.innerHTML = `
|
|
|
|
|
<form id="settings-form" class="w-48">
|
|
|
|
|
<label for="route-opacity">Route Opacity</label>
|
|
|
|
|
<div class="join">
|
|
|
|
|
<input type="number" class="input input-ghost join-item focus:input-ghost input-xs input-bordered w-full max-w-xs" id="route-opacity" name="route_opacity" min="0" max="1" step="0.1" value="${this.routeOpacity}">
|
|
|
|
|
<label for="route_opacity_info" class="btn-xs join-item ">?</label>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<label for="fog_of_war_meters">Fog of War radius</label>
|
|
|
|
|
<div class="join">
|
|
|
|
|
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="fog_of_war_meters" name="fog_of_war_meters" min="5" max="100" step="1" value="${this.clearFogRadius}">
|
|
|
|
|
<label for="fog_of_war_meters_info" class="btn-xs join-item">?</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label for="meters_between_routes">Meters between routes</label>
|
|
|
|
|
<div class="join">
|
|
|
|
|
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="meters_between_routes" name="meters_between_routes" step="1" value="${this.userSettings.meters_between_routes}">
|
|
|
|
|
<label for="meters_between_routes_info" class="btn-xs join-item">?</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label for="minutes_between_routes">Minutes between routes</label>
|
|
|
|
|
<div class="join">
|
|
|
|
|
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="minutes_between_routes" name="minutes_between_routes" step="1" value="${this.userSettings.minutes_between_routes}">
|
|
|
|
|
<label for="minutes_between_routes_info" class="btn-xs join-item">?</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label for="time_threshold_minutes">Time threshold minutes</label>
|
|
|
|
|
<div class="join">
|
|
|
|
|
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="time_threshold_minutes" name="time_threshold_minutes" step="1" value="${this.userSettings.time_threshold_minutes}">
|
|
|
|
|
<label for="time_threshold_minutes_info" class="btn-xs join-item">?</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<label for="merge_threshold_minutes">Merge threshold minutes</label>
|
|
|
|
|
<div class="join">
|
|
|
|
|
<input type="number" class="join-item input input-ghost focus:input-ghost input-xs input-bordered w-full max-w-xs" id="merge_threshold_minutes" name="merge_threshold_minutes" step="1" value="${this.userSettings.merge_threshold_minutes}">
|
|
|
|
|
<label for="merge_threshold_minutes_info" class="btn-xs join-item">?</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
2024-10-20 14:55:43 -04:00
|
|
|
|
2024-10-22 06:02:12 -04:00
|
|
|
<label for="points_rendering_mode">
|
|
|
|
|
Points rendering mode
|
|
|
|
|
<label for="points_rendering_mode_info" class="btn-xs join-item inline">?</label>
|
|
|
|
|
</label>
|
|
|
|
|
<label for="raw">
|
|
|
|
|
<input type="radio" id="raw" name="points_rendering_mode" class='w-4' style="width: 20px;" value="raw" ${this.pointsRenderingModeChecked('raw')} />
|
|
|
|
|
Raw
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<label for="simplified">
|
|
|
|
|
<input type="radio" id="simplified" name="points_rendering_mode" class='w-4' style="width: 20px;" value="simplified" ${this.pointsRenderingModeChecked('simplified')}/>
|
|
|
|
|
Simplified
|
|
|
|
|
</label>
|
2024-08-28 14:24:35 -04:00
|
|
|
|
2024-11-07 07:07:54 -05:00
|
|
|
<label for="live_map_enabled">
|
|
|
|
|
Live Map
|
|
|
|
|
<label for="live_map_enabled_info" class="btn-xs join-item inline">?</label>
|
2024-11-07 07:34:22 -05:00
|
|
|
<input type="checkbox" id="live_map_enabled" name="live_map_enabled" class='w-4' style="width: 20px;" value="false" ${this.liveMapEnabledChecked(true)} />
|
2024-11-07 07:07:54 -05:00
|
|
|
</label>
|
|
|
|
|
|
2024-08-28 14:24:35 -04:00
|
|
|
<button type="submit">Update</button>
|
|
|
|
|
</form>
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Style the panel
|
|
|
|
|
div.style.backgroundColor = 'white';
|
|
|
|
|
div.style.padding = '10px';
|
|
|
|
|
div.style.border = '1px solid #ccc';
|
|
|
|
|
div.style.boxShadow = '0 1px 4px rgba(0,0,0,0.3)';
|
|
|
|
|
|
|
|
|
|
// Prevent map interactions when interacting with the form
|
|
|
|
|
L.DomEvent.disableClickPropagation(div);
|
|
|
|
|
|
|
|
|
|
// Add event listener to the form submission
|
|
|
|
|
div.querySelector('#settings-form').addEventListener(
|
|
|
|
|
'submit', this.updateSettings.bind(this)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return div;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.map.addControl(this.settingsPanel);
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-20 14:55:43 -04:00
|
|
|
pointsRenderingModeChecked(value) {
|
|
|
|
|
if (value === this.pointsRenderingMode) {
|
|
|
|
|
return 'checked';
|
|
|
|
|
} else {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-07 07:07:54 -05:00
|
|
|
liveMapEnabledChecked(value) {
|
2024-11-07 07:34:22 -05:00
|
|
|
if (value === this.liveMapEnabled) {
|
2024-11-07 07:07:54 -05:00
|
|
|
return 'checked';
|
|
|
|
|
} else {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-28 14:24:35 -04:00
|
|
|
updateSettings(event) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
|
|
|
|
fetch(`/api/v1/settings?api_key=${this.apiKey}`, {
|
|
|
|
|
method: 'PATCH',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
settings: {
|
|
|
|
|
route_opacity: event.target.route_opacity.value,
|
|
|
|
|
fog_of_war_meters: event.target.fog_of_war_meters.value,
|
|
|
|
|
meters_between_routes: event.target.meters_between_routes.value,
|
|
|
|
|
minutes_between_routes: event.target.minutes_between_routes.value,
|
|
|
|
|
time_threshold_minutes: event.target.time_threshold_minutes.value,
|
|
|
|
|
merge_threshold_minutes: event.target.merge_threshold_minutes.value,
|
2024-11-07 07:07:54 -05:00
|
|
|
points_rendering_mode: event.target.points_rendering_mode.value,
|
|
|
|
|
live_map_enabled: event.target.live_map_enabled.checked
|
2024-08-28 14:24:35 -04:00
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
.then((response) => response.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
if (data.status === 'success') {
|
2024-10-20 14:32:51 -04:00
|
|
|
showFlashMessage('notice', data.message);
|
2024-08-28 14:24:35 -04:00
|
|
|
this.updateMapWithNewSettings(data.settings);
|
2024-11-07 07:34:22 -05:00
|
|
|
|
|
|
|
|
if (data.settings.live_map_enabled) {
|
|
|
|
|
this.setupSubscription();
|
|
|
|
|
}
|
2024-08-28 14:24:35 -04:00
|
|
|
} else {
|
2024-10-20 14:32:51 -04:00
|
|
|
showFlashMessage('error', data.message);
|
2024-08-28 14:24:35 -04:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 = {
|
2024-08-28 15:34:26 -04:00
|
|
|
Points: this.markersLayer,
|
|
|
|
|
Polylines: this.polylinesLayer,
|
|
|
|
|
Heatmap: this.heatmapLayer,
|
2024-08-28 14:24:35 -04:00
|
|
|
"Fog of War": this.fogOverlay,
|
2024-08-28 15:34:26 -04:00
|
|
|
Areas: this.areasLayer,
|
2024-08-28 14:24:35 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Clear all layers except base layers
|
|
|
|
|
this.map.eachLayer((layer) => {
|
|
|
|
|
if (!(layer instanceof L.TileLayer)) {
|
|
|
|
|
this.map.removeLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Recreate layers only if they don't exist
|
2024-10-20 14:23:58 -04:00
|
|
|
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);
|
2024-08-28 15:34:26 -04:00
|
|
|
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();
|
2024-08-28 14:24:35 -04:00
|
|
|
|
|
|
|
|
// Redraw areas
|
2024-10-20 14:23:58 -04:00
|
|
|
fetchAndDrawAreas(this.areasLayer, this.apiKey);
|
2024-08-28 14:24:35 -04:00
|
|
|
|
|
|
|
|
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();
|
2024-10-20 14:23:58 -04:00
|
|
|
updatePolylinesOpacity(this.polylinesLayer, this.routeOpacity);
|
2024-08-28 14:24:35 -04:00
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLayerControlStates() {
|
|
|
|
|
const controls = {};
|
|
|
|
|
|
|
|
|
|
this.map.eachLayer((layer) => {
|
|
|
|
|
const layerName = this.getLayerName(layer);
|
2024-08-28 14:30:47 -04:00
|
|
|
|
2024-08-28 14:24:35 -04:00
|
|
|
if (layerName) {
|
|
|
|
|
controls[layerName] = this.map.hasLayer(layer);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return controls;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getLayerName(layer) {
|
|
|
|
|
const controlLayers = {
|
|
|
|
|
Points: this.markersLayer,
|
|
|
|
|
Polylines: this.polylinesLayer,
|
|
|
|
|
Heatmap: this.heatmapLayer,
|
|
|
|
|
"Fog of War": this.fogOverlay,
|
|
|
|
|
Areas: this.areasLayer,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const [name, val] of Object.entries(controlLayers)) {
|
|
|
|
|
if (val && val.hasLayer && layer && val.hasLayer(layer)) // Check if the group layer contains the current layer
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Direct instance matching
|
|
|
|
|
for (const [name, val] of Object.entries(controlLayers)) {
|
|
|
|
|
if (val === layer) return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return undefined; // Indicate no matching layer name found
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
applyLayerControlStates(states) {
|
|
|
|
|
const layerControl = {
|
|
|
|
|
Points: this.markersLayer,
|
|
|
|
|
Polylines: this.polylinesLayer,
|
|
|
|
|
Heatmap: this.heatmapLayer,
|
|
|
|
|
"Fog of War": this.fogOverlay,
|
|
|
|
|
Areas: this.areasLayer,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
for (const [name, isVisible] of Object.entries(states)) {
|
|
|
|
|
const layer = layerControl[name];
|
2024-08-28 14:30:47 -04:00
|
|
|
|
|
|
|
|
if (isVisible && !this.map.hasLayer(layer)) {
|
|
|
|
|
this.map.addLayer(layer);
|
|
|
|
|
} else if (this.map.hasLayer(layer)) {
|
|
|
|
|
this.map.removeLayer(layer);
|
2024-08-28 14:24:35 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ensure the layer control reflects the current state
|
2024-08-28 15:34:26 -04:00
|
|
|
this.map.removeControl(this.layerControl);
|
2024-08-28 14:24:35 -04:00
|
|
|
this.layerControl = L.control.layers(this.baseMaps(), layerControl).addTo(this.map);
|
|
|
|
|
}
|
2024-11-26 08:46:26 -05:00
|
|
|
|
2024-11-26 10:36:02 -05:00
|
|
|
async fetchAndDisplayPhotos(startDate, endDate, retryCount = 0) {
|
|
|
|
|
const MAX_RETRIES = 3;
|
|
|
|
|
const RETRY_DELAY = 3000; // 3 seconds
|
|
|
|
|
|
2024-11-26 11:36:22 -05:00
|
|
|
// Create loading control
|
|
|
|
|
const LoadingControl = L.Control.extend({
|
|
|
|
|
onAdd: (map) => {
|
|
|
|
|
const container = L.DomUtil.create('div', 'leaflet-loading-control');
|
|
|
|
|
container.innerHTML = '<div class="loading-spinner"></div>';
|
|
|
|
|
return container;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const loadingControl = new LoadingControl({ position: 'topleft' });
|
|
|
|
|
this.map.addControl(loadingControl);
|
|
|
|
|
|
2024-11-26 08:46:26 -05:00
|
|
|
try {
|
|
|
|
|
const params = new URLSearchParams({
|
|
|
|
|
api_key: this.apiKey,
|
|
|
|
|
start_date: startDate,
|
|
|
|
|
end_date: endDate
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const response = await fetch(`/api/v1/photos?${params}`);
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const photos = await response.json();
|
|
|
|
|
this.photoMarkers.clearLayers();
|
|
|
|
|
|
2024-11-26 11:36:22 -05:00
|
|
|
// Create a promise for each photo to track when it's fully loaded
|
|
|
|
|
const photoLoadPromises = photos.map(photo => {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const img = new Image();
|
|
|
|
|
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${this.apiKey}`;
|
|
|
|
|
|
|
|
|
|
img.onload = () => {
|
|
|
|
|
this.createPhotoMarker(photo);
|
|
|
|
|
resolve();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
img.onerror = () => {
|
|
|
|
|
console.error(`Failed to load photo ${photo.id}`);
|
|
|
|
|
resolve(); // Resolve anyway to not block other photos
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
img.src = thumbnailUrl;
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Wait for all photos to be loaded and rendered
|
|
|
|
|
await Promise.all(photoLoadPromises);
|
2024-11-26 08:46:26 -05:00
|
|
|
|
|
|
|
|
if (!this.map.hasLayer(this.photoMarkers)) {
|
|
|
|
|
this.photoMarkers.addTo(this.map);
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 11:36:22 -05:00
|
|
|
// Show checkmark for 1 second before removing
|
|
|
|
|
const loadingSpinner = document.querySelector('.loading-spinner');
|
|
|
|
|
loadingSpinner.classList.add('done');
|
|
|
|
|
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
|
|
2024-11-26 08:46:26 -05:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error fetching photos:', error);
|
2024-11-26 11:36:22 -05:00
|
|
|
showFlashMessage('error', 'Failed to fetch photos');
|
2024-11-26 10:36:02 -05:00
|
|
|
|
|
|
|
|
if (retryCount < MAX_RETRIES) {
|
|
|
|
|
console.log(`Retrying in ${RETRY_DELAY/1000} seconds... (Attempt ${retryCount + 1}/${MAX_RETRIES})`);
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
this.fetchAndDisplayPhotos(startDate, endDate, retryCount + 1);
|
|
|
|
|
}, RETRY_DELAY);
|
|
|
|
|
} else {
|
|
|
|
|
showFlashMessage('error', 'Failed to fetch photos after multiple attempts');
|
|
|
|
|
}
|
2024-11-26 11:36:22 -05:00
|
|
|
} finally {
|
|
|
|
|
// Remove loading control after the delay
|
|
|
|
|
this.map.removeControl(loadingControl);
|
2024-11-26 08:46:26 -05:00
|
|
|
}
|
|
|
|
|
}
|
2024-11-26 10:36:02 -05:00
|
|
|
|
|
|
|
|
createPhotoMarker(photo) {
|
|
|
|
|
if (!photo.exifInfo?.latitude || !photo.exifInfo?.longitude) return;
|
|
|
|
|
|
|
|
|
|
const thumbnailUrl = `/api/v1/photos/${photo.id}/thumbnail.jpg?api_key=${this.apiKey}`;
|
|
|
|
|
|
|
|
|
|
const icon = L.divIcon({
|
|
|
|
|
className: 'photo-marker',
|
|
|
|
|
html: `<img src="${thumbnailUrl}" style="width: 48px; height: 48px;">`,
|
|
|
|
|
iconSize: [48, 48]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const marker = L.marker(
|
|
|
|
|
[photo.exifInfo.latitude, photo.exifInfo.longitude],
|
|
|
|
|
{ icon }
|
|
|
|
|
);
|
|
|
|
|
|
2024-11-26 12:03:46 -05:00
|
|
|
const startOfDay = new Date(photo.localDateTime);
|
|
|
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
const endOfDay = new Date(photo.localDateTime);
|
|
|
|
|
endOfDay.setHours(23, 59, 59, 999);
|
|
|
|
|
|
|
|
|
|
const queryParams = {
|
|
|
|
|
takenAfter: startOfDay.toISOString(),
|
|
|
|
|
takenBefore: endOfDay.toISOString()
|
|
|
|
|
};
|
|
|
|
|
const encodedQuery = encodeURIComponent(JSON.stringify(queryParams));
|
|
|
|
|
const immich_photo_link = `${this.userSettings.immich_url}/search?query=${encodedQuery}`;
|
2024-11-26 10:36:02 -05:00
|
|
|
const popupContent = `
|
|
|
|
|
<div class="max-w-xs">
|
2024-11-26 12:03:46 -05:00
|
|
|
<a href="${immich_photo_link}" target="_blank">
|
|
|
|
|
<img src="${thumbnailUrl}"
|
|
|
|
|
class="w-8 h-8 mb-2 rounded"
|
|
|
|
|
alt="${photo.originalFileName}">
|
|
|
|
|
</a>
|
2024-11-26 10:36:02 -05:00
|
|
|
<h3 class="font-bold">${photo.originalFileName}</h3>
|
|
|
|
|
<p>Taken: ${new Date(photo.localDateTime).toLocaleString()}</p>
|
|
|
|
|
<p>Location: ${photo.exifInfo.city}, ${photo.exifInfo.state}, ${photo.exifInfo.country}</p>
|
|
|
|
|
${photo.type === 'VIDEO' ? '🎥 Video' : '📷 Photo'}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
marker.bindPopup(popupContent);
|
|
|
|
|
|
|
|
|
|
this.photoMarkers.addLayer(marker);
|
|
|
|
|
}
|
2024-03-15 20:07:20 -04:00
|
|
|
}
|