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() } }