dawarich/app/javascript/controllers/area_selector_controller.js
Evgenii Burmakin 8934c29fce
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>
2025-12-06 20:54:49 +01:00

161 lines
3.7 KiB
JavaScript

import { Controller } from '@hotwired/stimulus'
import { createRectangle } from 'maps_maplibre/utils/geometry'
/**
* Area selector controller
* Draw rectangle selection on map
*/
export default class extends Controller {
static outlets = ['mapsV2']
connect() {
this.isSelecting = false
this.startPoint = null
this.currentPoint = null
}
/**
* Start rectangle selection mode
*/
startSelection() {
if (!this.hasMapsV2Outlet) {
console.error('Maps V2 outlet not found')
return
}
this.isSelecting = true
const map = this.mapsV2Outlet.map
map.getCanvas().style.cursor = 'crosshair'
// Add temporary layer for selection
if (!map.getSource('selection-source')) {
map.addSource('selection-source', {
type: 'geojson',
data: { type: 'FeatureCollection', features: [] }
})
map.addLayer({
id: 'selection-fill',
type: 'fill',
source: 'selection-source',
paint: {
'fill-color': '#3b82f6',
'fill-opacity': 0.2
}
})
map.addLayer({
id: 'selection-outline',
type: 'line',
source: 'selection-source',
paint: {
'line-color': '#3b82f6',
'line-width': 2,
'line-dasharray': [2, 2]
}
})
}
// Add event listeners
map.on('mousedown', this.onMouseDown)
map.on('mousemove', this.onMouseMove)
map.on('mouseup', this.onMouseUp)
}
/**
* Cancel selection mode
*/
cancelSelection() {
if (!this.hasMapsV2Outlet) return
this.isSelecting = false
this.startPoint = null
this.currentPoint = null
const map = this.mapsV2Outlet.map
map.getCanvas().style.cursor = ''
// Clear selection
const source = map.getSource('selection-source')
if (source) {
source.setData({ type: 'FeatureCollection', features: [] })
}
// Remove event listeners
map.off('mousedown', this.onMouseDown)
map.off('mousemove', this.onMouseMove)
map.off('mouseup', this.onMouseUp)
}
/**
* Mouse down handler
*/
onMouseDown = (e) => {
if (!this.isSelecting || !this.hasMapsV2Outlet) return
this.startPoint = [e.lngLat.lng, e.lngLat.lat]
this.mapsV2Outlet.map.dragPan.disable()
}
/**
* Mouse move handler
*/
onMouseMove = (e) => {
if (!this.isSelecting || !this.startPoint || !this.hasMapsV2Outlet) return
this.currentPoint = [e.lngLat.lng, e.lngLat.lat]
this.updateSelection()
}
/**
* Mouse up handler
*/
onMouseUp = (e) => {
if (!this.isSelecting || !this.startPoint || !this.hasMapsV2Outlet) return
this.currentPoint = [e.lngLat.lng, e.lngLat.lat]
this.mapsV2Outlet.map.dragPan.enable()
// Emit selection event
const bounds = this.getSelectionBounds()
this.dispatch('selected', { detail: { bounds } })
this.cancelSelection()
}
/**
* Update selection visualization
*/
updateSelection() {
if (!this.startPoint || !this.currentPoint || !this.hasMapsV2Outlet) return
const bounds = this.getSelectionBounds()
const rectangle = createRectangle(bounds)
const source = this.mapsV2Outlet.map.getSource('selection-source')
if (source) {
source.setData({
type: 'FeatureCollection',
features: [{
type: 'Feature',
geometry: {
type: 'Polygon',
coordinates: rectangle
}
}]
})
}
}
/**
* Get selection bounds
*/
getSelectionBounds() {
return {
minLng: Math.min(this.startPoint[0], this.currentPoint[0]),
minLat: Math.min(this.startPoint[1], this.currentPoint[1]),
maxLng: Math.max(this.startPoint[0], this.currentPoint[0]),
maxLat: Math.max(this.startPoint[1], this.currentPoint[1])
}
}
}