2025-01-24 09:03:57 -05:00
|
|
|
// This controller is being used on:
|
|
|
|
|
// - trips/show
|
|
|
|
|
// - trips/edit
|
|
|
|
|
// - trips/new
|
|
|
|
|
|
2024-11-27 15:37:21 -05:00
|
|
|
import { Controller } from "@hotwired/stimulus"
|
|
|
|
|
import L from "leaflet"
|
2025-01-24 06:01:54 -05:00
|
|
|
import {
|
|
|
|
|
osmMapLayer,
|
|
|
|
|
osmHotMapLayer,
|
|
|
|
|
OPNVMapLayer,
|
|
|
|
|
openTopoMapLayer,
|
|
|
|
|
cyclOsmMapLayer,
|
|
|
|
|
esriWorldStreetMapLayer,
|
|
|
|
|
esriWorldTopoMapLayer,
|
|
|
|
|
esriWorldImageryMapLayer,
|
|
|
|
|
esriWorldGrayCanvasMapLayer
|
|
|
|
|
} from "../maps/layers"
|
2024-11-27 15:37:21 -05:00
|
|
|
import { createPopupContent } from "../maps/popups"
|
2025-01-24 06:01:54 -05:00
|
|
|
import {
|
|
|
|
|
fetchAndDisplayPhotos,
|
|
|
|
|
showFlashMessage
|
|
|
|
|
} from '../maps/helpers';
|
2024-11-27 15:37:21 -05:00
|
|
|
|
|
|
|
|
export default class extends Controller {
|
2024-11-28 07:20:03 -05:00
|
|
|
static targets = ["container", "startedAt", "endedAt"]
|
|
|
|
|
static values = { }
|
2024-11-27 15:37:21 -05:00
|
|
|
|
|
|
|
|
connect() {
|
2024-11-28 07:50:21 -05:00
|
|
|
if (!this.hasContainerTarget) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-28 07:20:03 -05:00
|
|
|
console.log("Trips controller connected")
|
2025-01-24 08:54:10 -05:00
|
|
|
|
2024-11-28 07:20:03 -05:00
|
|
|
this.apiKey = this.containerTarget.dataset.api_key
|
2025-01-24 08:54:10 -05:00
|
|
|
this.userSettings = JSON.parse(this.containerTarget.dataset.user_settings || '{}')
|
2024-11-28 07:20:03 -05:00
|
|
|
this.timezone = this.containerTarget.dataset.timezone
|
|
|
|
|
this.distanceUnit = this.containerTarget.dataset.distance_unit
|
|
|
|
|
|
|
|
|
|
// Initialize map and layers
|
|
|
|
|
this.initializeMap()
|
|
|
|
|
|
|
|
|
|
// Add event listener for coordinates updates
|
|
|
|
|
this.element.addEventListener('coordinates-updated', (event) => {
|
|
|
|
|
this.updateMapWithCoordinates(event.detail.coordinates)
|
|
|
|
|
})
|
|
|
|
|
}
|
2024-11-27 15:37:21 -05:00
|
|
|
|
2024-11-28 07:20:03 -05:00
|
|
|
// Move map initialization to separate method
|
|
|
|
|
initializeMap() {
|
2024-11-27 15:37:21 -05:00
|
|
|
// Initialize layer groups
|
|
|
|
|
this.polylinesLayer = L.layerGroup()
|
|
|
|
|
this.photoMarkers = L.layerGroup()
|
|
|
|
|
|
2024-11-28 07:20:03 -05:00
|
|
|
// Set default center and zoom for world view
|
2025-01-24 08:54:10 -05:00
|
|
|
const center = [20, 0] // Roughly centers the world map
|
|
|
|
|
const zoom = 2
|
2024-11-27 15:37:21 -05:00
|
|
|
|
|
|
|
|
// Initialize map
|
2024-11-28 07:20:03 -05:00
|
|
|
this.map = L.map(this.containerTarget).setView(center, zoom)
|
2024-11-27 15:37:21 -05:00
|
|
|
|
|
|
|
|
// Add base map layer
|
|
|
|
|
osmMapLayer(this.map, "OpenStreetMap")
|
|
|
|
|
|
|
|
|
|
// Add scale control to bottom right
|
|
|
|
|
L.control.scale({
|
|
|
|
|
position: 'bottomright',
|
|
|
|
|
imperial: this.distanceUnit === 'mi',
|
|
|
|
|
metric: this.distanceUnit === 'km',
|
|
|
|
|
maxWidth: 120
|
|
|
|
|
}).addTo(this.map)
|
|
|
|
|
|
|
|
|
|
const overlayMaps = {
|
|
|
|
|
"Route": this.polylinesLayer,
|
|
|
|
|
"Photos": this.photoMarkers
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Add layer control
|
|
|
|
|
L.control.layers(this.baseMaps(), overlayMaps).addTo(this.map)
|
|
|
|
|
|
2024-11-28 04:40:08 -05:00
|
|
|
// Add event listener for layer changes
|
|
|
|
|
this.map.on('overlayadd', (e) => {
|
2024-11-29 05:52:57 -05:00
|
|
|
if (e.name !== 'Photos') return;
|
|
|
|
|
|
2025-01-24 09:35:35 -05:00
|
|
|
const startedAt = this.element.dataset.started_at;
|
|
|
|
|
const endedAt = this.element.dataset.ended_at;
|
|
|
|
|
|
|
|
|
|
console.log('Dataset values:', {
|
|
|
|
|
startedAt,
|
|
|
|
|
endedAt,
|
|
|
|
|
path: this.element.dataset.path
|
|
|
|
|
});
|
|
|
|
|
|
2024-12-03 09:12:20 -05:00
|
|
|
if ((!this.userSettings.immich_url || !this.userSettings.immich_api_key) && (!this.userSettings.photoprism_url || !this.userSettings.photoprism_api_key)) {
|
2024-11-29 05:52:57 -05:00
|
|
|
showFlashMessage(
|
|
|
|
|
'error',
|
2024-12-03 09:12:20 -05:00
|
|
|
'Photos integration is not configured. Please check your integrations settings.'
|
2024-11-29 05:52:57 -05:00
|
|
|
);
|
|
|
|
|
return;
|
2024-11-28 04:40:08 -05:00
|
|
|
}
|
2024-11-29 05:52:57 -05:00
|
|
|
|
2025-01-24 09:35:35 -05:00
|
|
|
// Try to get dates from coordinates first, then fall back to path data
|
|
|
|
|
let startDate, endDate;
|
|
|
|
|
|
|
|
|
|
if (this.coordinates?.length) {
|
|
|
|
|
const firstCoord = this.coordinates[0];
|
|
|
|
|
const lastCoord = this.coordinates[this.coordinates.length - 1];
|
|
|
|
|
startDate = new Date(firstCoord[4] * 1000).toISOString().split('T')[0];
|
|
|
|
|
endDate = new Date(lastCoord[4] * 1000).toISOString().split('T')[0];
|
|
|
|
|
} else if (startedAt && endedAt) {
|
|
|
|
|
// Parse the dates and format them correctly
|
|
|
|
|
startDate = new Date(startedAt).toISOString().split('T')[0];
|
|
|
|
|
endDate = new Date(endedAt).toISOString().split('T')[0];
|
|
|
|
|
} else {
|
|
|
|
|
console.log('No date range available for photos');
|
|
|
|
|
showFlashMessage(
|
|
|
|
|
'error',
|
|
|
|
|
'No date range available for photos. Please ensure the trip has start and end dates.'
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-11-29 05:52:57 -05:00
|
|
|
|
|
|
|
|
fetchAndDisplayPhotos({
|
|
|
|
|
map: this.map,
|
|
|
|
|
photoMarkers: this.photoMarkers,
|
|
|
|
|
apiKey: this.apiKey,
|
|
|
|
|
startDate: startDate,
|
|
|
|
|
endDate: endDate,
|
|
|
|
|
userSettings: this.userSettings
|
|
|
|
|
});
|
2024-11-28 04:40:08 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Add markers and route
|
2024-11-27 15:37:21 -05:00
|
|
|
if (this.coordinates?.length > 0) {
|
|
|
|
|
this.addMarkers()
|
|
|
|
|
this.addPolyline()
|
|
|
|
|
this.fitMapToBounds()
|
|
|
|
|
}
|
2025-01-24 08:54:10 -05:00
|
|
|
|
|
|
|
|
// After map initialization, add the path if it exists
|
|
|
|
|
if (this.containerTarget.dataset.path) {
|
|
|
|
|
const pathData = this.containerTarget.dataset.path.replace(/^"|"$/g, ''); // Remove surrounding quotes
|
|
|
|
|
const coordinates = this.parseLineString(pathData);
|
|
|
|
|
|
|
|
|
|
const polyline = L.polyline(coordinates, {
|
|
|
|
|
color: 'blue',
|
|
|
|
|
opacity: 0.8,
|
|
|
|
|
weight: 3,
|
|
|
|
|
zIndexOffset: 400
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
polyline.addTo(this.polylinesLayer);
|
|
|
|
|
this.polylinesLayer.addTo(this.map);
|
|
|
|
|
|
|
|
|
|
// Fit the map to the polyline bounds
|
|
|
|
|
if (coordinates.length > 0) {
|
|
|
|
|
this.map.fitBounds(polyline.getBounds(), { padding: [50, 50] });
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-27 15:37:21 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
|
if (this.map) {
|
|
|
|
|
this.map.remove()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
baseMaps() {
|
|
|
|
|
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
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)
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addMarkers() {
|
|
|
|
|
this.coordinates.forEach(coord => {
|
2025-01-07 08:31:06 -05:00
|
|
|
const marker = L.circleMarker(
|
|
|
|
|
[coord[0], coord[1]],
|
|
|
|
|
{
|
|
|
|
|
radius: 4,
|
|
|
|
|
color: coord[5] < 0 ? "orange" : "blue",
|
|
|
|
|
zIndexOffset: 1000
|
|
|
|
|
}
|
|
|
|
|
)
|
2024-11-27 15:37:21 -05:00
|
|
|
|
|
|
|
|
const popupContent = createPopupContent(coord, this.timezone, this.distanceUnit)
|
|
|
|
|
marker.bindPopup(popupContent)
|
2025-01-24 09:35:35 -05:00
|
|
|
marker.addTo(this.polylinesLayer)
|
2024-11-27 15:37:21 -05:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addPolyline() {
|
|
|
|
|
const points = this.coordinates.map(coord => [coord[0], coord[1]])
|
|
|
|
|
const polyline = L.polyline(points, {
|
|
|
|
|
color: 'blue',
|
2025-01-07 08:31:06 -05:00
|
|
|
opacity: 0.8,
|
2024-11-27 15:37:21 -05:00
|
|
|
weight: 3,
|
2025-01-07 08:31:06 -05:00
|
|
|
zIndexOffset: 400
|
2024-11-27 15:37:21 -05:00
|
|
|
})
|
|
|
|
|
// Add to polylines layer instead of directly to map
|
|
|
|
|
this.polylinesLayer.addTo(this.map)
|
|
|
|
|
polyline.addTo(this.polylinesLayer)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fitMapToBounds() {
|
|
|
|
|
const bounds = L.latLngBounds(
|
|
|
|
|
this.coordinates.map(coord => [coord[0], coord[1]])
|
|
|
|
|
)
|
|
|
|
|
this.map.fitBounds(bounds, { padding: [50, 50] })
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-24 09:03:57 -05:00
|
|
|
// Update coordinates and refresh the map
|
2024-11-28 07:20:03 -05:00
|
|
|
updateMapWithCoordinates(newCoordinates) {
|
|
|
|
|
// Transform the coordinates to match the expected format
|
|
|
|
|
this.coordinates = newCoordinates.map(point => [
|
|
|
|
|
parseFloat(point.latitude),
|
|
|
|
|
parseFloat(point.longitude),
|
2024-11-28 07:50:21 -05:00
|
|
|
point.id,
|
|
|
|
|
null, // This is so we can use the same order and position of elements in the coordinates object as in the api/v1/points response
|
2024-11-28 07:20:03 -05:00
|
|
|
(point.timestamp).toString()
|
2024-11-28 07:50:21 -05:00
|
|
|
]).sort((a, b) => a[4] - b[4]);
|
2024-11-28 07:20:03 -05:00
|
|
|
|
|
|
|
|
// Clear existing layers
|
|
|
|
|
this.polylinesLayer.clearLayers()
|
|
|
|
|
this.photoMarkers.clearLayers()
|
|
|
|
|
|
|
|
|
|
// Add new markers and route if coordinates exist
|
|
|
|
|
if (this.coordinates?.length > 0) {
|
|
|
|
|
this.addMarkers()
|
|
|
|
|
this.addPolyline()
|
|
|
|
|
this.fitMapToBounds()
|
|
|
|
|
}
|
2024-11-27 15:37:21 -05:00
|
|
|
}
|
2025-01-24 08:54:10 -05:00
|
|
|
|
|
|
|
|
// Add this method to parse the LineString format
|
|
|
|
|
parseLineString(lineString) {
|
|
|
|
|
// Remove LINESTRING and parentheses, then split into coordinate pairs
|
|
|
|
|
const coordsString = lineString.replace('LINESTRING (', '').replace(')', '');
|
|
|
|
|
const coords = coordsString.split(', ');
|
|
|
|
|
|
|
|
|
|
// Convert each coordinate pair to [lat, lng] format
|
|
|
|
|
return coords.map(coord => {
|
|
|
|
|
const [lng, lat] = coord.split(' ').map(Number);
|
|
|
|
|
return [lat, lng]; // Swap to lat, lng for Leaflet
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-11-27 15:37:21 -05:00
|
|
|
}
|