@rural-environmental-registry/map_component 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +444 -0
  2. package/dist/constants/coordinatePanel.d.ts +1 -1
  3. package/dist/handlers/drawingControl.d.ts +0 -1
  4. package/dist/handlers/mapHandler.d.ts +8 -3
  5. package/dist/handlers/mapToolsHandler.d.ts +39 -0
  6. package/dist/handlers/measureToolsHandler.d.ts +51 -0
  7. package/dist/handlers/memorialButtonControl.d.ts +19 -0
  8. package/dist/handlers/toolsConstants.d.ts +3 -0
  9. package/dist/index.css +1 -1
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.mjs +32914 -25131
  12. package/dist/types/index.d.ts +69 -0
  13. package/dist/utils/areaCalculator.d.ts +3 -0
  14. package/dist/utils/drawingPathOptions.d.ts +8 -0
  15. package/dist/utils/geometryCalculator.d.ts +13 -0
  16. package/dist/utils/geometryFileParseResult.d.ts +8 -0
  17. package/dist/utils/memorialLayer.d.ts +3 -0
  18. package/dist/utils/menuHistory.d.ts +8 -0
  19. package/dist/utils/normalizeGeoJsonToFeatures.d.ts +2 -0
  20. package/dist/utils/parseGeoJsonFile.d.ts +2 -0
  21. package/dist/utils/parseKmlFile.d.ts +2 -0
  22. package/dist/utils/parseShapefileZip.d.ts +3 -0
  23. package/dist/utils/stableMarker.d.ts +15 -0
  24. package/dist/utils/validateImportedPolygonGeometry.d.ts +9 -0
  25. package/package.json +22 -4
  26. package/src/assets/icons/file-import-regular-full-gray.svg +1 -0
  27. package/src/assets/icons/file-lines-regular-full-gray.svg +1 -0
  28. package/src/assets/icons/layer-icon.svg +3 -0
  29. package/src/assets/icons/loading.gif +0 -0
  30. package/src/assets/icons/marker-icon.svg +1 -0
  31. package/src/assets/icons/measure-icon.svg +5 -0
  32. package/src/assets/layers/mapLayers.json +16 -0
  33. package/src/assets/main.css +187 -0
  34. package/src/assets/styles/element-plus.css +25 -0
  35. package/src/assets/styles/layers-menu.css +95 -0
  36. package/src/assets/styles/leaflet.css +361 -0
  37. package/src/components/MapDPG.vue +302 -0
  38. package/src/components/coordinate/CoordinatePanel.vue +1206 -0
  39. package/src/components/fa-icon/FontAwesomeIcon.vue +44 -0
  40. package/src/components/loading/Loading.vue +25 -0
  41. package/src/components/map/LeafletMap.vue +183 -0
  42. package/src/components/menu/ChildMenu.vue +112 -0
  43. package/src/components/menu/LayerMenu.vue +294 -0
  44. package/src/components/menu/ParentMenu.vue +132 -0
  45. package/src/constants/coordinatePanel.ts +68 -0
  46. package/src/handlers/constants.ts +35 -0
  47. package/src/handlers/drawingControl.ts +143 -0
  48. package/src/handlers/mapHandler.ts +124 -0
  49. package/src/handlers/mapToolsHandler.ts +281 -0
  50. package/src/handlers/measureToolsHandler.ts +424 -0
  51. package/src/handlers/memorialButtonControl.ts +171 -0
  52. package/src/handlers/toolsConstants.ts +69 -0
  53. package/src/index.ts +6 -0
  54. package/src/shims-vue.d.ts +5 -0
  55. package/src/types/index.ts +260 -0
  56. package/src/types/json.d.ts +4 -0
  57. package/src/types/shpjs.d.ts +7 -0
  58. package/src/types/svg.d.ts +4 -0
  59. package/src/utils/CoordinateConverter.ts +43 -0
  60. package/src/utils/areaCalculator.ts +9 -0
  61. package/src/utils/drawingPathOptions.ts +57 -0
  62. package/src/utils/geometryCalculator.ts +62 -0
  63. package/src/utils/geometryFileParseResult.ts +5 -0
  64. package/src/utils/memorialLayer.ts +7 -0
  65. package/src/utils/menuHistory.ts +71 -0
  66. package/src/utils/normalizeGeoJsonToFeatures.ts +52 -0
  67. package/src/utils/parseGeoJsonFile.ts +23 -0
  68. package/src/utils/parseKmlFile.ts +31 -0
  69. package/src/utils/parseShapefileZip.ts +29 -0
  70. package/src/utils/stableMarker.ts +52 -0
  71. package/src/utils/validateImportedPolygonGeometry.ts +62 -0
