mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 08:47:11 -05:00
* fix: move foreman to global gems to fix startup crash (#1971) * Update exporting code to stream points data to file in batches to red… (#1980) * Update exporting code to stream points data to file in batches to reduce memory usage * Update changelog * Update changelog * Feature/maplibre frontend (#1953) * Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet * Implement phase 1 * Phases 1-3 + part of 4 * Fix e2e tests * Phase 6 * Implement fog of war * Phase 7 * Next step: fix specs, phase 7 done * Use our own map tiles * Extract v2 map logic to separate manager classes * Update settings panel on v2 map * Update v2 e2e tests structure * Reimplement location search in maps v2 * Update speed routes * Implement visits and places creation in v2 * Fix last failing test * Implement visits merging * Fix a routes e2e test and simplify the routes layer styling. * Extract js to modules from maps_v2_controller.js * Implement area creation * Fix spec problem * Fix some e2e tests * Implement live mode in v2 map * Update icons and panel * Extract some styles * Remove unused file * Start adding dark theme to popups on MapLibre maps * Make popups respect dark theme * Move v2 maps to maplibre namespace * Update v2 references to maplibre * Put place, area and visit info into side panel * Update API to use safe settings config method * Fix specs * Fix method name to config in SafeSettings and update usages accordingly * Add missing public files * Add handling for real time points * Fix remembering enabled/disabled layers of the v2 map * Fix lots of e2e tests * Add settings to select map version * Use maps/v2 as main path for MapLibre maps * Update routing * Update live mode * Update maplibre controller * Update changelog * Remove some console.log statements * Pull only necessary data for map v2 points * Feature/raw data archive (#2009) * 0.36.2 (#2007) * fix: move foreman to global gems to fix startup crash (#1971) * Update exporting code to stream points data to file in batches to red… (#1980) * Update exporting code to stream points data to file in batches to reduce memory usage * Update changelog * Update changelog * Feature/maplibre frontend (#1953) * Add a plan to use MapLibre GL JS for the frontend map rendering, replacing Leaflet * Implement phase 1 * Phases 1-3 + part of 4 * Fix e2e tests * Phase 6 * Implement fog of war * Phase 7 * Next step: fix specs, phase 7 done * Use our own map tiles * Extract v2 map logic to separate manager classes * Update settings panel on v2 map * Update v2 e2e tests structure * Reimplement location search in maps v2 * Update speed routes * Implement visits and places creation in v2 * Fix last failing test * Implement visits merging * Fix a routes e2e test and simplify the routes layer styling. * Extract js to modules from maps_v2_controller.js * Implement area creation * Fix spec problem * Fix some e2e tests * Implement live mode in v2 map * Update icons and panel * Extract some styles * Remove unused file * Start adding dark theme to popups on MapLibre maps * Make popups respect dark theme * Move v2 maps to maplibre namespace * Update v2 references to maplibre * Put place, area and visit info into side panel * Update API to use safe settings config method * Fix specs * Fix method name to config in SafeSettings and update usages accordingly * Add missing public files * Add handling for real time points * Fix remembering enabled/disabled layers of the v2 map * Fix lots of e2e tests * Add settings to select map version * Use maps/v2 as main path for MapLibre maps * Update routing * Update live mode * Update maplibre controller * Update changelog * Remove some console.log statements --------- Co-authored-by: Robin Tuszik <mail@robin.gg> * Remove esbuild scripts from package.json * Remove sideEffects field from package.json * Raw data archivation * Add tests * Fix tests * Fix tests * Update ExceptionReporter * Add schedule to run raw data archival job monthly * Change file structure for raw data archival feature * Update changelog and version for raw data archival feature --------- Co-authored-by: Robin Tuszik <mail@robin.gg> * Set raw_data to an empty hash instead of nil when archiving * Fix storage configuration and file extraction * Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation (#2018) * Consider MIN_MINUTES_SPENT_IN_CITY during stats calculation * Remove raw data from visited cities api endpoint * Use user timezone to show dates on maps (#2020) * Fix/pre epoch time (#2019) * Use user timezone to show dates on maps * Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates. * Limit timestamps to valid range to prevent database errors when users enter pre-epoch dates. * Fix tests failing due to new index on stats table * Fix failing specs * Update redis client configuration to support unix socket connection * Update changelog * Fix kml kmz import issues (#2023) * Fix kml kmz import issues * Refactor KML importer to improve readability and maintainability * Implement moving points in map v2 and fix route rendering logic to ma… (#2027) * Implement moving points in map v2 and fix route rendering logic to match map v1. * Fix route spec * fix(maplibre): update date format to ISO 8601 (#2029) * Add verification step to raw data archival process (#2028) * Add verification step to raw data archival process * Add actual verification of raw data archives after creation, and only clear raw_data for verified archives. * Fix failing specs * Eliminate zip-bomb risk * Fix potential memory leak in js * Return .keep files * Use Toast instead of alert for notifications * Add help section to navbar dropdown * Update changelog * Remove raw_data_archival_job * Ensure file is being closed properly after reading in Archivable concern --------- Co-authored-by: Robin Tuszik <mail@robin.gg>
265 lines
7 KiB
JavaScript
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()
|
|
}
|
|
}
|