dawarich/app/javascript/maps_maplibre/layers/points_layer.js
2025-12-14 00:32:12 +01:00

265 lines
7 KiB
JavaScript

import { BaseLayer } from './base_layer'
import { Toast } from 'maps_maplibre/components/toast'
/**
* Points layer for displaying individual location points
* Supports dragging points to update their positions
*/
export class PointsLayer extends BaseLayer {
constructor(map, options = {}) {
super(map, { id: 'points', ...options })
this.apiClient = options.apiClient
this.layerManager = options.layerManager
this.isDragging = false
this.draggedFeature = null
this.canvas = null
// Bind event handlers once and store references for proper cleanup
this._onMouseEnter = this.onMouseEnter.bind(this)
this._onMouseLeave = this.onMouseLeave.bind(this)
this._onMouseDown = this.onMouseDown.bind(this)
this._onMouseMove = this.onMouseMove.bind(this)
this._onMouseUp = this.onMouseUp.bind(this)
}
getSourceConfig() {
return {
type: 'geojson',
data: this.data || {
type: 'FeatureCollection',
features: []
}
}
}
getLayerConfigs() {
return [
// Individual points
{
id: this.id,
type: 'circle',
source: this.sourceId,
paint: {
'circle-color': '#3b82f6',
'circle-radius': 6,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
}
]
}
/**
* Enable dragging for points
*/
enableDragging() {
if (this.draggingEnabled) return
this.draggingEnabled = true
this.canvas = this.map.getCanvasContainer()
// Change cursor to pointer when hovering over points
this.map.on('mouseenter', this.id, this._onMouseEnter)
this.map.on('mouseleave', this.id, this._onMouseLeave)
// Handle drag events
this.map.on('mousedown', this.id, this._onMouseDown)
}
/**
* Disable dragging for points
*/
disableDragging() {
if (!this.draggingEnabled) return
this.draggingEnabled = false
this.map.off('mouseenter', this.id, this._onMouseEnter)
this.map.off('mouseleave', this.id, this._onMouseLeave)
this.map.off('mousedown', this.id, this._onMouseDown)
}
onMouseEnter() {
this.canvas.style.cursor = 'move'
}
onMouseLeave() {
if (!this.isDragging) {
this.canvas.style.cursor = ''
}
}
onMouseDown(e) {
// Prevent default map drag behavior
e.preventDefault()
// Store the feature being dragged
this.draggedFeature = e.features[0]
this.isDragging = true
this.canvas.style.cursor = 'grabbing'
// Bind mouse move and up events
this.map.on('mousemove', this._onMouseMove)
this.map.once('mouseup', this._onMouseUp)
}
onMouseMove(e) {
if (!this.isDragging || !this.draggedFeature) return
// Get the new coordinates
const coords = e.lngLat
// Update the feature's coordinates in the source
const source = this.map.getSource(this.sourceId)
if (source) {
const data = source._data
const feature = data.features.find(f => f.properties.id === this.draggedFeature.properties.id)
if (feature) {
feature.geometry.coordinates = [coords.lng, coords.lat]
source.setData(data)
}
}
}
async onMouseUp(e) {
if (!this.isDragging || !this.draggedFeature) return
const coords = e.lngLat
const pointId = this.draggedFeature.properties.id
const originalCoords = this.draggedFeature.geometry.coordinates
// Clean up drag state
this.isDragging = false
this.canvas.style.cursor = ''
this.map.off('mousemove', this._onMouseMove)
// Update the point on the backend
try {
await this.updatePointPosition(pointId, coords.lat, coords.lng)
// Update routes after successful point update
await this.updateConnectedRoutes(pointId, originalCoords, [coords.lng, coords.lat])
} catch (error) {
console.error('Failed to update point:', error)
// Revert the point position on error
const source = this.map.getSource(this.sourceId)
if (source) {
const data = source._data
const feature = data.features.find(f => f.properties.id === pointId)
if (feature && originalCoords) {
feature.geometry.coordinates = originalCoords
source.setData(data)
}
}
Toast.error('Failed to update point position. Please try again.')
}
this.draggedFeature = null
}
/**
* Update point position via API
*/
async updatePointPosition(pointId, latitude, longitude) {
if (!this.apiClient) {
throw new Error('API client not configured')
}
const response = await fetch(`/api/v1/points/${pointId}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': `Bearer ${this.apiClient.apiKey}`
},
body: JSON.stringify({
point: {
latitude: latitude.toString(),
longitude: longitude.toString()
}
})
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
}
/**
* Update connected route segments when a point is moved
*/
async updateConnectedRoutes(pointId, oldCoords, newCoords) {
if (!this.layerManager) {
console.warn('LayerManager not configured, cannot update routes')
return
}
const routesLayer = this.layerManager.getLayer('routes')
if (!routesLayer) {
console.warn('Routes layer not found')
return
}
const routesSource = this.map.getSource(routesLayer.sourceId)
if (!routesSource) {
console.warn('Routes source not found')
return
}
const routesData = routesSource._data
if (!routesData || !routesData.features) {
return
}
// Tolerance for coordinate comparison (account for floating point precision)
const tolerance = 0.0001
let routesUpdated = false
// Find and update route segments that contain the moved point
routesData.features.forEach(feature => {
if (feature.geometry.type === 'LineString') {
const coordinates = feature.geometry.coordinates
// Check each coordinate in the line
for (let i = 0; i < coordinates.length; i++) {
const coord = coordinates[i]
// Check if this coordinate matches the old position
if (Math.abs(coord[0] - oldCoords[0]) < tolerance &&
Math.abs(coord[1] - oldCoords[1]) < tolerance) {
// Update to new position
coordinates[i] = newCoords
routesUpdated = true
}
}
}
})
// Update the routes source if any routes were modified
if (routesUpdated) {
routesSource.setData(routesData)
}
}
/**
* Override add method to enable dragging when layer is added
*/
add(data) {
super.add(data)
// Wait for next tick to ensure layers are fully added before enabling dragging
setTimeout(() => {
this.enableDragging()
}, 100)
}
/**
* Override remove method to clean up dragging handlers
*/
remove() {
this.disableDragging()
super.remove()
}
}