@@ -0,0 +1,281 @@
1
+ import L, { FeatureGroup, LatLngBounds, Map } from 'leaflet'
2
+
3
+ import MeasureToolsHandler from './measureToolsHandler'
4
+ import { resolveMapToolsConfig } from './toolsConstants'
5
+ import { MapToolsConfig, MeasureCompleteEvent } from '../types'
6
+
7
+ type MapToolsCallbacks = {
8
+ onFullscreenChange?: (active: boolean) => void
9
+ onMeasureComplete?: (data: MeasureCompleteEvent) => void
10
+ }
11
+
12
+ type InitialView = {
13
+ center: L.LatLngExpression
14
+ zoom: number
15
+ }
16
+
17
+ const FULLSCREEN_ICON =
18
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>'
19
+
20
+ const EXIT_FULLSCREEN_ICON =
21
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 14h6v6M20 10h-6V4M14 10l7-7M3 21l7-7"/></svg>'
22
+
23
+ const CENTER_ICON =
24
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M12 2v4M12 18v4M2 12h4M18 12h4"/></svg>'
25
+
26
+ export default class MapToolsHandler {
27
+ private readonly _map: Map
28
+ private readonly _config: MapToolsConfig
29
+ private readonly _initialView: InitialView
30
+ private readonly _callbacks: MapToolsCallbacks
31
+ private readonly _fullscreenContainer: HTMLElement
32
+ private _drawItemsGroup: FeatureGroup | null
33
+ private _control: L.Control | null = null
34
+ private _measureTools: MeasureToolsHandler | null = null
35
+ private _fullscreenBtn: HTMLButtonElement | null = null
36
+ private _boundFullscreenChange: () => void
37
+
38
+ constructor(
39
+ map: Map,
40
+ toolsConfig: MapToolsConfig | undefined,
41
+ initialView: InitialView,
42
+ drawItemsGroup: FeatureGroup | null,
43
+ fullscreenContainer: HTMLElement,
44
+ callbacks: MapToolsCallbacks = {}
45
+ ) {
46
+ const resolved = resolveMapToolsConfig(toolsConfig)
47
+ if (!resolved) {
48
+ throw new Error('MapToolsHandler requires tools.show to be true')
49
+ }
50
+
51
+ this._map = map
52
+ this._config = resolved
53
+ this._initialView = initialView
54
+ this._drawItemsGroup = drawItemsGroup
55
+ this._callbacks = callbacks
56
+ this._fullscreenContainer = fullscreenContainer
57
+
58
+ this._boundFullscreenChange = () => this.handleFullscreenChange()
59
+
60
+ this.addControl()
61
+
62
+ if (this._config.measureArea?.show !== false) {
63
+ this._measureTools = new MeasureToolsHandler(this._map, this._config, {
64
+ onMeasureComplete: (data) => this._callbacks.onMeasureComplete?.(data)
65
+ })
66
+ this.alignTopRightControls()
67
+ }
68
+
69
+ this.bindFullscreenEvents()
70
+ }
71
+
72
+ public setDrawItemsGroup(group: FeatureGroup | null): void {
73
+ this._drawItemsGroup = group
74
+ }
75
+
76
+ public centerMap(): void {
77
+ const padding = this._config.center?.padding ?? [20, 20]
78
+ const target = this._config.center?.target ?? 'drawn'
79
+
80
+ if (target === 'drawn' && this._drawItemsGroup && this._drawItemsGroup.getLayers().length > 0) {
81
+ const bounds = this._drawItemsGroup.getBounds()
82
+ if (bounds.isValid()) {
83
+ this._map.fitBounds(bounds, { padding })
84
+ return
85
+ }
86
+ }
87
+
88
+ this._map.setView(this._initialView.center, this._initialView.zoom)
89
+ }
90
+
91
+ public enterFullscreen(): void {
92
+ if (!document.fullscreenElement) {
93
+ void this._fullscreenContainer.requestFullscreen?.()
94
+ }
95
+ }
96
+
97
+ public exitFullscreen(): void {
98
+ if (document.fullscreenElement) {
99
+ void document.exitFullscreen?.()
100
+ }
101
+ }
102
+
103
+ public toggleFullscreen(): void {
104
+ if (document.fullscreenElement) {
105
+ this.exitFullscreen()
106
+ } else {
107
+ this.enterFullscreen()
108
+ }
109
+ }
110
+
111
+ public toggleMeasureArea(): void {
112
+ if (this._measureTools?.isActive()) {
113
+ this._measureTools.cancel()
114
+ return
115
+ }
116
+
117
+ this._measureTools?.startLine()
118
+ }
119
+
120
+ public destroy(): void {
121
+ document.removeEventListener('fullscreenchange', this._boundFullscreenChange)
122
+
123
+ this._measureTools?.destroy()
124
+ this._measureTools = null
125
+
126
+ if (this._control) {
127
+ this._map.removeControl(this._control)
128
+ this._control = null
129
+ }
130
+ }
131
+
132
+ private addControl(): void {
133
+ const position = this._config.position ?? 'topright'
134
+ const config = this._config
135
+ const handler = this
136
+
137
+ const MapToolsControl = L.Control.extend({
138
+ options: { position },
139
+ onAdd() {
140
+ const container = L.DomUtil.create('div', 'leaflet-control leaflet-control-map-tools')
141
+
142
+ if (config.zoom?.show !== false) {
143
+ handler.createButton(
144
+ container,
145
+ 'map-tools-btn map-tools-btn--zoom-in',
146
+ config.zoom?.titleIn ?? 'Zoom in',
147
+ '+',
148
+ (e) => {
149
+ L.DomEvent.stop(e)
150
+ handler._map.zoomIn()
151
+ }
152
+ )
153
+
154
+ handler.createButton(
155
+ container,
156
+ 'map-tools-btn map-tools-btn--zoom-out',
157
+ config.zoom?.titleOut ?? 'Zoom out',
158
+ '−',
159
+ (e) => {
160
+ L.DomEvent.stop(e)
161
+ handler._map.zoomOut()
162
+ }
163
+ )
164
+ }
165
+
166
+ if (config.center?.show !== false) {
167
+ handler.createButton(
168
+ container,
169
+ 'map-tools-btn map-tools-btn--center',
170
+ config.center?.title ?? 'Center map',
171
+ CENTER_ICON,
172
+ (e) => {
173
+ L.DomEvent.stop(e)
174
+ handler.centerMap()
175
+ }
176
+ )
177
+ }
178
+
179
+ if (config.fullscreen?.show !== false) {
180
+ handler._fullscreenBtn = handler.createButton(
181
+ container,
182
+ 'map-tools-btn map-tools-btn--fullscreen',
183
+ config.fullscreen?.title ?? 'Fullscreen',
184
+ FULLSCREEN_ICON,
185
+ (e) => {
186
+ L.DomEvent.stop(e)
187
+ handler.toggleFullscreen()
188
+ }
189
+ )
190
+ }
191
+
192
+ L.DomEvent.disableClickPropagation(container)
193
+ L.DomEvent.disableScrollPropagation(container)
194
+
195
+ return container
196
+ }
197
+ })
198
+
199
+ this._control = new MapToolsControl()
200
+ this._map.addControl(this._control)
201
+ this.removeNativeZoomControl()
202
+ this.alignTopRightControls()
203
+ }
204
+
205
+ private removeNativeZoomControl(): void {
206
+ const zoomControl = this._map.getContainer().querySelector('.leaflet-control-zoom')
207
+ if (!zoomControl) return
208
+
209
+ const control = this._map.zoomControl
210
+ if (control) {
211
+ this._map.removeControl(control)
212
+ }
213
+ }
214
+
215
+ public alignTopRightControls(): void {
216
+ const corner = this._map.getContainer().querySelector('.leaflet-top.leaflet-right')
217
+ if (!corner) return
218
+
219
+ const prioritySelectors = [
220
+ '.leaflet-control-map-tools',
221
+ '.leaflet-control-layers',
222
+ '.leaflet-control-measure-tools',
223
+ '.leaflet-control-measure'
224
+ ]
225
+
226
+ const controls = Array.from(corner.querySelectorAll(':scope > .leaflet-control'))
227
+ const sorted = [
228
+ ...prioritySelectors.flatMap((selector) => controls.filter((control) => control.matches(selector))),
229
+ ...controls.filter((control) => !prioritySelectors.some((selector) => control.matches(selector)))
230
+ ]
231
+
232
+ sorted.forEach((control) => corner.appendChild(control))
233
+ }
234
+
235
+ private createButton(
236
+ container: HTMLElement,
237
+ className: string,
238
+ title: string,
239
+ iconHtml: string,
240
+ onClick: (e: Event) => void
241
+ ): HTMLButtonElement {
242
+ const btn = L.DomUtil.create('button', className, container) as HTMLButtonElement
243
+ btn.type = 'button'
244
+ btn.title = title
245
+ btn.innerHTML = iconHtml
246
+
247
+ L.DomEvent.on(btn, 'click', onClick)
248
+
249
+ return btn
250
+ }
251
+
252
+ private bindFullscreenEvents(): void {
253
+ document.addEventListener('fullscreenchange', this._boundFullscreenChange)
254
+ }
255
+
256
+ private handleFullscreenChange(): void {
257
+ const isActive = document.fullscreenElement === this._fullscreenContainer
258
+ this._fullscreenContainer.classList.toggle('map-container--fullscreen', isActive)
259
+
260
+ if (this._fullscreenBtn) {
261
+ this._fullscreenBtn.innerHTML = isActive ? EXIT_FULLSCREEN_ICON : FULLSCREEN_ICON
262
+ this._fullscreenBtn.title = isActive ? 'Exit fullscreen' : (this._config.fullscreen?.title ?? 'Fullscreen')
263
+ this._fullscreenBtn.classList.toggle('map-tools-btn--active', isActive)
264
+ }
265
+
266
+ setTimeout(() => {
267
+ this._map.invalidateSize()
268
+ }, 100)
269
+ this._callbacks.onFullscreenChange?.(isActive)
270
+ }
271
+ }
272
+
273
+ export function isMeasureModeActive(map: Map): boolean {
274
+ return !!(map as Map & { _leafletMeasureActive?: boolean })._leafletMeasureActive
275
+ }
276
+
277
+ export function getDrawnBounds(drawItemsGroup: FeatureGroup | null): LatLngBounds | null {
278
+ if (!drawItemsGroup || drawItemsGroup.getLayers().length === 0) return null
279
+ const bounds = drawItemsGroup.getBounds()
280
+ return bounds.isValid() ? bounds : null
281
+ }
@@ -0,0 +1,424 @@
1
+ import L, { FeatureGroup, Layer, Map, Path } from 'leaflet'
2
+ import '@geoman-io/leaflet-geoman-free'
3
+
4
+ import { MapToolsConfig, MeasureCompleteEvent, GeomanDrawingEvent } from '../types'
5
+ import {
6
+ calculateLayerArea,
7
+ calculateLayerLength,
8
+ formatAreaHa,
9
+ formatLengthKm
10
+ } from '../utils/geometryCalculator'
11
+
12
+ type MeasureToolsCallbacks = {
13
+ onMeasureComplete?: (data: MeasureCompleteEvent) => void
14
+ }
15
+
16
+ type MeasureMode = 'line' | 'polygon'
17
+
18
+ const LINE_ICON =
19
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 20L20 4"/><circle cx="6" cy="18" r="1.5" fill="currentColor" stroke="none"/><circle cx="18" cy="6" r="1.5" fill="currentColor" stroke="none"/></svg>'
20
+
21
+ const POLYGON_ICON =
22
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l8 5v10l-8 5-8-5V7z"/></svg>'
23
+
24
+ export default class MeasureToolsHandler {
25
+ private readonly _map: Map
26
+ private readonly _mapContainer: HTMLElement
27
+ private readonly _outerContainer: HTMLElement | null
28
+ private readonly _config: MapToolsConfig
29
+ private readonly _callbacks: MeasureToolsCallbacks
30
+ private readonly _measureGroup: FeatureGroup
31
+ private readonly _measureSvgRenderer: L.SVG
32
+ private readonly _boundMeasureCreate: (evt: GeomanDrawingEvent) => void
33
+ private readonly _boundMeasureCancel: () => void
34
+ private readonly _boundEscapeKey: (event: KeyboardEvent) => void
35
+ private _control: L.Control | null = null
36
+ private _lineBtn: HTMLButtonElement | null = null
37
+ private _polygonBtn: HTMLButtonElement | null = null
38
+ private _interactionPanel: HTMLElement | null = null
39
+ private _resultsEl: HTMLElement | null = null
40
+ private _helpEl: HTMLElement | null = null
41
+ private _activeMode: MeasureMode | null = null
42
+ private _suppressMeasureCancel = false
43
+ private _restoreZoomAnimation = true
44
+ private _measureCreateTimeoutId: number | null = null
45
+
46
+ constructor(map: Map, config: MapToolsConfig, callbacks: MeasureToolsCallbacks = {}) {
47
+ this._map = map
48
+ this._config = config
49
+ this._callbacks = callbacks
50
+ this._mapContainer = map.getContainer()
51
+ this._outerContainer = this._mapContainer.closest('.map-container')
52
+ this._measureGroup = new FeatureGroup()
53
+ this._measureGroup.addTo(map)
54
+ this._measureSvgRenderer = L.svg({ pane: 'overlayPane' })
55
+
56
+ this._boundMeasureCreate = (evt) => this.handleMeasureCreate(evt)
57
+ this._boundMeasureCancel = () => this.handleMeasureCancel()
58
+ this._boundEscapeKey = (event) => {
59
+ if (event.key === 'Escape' && this.isActive()) {
60
+ this.cancel()
61
+ }
62
+ }
63
+
64
+ this.addControl()
65
+ this._map.on('pm:create', this._boundMeasureCreate)
66
+ this._map.on('pm:drawcancel', this._boundMeasureCancel)
67
+ }
68
+
69
+ public destroy(): void {
70
+ this.clearMeasureCreateTimeout()
71
+ this.stopMode()
72
+ document.removeEventListener('keydown', this._boundEscapeKey)
73
+ this._map.off('pm:create', this._boundMeasureCreate)
74
+ this._map.off('pm:drawcancel', this._boundMeasureCancel)
75
+ this._measureGroup.clearLayers()
76
+ this._map.removeLayer(this._measureGroup)
77
+
78
+ if (this._control) {
79
+ this._map.removeControl(this._control)
80
+ this._control = null
81
+ }
82
+ }
83
+
84
+ public cancel(): void {
85
+ if (!this.isActive()) return
86
+
87
+ this._suppressMeasureCancel = true
88
+ this._map.pm.disableDraw()
89
+ this._suppressMeasureCancel = false
90
+ this.stopMode()
91
+ this._measureGroup.clearLayers()
92
+ }
93
+
94
+ public isActive(): boolean {
95
+ return this._activeMode !== null
96
+ }
97
+
98
+ public startLine(): void {
99
+ this.startMode('line')
100
+ }
101
+
102
+ public startPolygon(): void {
103
+ this.startMode('polygon')
104
+ }
105
+
106
+ private addControl(): void {
107
+ const position = this._config.position ?? 'topright'
108
+ const config = this._config
109
+ const handler = this
110
+
111
+ const MeasureToolsControl = L.Control.extend({
112
+ options: { position },
113
+ onAdd() {
114
+ const container = L.DomUtil.create('div', 'leaflet-control leaflet-control-measure-tools')
115
+
116
+ const buttons = L.DomUtil.create('div', 'measure-tools-buttons', container)
117
+
118
+ if (config.measureLine?.show !== false) {
119
+ handler._lineBtn = handler.createToolButton(
120
+ buttons,
121
+ 'measure-tools-btn measure-tools-btn--line',
122
+ config.measureLine?.title ?? config.texts?.measureLineTitle ?? 'Measure line',
123
+ LINE_ICON,
124
+ () => handler.toggleMode('line')
125
+ )
126
+ }
127
+
128
+ if (config.measurePolygon?.show !== false) {
129
+ handler._polygonBtn = handler.createToolButton(
130
+ buttons,
131
+ 'measure-tools-btn measure-tools-btn--polygon',
132
+ config.measurePolygon?.title ?? config.texts?.measurePolygonTitle ?? 'Measure polygon',
133
+ POLYGON_ICON,
134
+ () => handler.toggleMode('polygon')
135
+ )
136
+ }
137
+
138
+ handler._interactionPanel = L.DomUtil.create('div', 'measure-tools-interaction js-measure-interaction', container)
139
+ handler._interactionPanel.style.display = 'none'
140
+ handler._interactionPanel.innerHTML = handler.buildInteractionHtml(config)
141
+
142
+ handler._resultsEl = handler._interactionPanel.querySelector('.js-results')
143
+ handler._helpEl = handler._interactionPanel.querySelector('.js-measure-help')
144
+
145
+ const cancelBtn = handler._interactionPanel.querySelector('.js-cancel')
146
+ const finishBtn = handler._interactionPanel.querySelector('.js-finish')
147
+
148
+ if (cancelBtn instanceof HTMLElement) {
149
+ L.DomEvent.on(cancelBtn, 'click', (e) => {
150
+ L.DomEvent.stop(e)
151
+ handler.cancel()
152
+ })
153
+ }
154
+
155
+ if (finishBtn instanceof HTMLElement) {
156
+ L.DomEvent.on(finishBtn, 'click', (e) => {
157
+ L.DomEvent.stop(e)
158
+ handler.finishDrawing()
159
+ })
160
+ }
161
+
162
+ L.DomEvent.disableClickPropagation(container)
163
+ L.DomEvent.disableScrollPropagation(container)
164
+
165
+ return container
166
+ }
167
+ })
168
+
169
+ this._control = new MeasureToolsControl()
170
+ this._control.addTo(this._map)
171
+ }
172
+
173
+ private buildInteractionHtml(config: MapToolsConfig): string {
174
+ const title = config.texts?.measurePanelTitle ?? 'Measure distances and areas'
175
+ const cancel = config.texts?.measureCancel ?? 'Cancel'
176
+ const finish = config.texts?.measureFinish ?? 'Finish measurement'
177
+
178
+ return `
179
+ <div class="js-measuringprompt">
180
+ <h3>${title}</h3>
181
+ <p class="js-measure-help"></p>
182
+ <div class="js-results results"></div>
183
+ <ul class="js-measuretasks tasks">
184
+ <li><a href="#" class="js-cancel cancel">${cancel}</a></li>
185
+ <li><a href="#" class="js-finish finish">${finish}</a></li>
186
+ </ul>
187
+ </div>
188
+ `
189
+ }
190
+
191
+ private createToolButton(
192
+ container: HTMLElement,
193
+ className: string,
194
+ title: string,
195
+ iconHtml: string,
196
+ onClick: () => void
197
+ ): HTMLButtonElement {
198
+ const btn = L.DomUtil.create('button', className, container) as HTMLButtonElement
199
+ btn.type = 'button'
200
+ btn.title = title
201
+ btn.setAttribute('aria-label', title)
202
+ btn.innerHTML = iconHtml
203
+ L.DomEvent.on(btn, 'click', (e) => {
204
+ L.DomEvent.stop(e)
205
+ onClick()
206
+ })
207
+ return btn
208
+ }
209
+
210
+ private toggleMode(mode: MeasureMode): void {
211
+ if (this._activeMode === mode) {
212
+ this.cancel()
213
+ return
214
+ }
215
+
216
+ this.startMode(mode)
217
+ }
218
+
219
+ private startMode(mode: MeasureMode): void {
220
+ if (this.isActive()) {
221
+ this._suppressMeasureCancel = true
222
+ this._map.pm.disableDraw()
223
+ this._suppressMeasureCancel = false
224
+ }
225
+
226
+ this._measureGroup.clearLayers()
227
+ this._activeMode = mode
228
+ this.setMeasureActive(true)
229
+ this.updateButtons()
230
+ this.showInteractionPanel(mode)
231
+
232
+ if (this._map.doubleClickZoom.enabled()) {
233
+ this._map.doubleClickZoom.disable()
234
+ }
235
+
236
+ this._restoreZoomAnimation = this._map.options.zoomAnimation !== false
237
+ this._map.options.zoomAnimation = false
238
+
239
+ const shapeOptions = this._config.measureArea?.shapeOptions ?? {
240
+ color: '#e67e22',
241
+ fillColor: '#e67e22',
242
+ fillOpacity: 0.2,
243
+ weight: 2
244
+ }
245
+
246
+ const drawOptions = {
247
+ snappable: true,
248
+ templineStyle: shapeOptions,
249
+ hintlineStyle: shapeOptions,
250
+ pathOptions: shapeOptions
251
+ }
252
+
253
+ if (mode === 'line') {
254
+ this._map.pm.enableDraw('Line', {
255
+ ...drawOptions,
256
+ finishOn: 'dblclick'
257
+ } as unknown as L.PM.DrawModeOptions)
258
+ } else {
259
+ this._map.pm.enableDraw('Polygon', {
260
+ ...drawOptions,
261
+ finishOn: null,
262
+ allowSelfIntersection: true
263
+ } as unknown as L.PM.DrawModeOptions)
264
+ }
265
+ }
266
+
267
+ private stopMode(): void {
268
+ this._activeMode = null
269
+ this.setMeasureActive(false)
270
+ this._map.pm.disableDraw()
271
+ this._map.options.zoomAnimation = this._restoreZoomAnimation
272
+ this.updateButtons()
273
+ this.hideInteractionPanel()
274
+
275
+ if (!this._map.doubleClickZoom.enabled()) {
276
+ this._map.doubleClickZoom.enable()
277
+ }
278
+ }
279
+
280
+ private finishDrawing(): void {
281
+ const draw = (this._map as Map & { pm?: { Draw?: { finishDraw?: () => void } } }).pm?.Draw
282
+ draw?.finishDraw?.()
283
+ }
284
+
285
+ private handleMeasureCreate(evt: GeomanDrawingEvent): void {
286
+ if (!this.isActive()) return
287
+
288
+ const shape = String(evt.shape).toLowerCase()
289
+ const isLine = shape === 'line' || shape === 'polyline'
290
+ const isPolygon = shape === 'polygon' || shape === 'rectangle'
291
+
292
+ if (this._activeMode === 'line' && !isLine) return
293
+ if (this._activeMode === 'polygon' && !isPolygon) return
294
+
295
+ const { layer } = evt
296
+ const mode = this._activeMode
297
+
298
+ this.stopMode()
299
+
300
+ this.clearMeasureCreateTimeout()
301
+ this._measureCreateTimeoutId = window.setTimeout(() => {
302
+ this._measureCreateTimeoutId = null
303
+
304
+ if (!this._map?.hasLayer(this._measureGroup)) return
305
+
306
+ this.applyMeasureLayerOptions(layer)
307
+
308
+ if (this._map.hasLayer(layer)) {
309
+ this._map.removeLayer(layer)
310
+ }
311
+
312
+ this._measureGroup.clearLayers()
313
+ this._measureGroup.addLayer(layer)
314
+
315
+ const { html, event } = this.buildMeasurePopup(layer, mode!)
316
+ layer.bindPopup(html, { closeOnClick: false, autoClose: false, autoPan: false }).openPopup()
317
+
318
+ layer.on('popupclose', () => {
319
+ this._measureGroup.removeLayer(layer)
320
+ })
321
+
322
+ this._callbacks.onMeasureComplete?.(event)
323
+ }, 0)
324
+ }
325
+
326
+ private clearMeasureCreateTimeout(): void {
327
+ if (this._measureCreateTimeoutId !== null) {
328
+ window.clearTimeout(this._measureCreateTimeoutId)
329
+ this._measureCreateTimeoutId = null
330
+ }
331
+ }
332
+
333
+ private handleMeasureCancel(): void {
334
+ if (this._suppressMeasureCancel || !this.isActive()) return
335
+ this.stopMode()
336
+ this._measureGroup.clearLayers()
337
+ }
338
+
339
+ private buildMeasurePopup(
340
+ layer: Layer,
341
+ mode: MeasureMode
342
+ ): { html: string; event: MeasureCompleteEvent } {
343
+ const label = this._config.texts?.measureResult ?? 'Measurement'
344
+
345
+ if (mode === 'polygon') {
346
+ const areas = calculateLayerArea(layer)
347
+ const areaLine = `${this._config.texts?.measureArea ?? 'Area'}: ${formatAreaHa(areas.ha)}`
348
+ return {
349
+ html: `<div class="leaflet-measure-resultpopup"><h3>${label}</h3><p>${areaLine}</p></div>`,
350
+ event: {
351
+ m2: areas.m2,
352
+ ha: areas.ha,
353
+ geojson: (layer as L.Polygon).toGeoJSON() as MeasureCompleteEvent['geojson']
354
+ }
355
+ }
356
+ }
357
+
358
+ const lengths = calculateLayerLength(layer)
359
+ const distanceLine = `${this._config.texts?.measureLength ?? 'Distance'}: ${formatLengthKm(lengths.km)}`
360
+ return {
361
+ html: `<div class="leaflet-measure-resultpopup"><h3>${label}</h3><p>${distanceLine}</p></div>`,
362
+ event: {
363
+ lengthM: lengths.m,
364
+ lengthKm: lengths.km,
365
+ geojson: (layer as L.Polyline).toGeoJSON() as MeasureCompleteEvent['geojson']
366
+ }
367
+ }
368
+ }
369
+
370
+ private applyMeasureLayerOptions(layer: Layer): void {
371
+ if (layer instanceof Path) {
372
+ layer.options.renderer = this._measureSvgRenderer
373
+ if (typeof layer.redraw === 'function') {
374
+ layer.redraw()
375
+ }
376
+ }
377
+ }
378
+
379
+ private showInteractionPanel(mode: MeasureMode): void {
380
+ if (!this._interactionPanel || !this._helpEl) return
381
+
382
+ this._interactionPanel.style.removeProperty('display')
383
+ this._interactionPanel.closest('.leaflet-control-measure-tools')?.classList.add('measure-tools-expanded')
384
+
385
+ if (mode === 'line') {
386
+ this._helpEl.textContent =
387
+ this._config.texts?.measureLineHelp ??
388
+ 'Click two points on the map. Double-click to finish the line.'
389
+ } else {
390
+ this._helpEl.textContent =
391
+ this._config.texts?.measurePolygonHelp ??
392
+ 'Click to add vertices. Finish on the first point, use Finish, or double-click.'
393
+ }
394
+
395
+ if (this._resultsEl) {
396
+ this._resultsEl.innerHTML = ''
397
+ }
398
+
399
+ document.addEventListener('keydown', this._boundEscapeKey)
400
+ }
401
+
402
+ private hideInteractionPanel(): void {
403
+ if (!this._interactionPanel) return
404
+
405
+ this._interactionPanel.style.display = 'none'
406
+ this._interactionPanel.closest('.leaflet-control-measure-tools')?.classList.remove('measure-tools-expanded')
407
+ document.removeEventListener('keydown', this._boundEscapeKey)
408
+ }
409
+
410
+ private updateButtons(): void {
411
+ const active = this._activeMode
412
+ this._lineBtn?.classList.toggle('measure-tools-btn--active', active === 'line')
413
+ this._polygonBtn?.classList.toggle('measure-tools-btn--active', active === 'polygon')
414
+ }
415
+
416
+ private setMeasureActive(active: boolean): void {
417
+ ;(this._map as Map & { _leafletMeasureActive?: boolean })._leafletMeasureActive = active
418
+ this._mapContainer.classList.toggle('map-container--measuring', active)
419
+
420
+ if (this._outerContainer instanceof HTMLElement) {
421
+ this._outerContainer.classList.toggle('map-container--measuring', active)
422
+ }
423
+ }
424
+ }