@any-routing/leaflet-engine 1.0.0-rc.1

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.
@@ -0,0 +1,992 @@
1
+ import {
2
+ GeoJSON,
3
+ LatLng,
4
+ LatLngBounds,
5
+ LeafletMouseEvent,
6
+ Map,
7
+ Marker,
8
+ geoJSON,
9
+ divIcon,
10
+ Layer,
11
+ Path,
12
+ } from 'leaflet';
13
+
14
+ import {
15
+ type AnyRouting,
16
+ AnyRoutingDataResponse,
17
+ type AnyRoutingProjector,
18
+ Dispatcher,
19
+ type InternalWaypoint,
20
+ InternalWaypointC,
21
+ type RoutingEvents,
22
+ } from '@any-routing/core';
23
+
24
+ import { featureCollection } from '@turf/helpers';
25
+ import bbox from '@turf/bbox';
26
+
27
+ import { debounce } from './utils/debounce.util';
28
+ import * as Leaflet from 'leaflet';
29
+
30
+ import type {
31
+ LeafletProjectorEventMap,
32
+ LeafletProjectorOptions,
33
+ LeafletRouteFeature,
34
+ LeafletRouteStyle,
35
+ MarkerFactoryContext,
36
+ RouteFeatureProperties,
37
+ } from './projector.leaflet.plugin.types';
38
+
39
+ const isEqual = <T>(a: T, b: T): boolean => {
40
+ if (a === b) return true;
41
+
42
+ const bothAreObjects =
43
+ a &&
44
+ b &&
45
+ typeof a === 'object' &&
46
+ typeof b === 'object' &&
47
+ Array.isArray(a) === Array.isArray(b);
48
+
49
+ return Boolean(
50
+ bothAreObjects &&
51
+ Object.keys(a).length === Object.keys(b).length &&
52
+ Object.entries(a).every(([key, value]) => isEqual(value, b[key as keyof T])),
53
+ );
54
+ };
55
+
56
+ const DEFAULT_OPTIONS = {
57
+ maxWaypoints: Infinity,
58
+ canAddWaypoints: true,
59
+ canDragWaypoints: true,
60
+ canSelectRoute: true,
61
+ hoverEnabled: true,
62
+ routesWhileDragging: true,
63
+ waypointDragCommitDebounceTime: 150,
64
+
65
+ routeStyle: {
66
+ color: '#33C9EB',
67
+ weight: 5,
68
+ opacity: 1,
69
+ lineCap: 'round',
70
+ lineJoin: 'round',
71
+ },
72
+
73
+ selectedRouteStyle: {
74
+ color: '#e207ff',
75
+ weight: 5,
76
+ opacity: 1,
77
+ lineCap: 'round',
78
+ lineJoin: 'round',
79
+ },
80
+
81
+ routeOutlineStyle: {
82
+ color: '#ffffff',
83
+ weight: 9,
84
+ opacity: 0.95,
85
+ lineCap: 'round',
86
+ lineJoin: 'round',
87
+ },
88
+
89
+ routeZIndex: 1,
90
+ selectedRouteZIndex: 10,
91
+ } as const;
92
+
93
+ const DRAG_COMMIT_DEBOUNCE_WAIT_MS = 50;
94
+
95
+ interface LatLngPosition {
96
+ lat: number;
97
+ lng: number;
98
+ }
99
+
100
+ export class LeafletProjector implements AnyRoutingProjector {
101
+ private routing!: AnyRouting;
102
+
103
+ private readonly map: Map;
104
+
105
+ private readonly options: LeafletProjectorOptions & {
106
+ maxWaypoints: number;
107
+ canAddWaypoints: boolean;
108
+ canDragWaypoints: boolean;
109
+ canSelectRoute: boolean;
110
+ routesWhileDragging: boolean;
111
+ waypointDragCommitDebounceTime: number;
112
+ routeStyle: LeafletRouteStyle;
113
+ selectedRouteStyle: LeafletRouteStyle;
114
+ routeOutlineStyle: LeafletRouteStyle;
115
+ routeZIndex: number;
116
+ selectedRouteZIndex: number;
117
+ };
118
+
119
+ private readonly dispatcher: Dispatcher<LeafletProjectorEventMap> = new Dispatcher();
120
+
121
+ private addWaypointMarker: Marker | undefined;
122
+ private addWaypointMarkerAdded = false;
123
+
124
+ private _waypointsMarkers: Marker[] = [];
125
+
126
+ private _canAddWaypoints = true;
127
+
128
+ private _canDragWaypoints = true;
129
+
130
+ private _canSelectRoute = true;
131
+
132
+ private _hoverEnabled = true;
133
+
134
+ private _maxWaypoints = Infinity;
135
+
136
+ private routesLayer?: GeoJSON;
137
+ private routeOutlineLayer?: GeoJSON;
138
+ private hoveredRouteLayer?: Path;
139
+ private readonly routeFeatures = new WeakMap<Path, LeafletRouteFeature>();
140
+ private activeDragCleanup?: () => void;
141
+ private recalculationId = 0;
142
+ private previewRequestId = 0;
143
+ private previewLoading = false;
144
+ private lastPreviewData?: { waypoints: InternalWaypoint[]; data: AnyRoutingDataResponse };
145
+ private _waypoints: InternalWaypoint[] = [];
146
+
147
+ public get waypoints(): InternalWaypoint[] {
148
+ return this._waypoints;
149
+ }
150
+
151
+ private readonly calculationStartedHandler = (
152
+ _event: RoutingEvents<AnyRoutingDataResponse>['calculationStarted'],
153
+ ): void => {
154
+ this.clearRoutes();
155
+ };
156
+
157
+ private readonly stateUpdatedHandler = (
158
+ event: RoutingEvents<AnyRoutingDataResponse>['stateUpdated'],
159
+ ): void => {
160
+ if (event.updatedProperties.includes('routesShapeGeojson')) {
161
+ if (event.state.routesShapeGeojson) {
162
+ this.projectRoute(event.state.routesShapeGeojson);
163
+ } else {
164
+ this.clearRoutes();
165
+ }
166
+ }
167
+
168
+ if (event.updatedProperties.includes('waypoints')) {
169
+ this.projectWaypoints(event.state.waypoints);
170
+ }
171
+
172
+ if (event.updatedProperties.includes('selectedRouteId')) {
173
+ this.bringSelectedRouteToFront();
174
+ }
175
+ };
176
+
177
+ private readonly dragCommitHandler: ((
178
+ newWaypoints: InternalWaypoint[],
179
+ index: number,
180
+ ) => void) & {
181
+ cancel: () => void;
182
+ };
183
+
184
+ public get waypointsMarkers(): Marker[] {
185
+ return this._waypointsMarkers;
186
+ }
187
+
188
+ public get isEditable(): boolean {
189
+ return this._canAddWaypoints || this._canDragWaypoints;
190
+ }
191
+
192
+ constructor(options: LeafletProjectorOptions) {
193
+ this.map = options.map;
194
+
195
+ this.options = {
196
+ ...DEFAULT_OPTIONS,
197
+ ...options,
198
+ routeStyle: {
199
+ ...DEFAULT_OPTIONS.routeStyle,
200
+ ...options.routeStyle,
201
+ },
202
+ selectedRouteStyle: {
203
+ ...DEFAULT_OPTIONS.selectedRouteStyle,
204
+ ...options.selectedRouteStyle,
205
+ },
206
+ routeOutlineStyle: {
207
+ ...DEFAULT_OPTIONS.routeOutlineStyle,
208
+ ...options.routeOutlineStyle,
209
+ },
210
+ };
211
+
212
+ this._maxWaypoints = this.options.maxWaypoints ?? Infinity;
213
+
214
+ this.dragCommitHandler = debounce(
215
+ (newWaypoints: InternalWaypoint[], index: number) => {
216
+ this.dispatcher.fire('waypointDragCommit', {
217
+ waypoint: newWaypoints[index],
218
+ });
219
+
220
+ void this.previewRoute(newWaypoints);
221
+ },
222
+ DRAG_COMMIT_DEBOUNCE_WAIT_MS,
223
+ {
224
+ maxWait: this.options.waypointDragCommitDebounceTime,
225
+ },
226
+ );
227
+ }
228
+
229
+ // ---------------------------------------------------------------------
230
+ // Lifecycle
231
+ // ---------------------------------------------------------------------
232
+
233
+ public onAdd(anyRouting: AnyRouting): void {
234
+ this.routing = anyRouting;
235
+
236
+ this.setCanAddWaypoints(this.options.editable ?? !!this.options.canAddWaypoints);
237
+
238
+ this.setCanSelectRoute(this.options.editable ?? !!this.options.canSelectRoute);
239
+
240
+ this.setCanDragWaypoint(this.options.editable ?? !!this.options.canDragWaypoints);
241
+ this.setHoverEnabled(this.options.hoverEnabled ?? true);
242
+
243
+ this.routing.on('calculationStarted', this.calculationStartedHandler);
244
+ this.routing.on('stateUpdated', this.stateUpdatedHandler);
245
+ }
246
+
247
+ public onRemove(): void {
248
+ this.destroy();
249
+ }
250
+
251
+ public destroy(): void {
252
+ this.recalculationId += 1;
253
+ this.dragCommitHandler.cancel();
254
+ this.activeDragCleanup?.();
255
+ this.activeDragCleanup = undefined;
256
+ this.routing?.off('calculationStarted', this.calculationStartedHandler);
257
+ this.routing?.off('stateUpdated', this.stateUpdatedHandler);
258
+
259
+ this.destroyRoutes();
260
+
261
+ this._waypointsMarkers.forEach((marker) => {
262
+ marker.remove();
263
+ });
264
+
265
+ this._waypointsMarkers = [];
266
+ this._waypoints = [];
267
+
268
+ this.addWaypointMarker?.remove();
269
+ this.addWaypointMarker = undefined;
270
+ this.addWaypointMarkerAdded = false;
271
+ }
272
+
273
+ // ---------------------------------------------------------------------
274
+ // Rendering
275
+ // ---------------------------------------------------------------------
276
+
277
+ public projectRoute(routesShapeGeojson: AnyRoutingDataResponse['routesShapeGeojson']): void {
278
+ this.destroyRoutes();
279
+ this.hoveredRouteLayer = undefined;
280
+
281
+ this.routeOutlineLayer = geoJSON(routesShapeGeojson, {
282
+ style: () => this.options.routeOutlineStyle,
283
+ }).addTo(this.map);
284
+
285
+ this.routesLayer = geoJSON(routesShapeGeojson, {
286
+ style: (feature) => {
287
+ return this.getRouteStyle(feature as LeafletRouteFeature);
288
+ },
289
+
290
+ onEachFeature: (feature, layer) => {
291
+ this.bindRouteFeature(feature as LeafletRouteFeature, layer);
292
+ },
293
+ });
294
+
295
+ this.routesLayer.addTo(this.map);
296
+ this.bringSelectedRouteToFront();
297
+
298
+ this.dispatcher.fire('routesProjected', {
299
+ routesShapeGeojson,
300
+ });
301
+ this.dispatcher.fire('viewStateChanged', {
302
+ state: this.routing.state,
303
+ reason: 'route',
304
+ });
305
+ }
306
+
307
+ public clearRoutes(): void {
308
+ this.destroyRoutes();
309
+ }
310
+
311
+ public projectWaypoints(waypoints: InternalWaypoint[]): void {
312
+ this._waypoints = waypoints;
313
+ this._waypointsMarkers.forEach((marker) => {
314
+ marker.remove();
315
+ });
316
+
317
+ this._waypointsMarkers = waypoints.map((waypoint, index) => {
318
+ const marker = this.options
319
+ .markerFactory({ waypoint })
320
+ .setLatLng([waypoint.position.lat, waypoint.position.lng])
321
+ .addTo(this.map);
322
+
323
+ this.configureWaypointDrag(marker, index);
324
+
325
+ return marker;
326
+ });
327
+
328
+ this.dispatcher.fire('waypointsProjected', {
329
+ waypoints,
330
+ });
331
+ this.dispatcher.fire('viewStateChanged', {
332
+ state: { ...this.routing.state, waypoints },
333
+ reason: 'waypoints',
334
+ });
335
+ }
336
+
337
+ // ---------------------------------------------------------------------
338
+ // Route rendering
339
+ // ---------------------------------------------------------------------
340
+
341
+ private getRouteStyle(feature: LeafletRouteFeature): LeafletRouteStyle {
342
+ const routeId = feature.properties?.routeId;
343
+ const selected = routeId === this.routing.state.selectedRouteId;
344
+
345
+ return {
346
+ ...(selected ? this.options.selectedRouteStyle : this.options.routeStyle),
347
+ };
348
+ }
349
+
350
+ private bindRouteFeature(feature: LeafletRouteFeature, layer: Layer): void {
351
+ if (layer instanceof Path) {
352
+ this.routeFeatures.set(layer, feature);
353
+ }
354
+
355
+ layer.on({
356
+ click: (event: LeafletMouseEvent) => {
357
+ this.onRouteClick(event, feature, layer);
358
+ },
359
+
360
+ mouseover: (event: LeafletMouseEvent) => {
361
+ this.onRouteHover(event, feature, layer);
362
+ },
363
+
364
+ mouseout: () => {
365
+ this.onRouteHoverOut();
366
+ },
367
+
368
+ mousedown: (event: LeafletMouseEvent) => {
369
+ this.onRouteMouseDown(event, feature, layer);
370
+ },
371
+
372
+ mousemove: (event: LeafletMouseEvent) => {
373
+ this.onRouteMove(event, feature);
374
+ },
375
+ });
376
+ }
377
+
378
+ // ---------------------------------------------------------------------
379
+ // Waypoint markers
380
+ // ---------------------------------------------------------------------
381
+
382
+ private configureWaypointDrag(marker: Marker, index: number): void {
383
+ marker.options.draggable = this._canDragWaypoints;
384
+
385
+ if (!this._canDragWaypoints) {
386
+ return;
387
+ }
388
+
389
+ const dragHandler = (): void => {
390
+ const position = marker.getLatLng();
391
+
392
+ const newWaypoints = this.withUpdatedPosition(this.waypoints, index, position);
393
+
394
+ this.dispatcher.fire('waypointDrag', {
395
+ waypoint: newWaypoints[index],
396
+ });
397
+ this.dispatcher.fire('viewStateChanged', {
398
+ state: { ...this.routing.state, waypoints: newWaypoints },
399
+ reason: 'interaction',
400
+ });
401
+ this._waypoints = newWaypoints;
402
+
403
+ if (this.options.routesWhileDragging) {
404
+ this.dragCommitHandler(newWaypoints, index);
405
+ }
406
+ };
407
+
408
+ marker.on('drag', dragHandler);
409
+
410
+ marker.on('dragend', () => {
411
+ this.dragCommitHandler.cancel();
412
+
413
+ marker.off('drag', dragHandler);
414
+
415
+ const position = marker.getLatLng();
416
+
417
+ const newWaypoints = this.withUpdatedPosition(this.routing.state.waypoints, index, position);
418
+
419
+ this.dispatcher.fire('waypointDragEnd', {
420
+ waypoint: newWaypoints[index],
421
+ });
422
+ this.dispatcher.fire('viewStateChanged', {
423
+ state: { ...this.routing.state, waypoints: newWaypoints },
424
+ reason: 'interaction',
425
+ });
426
+
427
+ if (
428
+ this.lastPreviewData &&
429
+ isEqual(this.lastPreviewData.waypoints, newWaypoints) &&
430
+ this.options.previewDataProvider === this.routing.dataProvider
431
+ ) {
432
+ this.routing.setWaypoints(newWaypoints);
433
+ this.routing.applyCalculationResult(this.lastPreviewData.data);
434
+ } else {
435
+ this.setAndRecalculate(newWaypoints);
436
+ }
437
+ });
438
+ }
439
+
440
+ // ---------------------------------------------------------------------
441
+ // Route interaction
442
+ // ---------------------------------------------------------------------
443
+
444
+ private onRouteClick(
445
+ event: LeafletMouseEvent,
446
+ feature: LeafletRouteFeature,
447
+ _layer: Layer,
448
+ ): void {
449
+ const props = feature.properties;
450
+
451
+ if (
452
+ props.routeId != null &&
453
+ props.routeId !== this.routing.state.selectedRouteId &&
454
+ this._canSelectRoute &&
455
+ !this.eventIsCancelled(event)
456
+ ) {
457
+ this.stopEventPropagation(event);
458
+
459
+ this.routing.selectRoute(props.routeId);
460
+
461
+ this.dispatcher.fire('routeClick', {
462
+ routeId: props.routeId,
463
+ });
464
+ this.dispatcher.fire('viewStateChanged', {
465
+ state: this.routing.state,
466
+ reason: 'interaction',
467
+ });
468
+ }
469
+ }
470
+
471
+ private onRouteHover(
472
+ event: LeafletMouseEvent,
473
+ feature: LeafletRouteFeature,
474
+ layer: Layer,
475
+ ): void {
476
+ if (!this._hoverEnabled) return;
477
+ this.map.getContainer().style.cursor = 'pointer';
478
+ const path = layer instanceof Path ? layer : undefined;
479
+ if (path) {
480
+ const previousFeature = (
481
+ this.hoveredRouteLayer as (Layer & { feature?: LeafletRouteFeature }) | undefined
482
+ )?.feature;
483
+ if (previousFeature) {
484
+ this.hoveredRouteLayer?.setStyle(this.getRouteStyle(previousFeature));
485
+ }
486
+ this.hoveredRouteLayer = path;
487
+ const style = this.getRouteStyle(feature);
488
+ path.setStyle({
489
+ ...style,
490
+ weight: (style.weight ?? 5) + 2,
491
+ opacity: 1,
492
+ });
493
+ path.bringToFront();
494
+ }
495
+
496
+ const props = feature.properties;
497
+
498
+ if (
499
+ props.routeId != null &&
500
+ props.routeId === this.routing.state.selectedRouteId &&
501
+ this._canAddWaypoints &&
502
+ this.waypoints.length < this._maxWaypoints &&
503
+ !this.eventIsCancelled(event)
504
+ ) {
505
+ this.stopEventPropagation(event);
506
+
507
+ this.addWaypointMarker?.remove();
508
+ this.addWaypointMarkerAdded = false;
509
+
510
+ this.addWaypointMarker = this.options.markerFactory({
511
+ routeHover: feature,
512
+ });
513
+
514
+ this.showAddWaypointMarker(event.latlng);
515
+
516
+ this.addWaypointMarker.getElement()?.style.setProperty('pointer-events', 'none');
517
+ }
518
+ }
519
+
520
+ private onRouteHoverOut(): void {
521
+ this.map.getContainer().style.cursor = '';
522
+ if (this.hoveredRouteLayer) {
523
+ const feature = (this.hoveredRouteLayer as Layer & { feature?: LeafletRouteFeature }).feature;
524
+ if (feature) {
525
+ this.hoveredRouteLayer.setStyle(this.getRouteStyle(feature));
526
+ }
527
+ this.hoveredRouteLayer = undefined;
528
+ }
529
+ this.bringSelectedRouteToFront();
530
+
531
+ this.addWaypointMarker?.remove();
532
+ this.addWaypointMarkerAdded = false;
533
+ }
534
+
535
+ private onRouteMove(event: LeafletMouseEvent, feature: LeafletRouteFeature): void {
536
+ if (!this._hoverEnabled) return;
537
+ if (
538
+ !this._canAddWaypoints ||
539
+ this.waypoints.length >= this._maxWaypoints ||
540
+ this.eventIsCancelled(event)
541
+ ) {
542
+ return;
543
+ }
544
+
545
+ const props = feature.properties;
546
+
547
+ if (props?.routeId === this.routing.state.selectedRouteId) {
548
+ this.stopEventPropagation(event);
549
+
550
+ if (!this.addWaypointMarker) {
551
+ this.addWaypointMarker = this.options.markerFactory({
552
+ routeHover: feature,
553
+ });
554
+ }
555
+
556
+ this.showAddWaypointMarker(event.latlng);
557
+
558
+ this.addWaypointMarker.getElement()?.style.setProperty('pointer-events', 'none');
559
+ }
560
+ }
561
+
562
+ private onRouteMouseDown(
563
+ event: LeafletMouseEvent,
564
+ feature: LeafletRouteFeature,
565
+ layer: Layer,
566
+ ): void {
567
+ const props = feature.properties;
568
+
569
+ if (
570
+ !props ||
571
+ props.routeId !== this.routing.state.selectedRouteId ||
572
+ !this._canAddWaypoints ||
573
+ this.waypoints.length >= this._maxWaypoints ||
574
+ this.eventIsCancelled(event)
575
+ ) {
576
+ return;
577
+ }
578
+
579
+ const target = event.originalEvent.target as HTMLElement;
580
+
581
+ if (target.closest('.marker') || target.closest('.leaflet-marker-icon')) {
582
+ return;
583
+ }
584
+
585
+ event.originalEvent.preventDefault();
586
+
587
+ Leaflet.DomEvent.stopPropagation(event.originalEvent);
588
+ Leaflet.DomEvent.preventDefault(event.originalEvent);
589
+
590
+ this.stopEventPropagation(event);
591
+
592
+ const newWaypointIndex = props.waypoint + 1;
593
+
594
+ const waypoint = this.createWaypointAt(event.latlng, newWaypointIndex);
595
+
596
+ const newWaypointMarker = this.options
597
+ .markerFactory({ waypoint })
598
+ .setLatLng(event.latlng)
599
+ .addTo(this.map);
600
+ const newWaypoints = this.withWaypointAt(this.waypoints, newWaypointIndex, waypoint);
601
+ this._waypoints = newWaypoints;
602
+
603
+ this.activeDragCleanup?.();
604
+ this.activeDragCleanup = undefined;
605
+
606
+ this.dispatcher.fire('waypointAdded', {
607
+ waypoint,
608
+ });
609
+ this.dispatcher.fire('viewStateChanged', {
610
+ state: {
611
+ ...this.routing.state,
612
+ waypoints: newWaypoints,
613
+ },
614
+ reason: 'interaction',
615
+ });
616
+
617
+ void this.previewRoute(newWaypoints);
618
+
619
+ const mouseMoveHandler = (moveEvent: LeafletMouseEvent): void => {
620
+ newWaypointMarker.setLatLng(moveEvent.latlng);
621
+
622
+ const draggedWaypoint = this.createWaypointAt(moveEvent.latlng, newWaypointIndex);
623
+
624
+ const movedWaypoints = this.withUpdatedWaypointAt(
625
+ this.waypoints,
626
+ newWaypointIndex,
627
+ draggedWaypoint,
628
+ );
629
+ this._waypoints = movedWaypoints;
630
+ this.dispatcher.fire('waypointDrag', {
631
+ waypoint: movedWaypoints[newWaypointIndex],
632
+ });
633
+ this.dispatcher.fire('viewStateChanged', {
634
+ state: { ...this.routing.state, waypoints: movedWaypoints },
635
+ reason: 'interaction',
636
+ });
637
+
638
+ if (this.options.routesWhileDragging) {
639
+ this.dragCommitHandler(movedWaypoints, newWaypointIndex);
640
+ }
641
+ };
642
+
643
+ const mouseUpHandler = (mouseUpEvent: LeafletMouseEvent): void => {
644
+ this.activeDragCleanup = undefined;
645
+ this.map.off('mousemove', mouseMoveHandler);
646
+ this.map.off('mouseup', mouseUpHandler);
647
+
648
+ this.dragCommitHandler.cancel();
649
+
650
+ const finalWaypoint = this.createWaypointAt(mouseUpEvent.latlng, newWaypointIndex);
651
+
652
+ const finalWaypoints = this.withUpdatedWaypointAt(
653
+ this.waypoints,
654
+ newWaypointIndex,
655
+ finalWaypoint,
656
+ );
657
+ this._waypoints = finalWaypoints;
658
+
659
+ this.projectWaypoints(finalWaypoints);
660
+
661
+ newWaypointMarker.remove();
662
+
663
+ this.dispatcher.fire('waypointDragEnd', {
664
+ waypoint: finalWaypoints[newWaypointIndex],
665
+ });
666
+ this.dispatcher.fire('viewStateChanged', {
667
+ state: { ...this.routing.state, waypoints: finalWaypoints },
668
+ reason: 'interaction',
669
+ });
670
+
671
+ this.routing.setWaypoints(finalWaypoints);
672
+ this.routing.recalculateRoute();
673
+ };
674
+
675
+ this.map.on('mousemove', mouseMoveHandler);
676
+
677
+ this.map.once('mouseup', mouseUpHandler);
678
+ this.activeDragCleanup = () => {
679
+ this.map.off('mousemove', mouseMoveHandler);
680
+ this.map.off('mouseup', mouseUpHandler);
681
+ this.dragCommitHandler.cancel();
682
+ newWaypointMarker.remove();
683
+ };
684
+ }
685
+
686
+ // ---------------------------------------------------------------------
687
+ // Editability
688
+ // ---------------------------------------------------------------------
689
+
690
+ public setEditable(isEditable: boolean): void {
691
+ this.setCanAddWaypoints(isEditable);
692
+
693
+ this.setCanSelectRoute(isEditable);
694
+
695
+ this.setCanDragWaypoint(isEditable);
696
+ }
697
+
698
+ public setCanDragWaypoint(canDragWaypoints: boolean): void {
699
+ this._canDragWaypoints = canDragWaypoints;
700
+
701
+ this.addWaypointMarker?.remove();
702
+ this.addWaypointMarkerAdded = false;
703
+
704
+ this.waypointsMarkers.forEach((marker) => {
705
+ if (canDragWaypoints) {
706
+ marker.dragging?.enable();
707
+ } else {
708
+ marker.dragging?.disable();
709
+ }
710
+ });
711
+ }
712
+
713
+ public setCanSelectRoute(canSelectRoute: boolean): void {
714
+ this._canSelectRoute = canSelectRoute;
715
+ }
716
+
717
+ public setCanAddWaypoints(canAddWaypoints: boolean): void {
718
+ this._canAddWaypoints = canAddWaypoints;
719
+
720
+ if (!canAddWaypoints) {
721
+ this.addWaypointMarker?.remove();
722
+ this.addWaypointMarkerAdded = false;
723
+ }
724
+ }
725
+
726
+ public setMaxWaypoints(maxWaypoints: number): void {
727
+ this._maxWaypoints = maxWaypoints;
728
+ }
729
+
730
+ public setHoverEnabled(hoverEnabled: boolean): void {
731
+ this._hoverEnabled = hoverEnabled;
732
+ if (!hoverEnabled) {
733
+ this.map.getContainer().style.cursor = '';
734
+ this.onRouteHoverOut();
735
+ }
736
+ }
737
+
738
+ // ---------------------------------------------------------------------
739
+ // Bounds
740
+ // ---------------------------------------------------------------------
741
+
742
+ public fitViewToData(
743
+ options: {
744
+ padding?: [number, number];
745
+ maxZoom?: number;
746
+ } = {},
747
+ ): void {
748
+ const state = this.routing.state;
749
+
750
+ const bounds = this.computeBounds(state);
751
+
752
+ if (!bounds) {
753
+ return;
754
+ }
755
+
756
+ this.map.fitBounds(bounds, {
757
+ padding: [40, 40],
758
+ ...options,
759
+ });
760
+ }
761
+
762
+ private computeBounds(state: AnyRouting['state']): LatLngBounds | undefined {
763
+ if (state.data?.routesShapeBounds) {
764
+ return new LatLngBounds([
765
+ [state.data.routesShapeBounds[1], state.data.routesShapeBounds[0]],
766
+ [state.data.routesShapeBounds[3], state.data.routesShapeBounds[2]],
767
+ ]);
768
+ }
769
+
770
+ if (state.routesShapeGeojson) {
771
+ const bounds = bbox(state.routesShapeGeojson) as [number, number, number, number];
772
+
773
+ return new LatLngBounds([
774
+ [bounds[1], bounds[0]],
775
+ [bounds[3], bounds[2]],
776
+ ]);
777
+ }
778
+
779
+ if (state.waypoints.length > 0) {
780
+ const bounds = new LatLngBounds([]);
781
+
782
+ state.waypoints.forEach((waypoint) => {
783
+ bounds.extend([waypoint.position.lat, waypoint.position.lng]);
784
+ });
785
+
786
+ return bounds;
787
+ }
788
+
789
+ return undefined;
790
+ }
791
+
792
+ // ---------------------------------------------------------------------
793
+ // Event bus
794
+ // ---------------------------------------------------------------------
795
+
796
+ public on<E extends keyof LeafletProjectorEventMap>(
797
+ event: E,
798
+ call: (event: LeafletProjectorEventMap[E]) => void,
799
+ ): void {
800
+ this.dispatcher.on(event, call);
801
+ }
802
+
803
+ public off<E extends keyof LeafletProjectorEventMap>(
804
+ event: E,
805
+ call: (event: LeafletProjectorEventMap[E]) => void,
806
+ ): void {
807
+ this.dispatcher.off(event, call);
808
+ }
809
+
810
+ // ---------------------------------------------------------------------
811
+ // Internal helpers
812
+ // ---------------------------------------------------------------------
813
+
814
+ private createWaypointAt(latlng: LatLng, index: number): InternalWaypoint {
815
+ return InternalWaypointC.fromWaypoint(
816
+ {
817
+ position: {
818
+ lat: latlng.lat,
819
+ lng: latlng.lng,
820
+ },
821
+ },
822
+ {
823
+ isFirst: false,
824
+ isLast: false,
825
+ index,
826
+ },
827
+ );
828
+ }
829
+
830
+ private withWaypointAt(
831
+ waypoints: InternalWaypoint[],
832
+ index: number,
833
+ waypoint: InternalWaypoint,
834
+ ): InternalWaypoint[] {
835
+ const next = [...waypoints];
836
+
837
+ next.splice(index, 0, waypoint);
838
+
839
+ return next;
840
+ }
841
+
842
+ private withUpdatedWaypointAt(
843
+ waypoints: InternalWaypoint[],
844
+ index: number,
845
+ waypoint: InternalWaypoint,
846
+ ): InternalWaypoint[] {
847
+ const next = [...waypoints];
848
+
849
+ next[index] = waypoint;
850
+
851
+ return next;
852
+ }
853
+
854
+ private withUpdatedPosition(
855
+ waypoints: InternalWaypoint[],
856
+ index: number,
857
+ position: LatLngPosition,
858
+ ): InternalWaypoint[] {
859
+ const next = [...waypoints];
860
+
861
+ next[index] = {
862
+ ...next[index],
863
+ position: {
864
+ lat: position.lat,
865
+ lng: position.lng,
866
+ },
867
+ geocoded: false,
868
+ };
869
+
870
+ return next;
871
+ }
872
+
873
+ private async previewRoute(waypoints: InternalWaypoint[]): Promise<void> {
874
+ const requestId = ++this.previewRequestId;
875
+ if (!this.previewLoading) {
876
+ this.previewLoading = true;
877
+ this.dispatcher.fire('previewStarted', {});
878
+ }
879
+
880
+ let data: AnyRoutingDataResponse | undefined;
881
+
882
+ try {
883
+ const provider = this.options.previewDataProvider ?? this.routing.dataProvider;
884
+ data = await provider.request(
885
+ waypoints.map((waypoint) => ({ ...waypoint })),
886
+ { mode: 'preview' },
887
+ );
888
+ if (requestId !== this.previewRequestId || !data) return;
889
+ this.dispatcher.fire('previewFinished', { data });
890
+ this.projectRoute(data.routesShapeGeojson);
891
+ } catch (error) {
892
+ if (requestId === this.previewRequestId) {
893
+ this.dispatcher.fire('previewError', {
894
+ error: error instanceof Error ? error : new Error(String(error)),
895
+ });
896
+ }
897
+ } finally {
898
+ if (requestId === this.previewRequestId) {
899
+ this.previewLoading = false;
900
+ if (data) {
901
+ this.lastPreviewData = { waypoints: [...waypoints], data };
902
+ }
903
+ }
904
+
905
+ }
906
+ }
907
+
908
+ private setAndRecalculate(waypoints: InternalWaypoint[]): void {
909
+ this.routing.setWaypoints(waypoints);
910
+ void this.routing.recalculateRoute();
911
+ }
912
+
913
+ private showAddWaypointMarker(position: LatLng): void {
914
+ if (!this.addWaypointMarker) return;
915
+
916
+ this.addWaypointMarker.setLatLng(position);
917
+ if (!this.addWaypointMarkerAdded) {
918
+ this.addWaypointMarker.addTo(this.map);
919
+ this.addWaypointMarkerAdded = true;
920
+ }
921
+ }
922
+
923
+ private bringSelectedRouteToFront(): void {
924
+ const selectedRouteId = this.routing.state.selectedRouteId;
925
+ const getFeature = (layer: Layer): LeafletRouteFeature | undefined =>
926
+ layer instanceof Path
927
+ ? this.routeFeatures.get(layer)
928
+ : (layer as Layer & { feature?: LeafletRouteFeature }).feature;
929
+ const outlineLayers: Path[] = [];
930
+ const routeLayers: Path[] = [];
931
+
932
+ this.routeOutlineLayer?.eachLayer((layer) => {
933
+ if (layer instanceof Path) {
934
+ outlineLayers.push(layer);
935
+ }
936
+ });
937
+ this.routesLayer?.eachLayer((layer) => {
938
+ if (layer instanceof Path) {
939
+ const feature = getFeature(layer);
940
+ if (feature) {
941
+ layer.setStyle(this.getRouteStyle(feature));
942
+ }
943
+ routeLayers.push(layer);
944
+ }
945
+ });
946
+
947
+ outlineLayers.forEach((layer) => layer.bringToFront());
948
+ routeLayers.forEach((layer) => layer.bringToFront());
949
+
950
+ if (selectedRouteId == null) return;
951
+
952
+ outlineLayers
953
+ .filter((layer) => getFeature(layer)?.properties?.routeId === selectedRouteId)
954
+ .forEach((layer) => layer.bringToFront());
955
+ routeLayers
956
+ .filter((layer) => getFeature(layer)?.properties?.routeId === selectedRouteId)
957
+ .forEach((layer) => layer.bringToFront());
958
+ }
959
+
960
+ private destroyRoutes(): void {
961
+ this.routeOutlineLayer?.removeFrom(this.map);
962
+ this.routeOutlineLayer = undefined;
963
+
964
+ if (!this.routesLayer) {
965
+ return;
966
+ }
967
+
968
+ this.routesLayer.removeFrom(this.map);
969
+
970
+ this.routesLayer = undefined;
971
+ }
972
+
973
+ // ---------------------------------------------------------------------
974
+ // Event cancellation
975
+ // ---------------------------------------------------------------------
976
+
977
+ private eventIsCancelled(event: LeafletMouseEvent): boolean {
978
+ const original = event.originalEvent as MouseEvent & {
979
+ handledFor?: string[];
980
+ };
981
+
982
+ return original.handledFor?.includes(event.type) ?? false;
983
+ }
984
+
985
+ private stopEventPropagation(event: LeafletMouseEvent): void {
986
+ const original = event.originalEvent as MouseEvent & {
987
+ handledFor?: string[];
988
+ };
989
+
990
+ original.handledFor = [...(original.handledFor ?? []), event.type];
991
+ }
992
+ }