• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jumpinjackie / mapguide-react-layout / 15160437878

21 May 2025 11:00AM UTC coverage: 21.631% (-42.6%) from 64.24%
15160437878

Pull #1552

github

web-flow
Merge 8b7153d9e into 236e2ea07
Pull Request #1552: Feature/package updates 2505

839 of 1165 branches covered (72.02%)

11 of 151 new or added lines in 25 files covered. (7.28%)

1332 existing lines in 50 files now uncovered.

4794 of 22163 relevant lines covered (21.63%)

6.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/map-providers/base.ts
1
import * as React from "react";
×
2
import * as ReactDOM from "react-dom";
×
3
import { IMapView, IExternalBaseLayer, Dictionary, ReduxDispatch, Bounds, GenericEvent, ActiveMapTool, DigitizerCallback, LayerProperty, Size2, RefreshMode, KC_U, ILayerManager, Coordinate2D, KC_ESCAPE, IMapViewer, IMapGuideViewerSupport, ILayerInfo, ClientKind, IMapImageExportOptions } from '../../api/common';
×
4
import { MouseTrackingTooltip } from '../tooltips/mouse';
×
5
import Map from "ol/Map";
×
6
import OverviewMap from 'ol/control/OverviewMap';
×
7
import DragBox from 'ol/interaction/DragBox';
×
8
import Select from 'ol/interaction/Select';
×
9
import Draw, { GeometryFunction } from 'ol/interaction/Draw';
×
10
import { SelectedFeaturesTooltip, ISelectionPopupContentOverrideProvider, SelectionPopupContentRenderer } from '../tooltips/selected-features';
×
11
import Feature from 'ol/Feature';
×
12
import Polygon from 'ol/geom/Polygon';
×
13
import Geometry from 'ol/geom/Geometry';
14
import type { MapOptions } from 'ol/Map';
15
import Attribution from 'ol/control/Attribution';
×
16
import Rotate from 'ol/control/Rotate';
×
17
import DragRotate from 'ol/interaction/DragRotate';
×
18
import DragPan from 'ol/interaction/DragPan';
×
19
import PinchRotate from 'ol/interaction/PinchRotate';
×
20
import PinchZoom from 'ol/interaction/PinchZoom';
×
21
import KeyboardPan from 'ol/interaction/KeyboardPan';
×
22
import KeyboardZoom from 'ol/interaction/KeyboardZoom';
×
23
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
×
24
import View from 'ol/View';
×
25
import Point from 'ol/geom/Point';
26
import LineString from 'ol/geom/LineString';
27
import Circle from 'ol/geom/Circle';
28
import type Interaction from 'ol/interaction/Interaction';
29
import type Overlay from 'ol/Overlay';
30
import { transformExtent, ProjectionLike } from 'ol/proj';
×
31
import { LayerManager } from '../../api/layer-manager';
×
32
import type Collection from 'ol/Collection';
33
import * as olExtent from "ol/extent";
×
34
import * as olEasing from "ol/easing";
×
35
import type MapBrowserEvent from 'ol/MapBrowserEvent';
36
import { tr, DEFAULT_LOCALE } from '../../api/i18n';
×
37
import type { LayerSetGroupBase } from '../../api/layer-set-group-base';
38
import { assertIsDefined } from '../../utils/assert';
×
39
import { info, debug } from '../../utils/logger';
×
40
import { setCurrentView, setViewRotation, mapResized, setMouseCoordinates, setBusyCount, setViewRotationEnabled, setActiveTool, mapLayerAdded, externalLayersReady, addClientSelectedFeature, clearClientSelection } from '../../actions/map';
×
41
import { Toaster, Intent } from '@blueprintjs/core';
×
42
import { IOLFactory, OLFactory } from '../../api/ol-factory';
×
43
import type { ISubscriberProps } from '../../containers/subscriber';
44
import isMobile from "ismobilejs";
45
import type { IInitialExternalLayer } from '../../actions/defs';
46
import { MapGuideMockMode } from '../mapguide-debug-context';
47
import LayerBase from 'ol/layer/Base';
48
import LayerGroup from 'ol/layer/Group';
×
49
import type { LoadFunction as TileLoadFunction } from 'ol/Tile';
50
import type { LoadFunction as ImageLoadFunction } from 'ol/Image';
51
import { IBasicPointCircleStyle, DEFAULT_POINT_CIRCLE_STYLE, IPointIconStyle, DEFAULT_POINT_ICON_STYLE, IBasicVectorLineStyle, DEFAULT_LINE_STYLE, IBasicVectorPolygonStyle, DEFAULT_POLY_STYLE, ClusterClickAction } from '../../api/ol-style-contracts';
×
52
import { isClusteredFeature, getClusterSubFeatures } from '../../api/ol-style-helpers';
×
53
import type { OLStyleMapSet } from '../../api/ol-style-map-set';
54
import { QueryMapFeaturesResponse } from '../../api/contracts/query';
55
import { setViewer, getViewer } from '../../api/runtime';
×
56
import { Client } from '../../api/client';
×
57
import { useReduxDispatch } from "./context";
×
58
import { ClientSelectionFeature } from "../../api/contracts/common";
59
import type { OLFeature, OLLayer } from "../../api/ol-types";
NEW
60
import { supportsTouch } from "../../utils/mobile-browser";
×
61

62
function isValidView(view: IMapView) {
×
63
    if (view.resolution) {
×
64
        return !isNaN(view.x)
×
65
            && !isNaN(view.y)
×
66
            && !isNaN(view.scale)
×
67
            && !isNaN(view.resolution);
×
68
    } else {
×
69
        return !isNaN(view.x)
×
70
            && !isNaN(view.y)
×
71
            && !isNaN(view.scale);
×
72
    }
×
73
}
×
74

75
export function inflateBoundsByMeters(thisProj: ProjectionLike, extent: Bounds, meters: number) {
×
76
    // We need to inflate this bbox by a known unit of measure (meters), so re-project this extent to a meter's based coordinate system (EPSG:3857)
77
    const webmBounds = transformExtent(extent, thisProj, "EPSG:3857");
×
78
    // Inflate the box by specified amount
79
    const webmBounds2 = olExtent.buffer(webmBounds, meters);
×
80
    // Re-project this extent back to the original projection
81
    const inflatedBounds = transformExtent(webmBounds2, "EPSG:3857", thisProj) as Bounds;
×
82
    return inflatedBounds;
×
83
}
×
84

85
export function recursiveFindLayer(layers: Collection<LayerBase>, predicate: (layer: LayerBase) => boolean): LayerBase | undefined {
×
86
    for (let i = 0; i < layers.getLength(); i++) {
×
87
        const layer = layers.item(i);
×
88
        if (layer instanceof LayerGroup) {
×
89
            const match = recursiveFindLayer(layer.getLayers(), predicate);
×
90
            if (match) {
×
91
                return match;
×
92
            }
×
93
        } else {
×
94
            if (predicate(layer)) {
×
95
                return layer;
×
96
            }
×
97
        }
×
98
    }
×
99
    return undefined;
×
100
}
×
101

102
export function isMiddleMouseDownEvent(e: MouseEvent): boolean {
×
103
    return (e && (e.which == 2 || e.button == 4));
×
104
}
×
105

106
export function useViewerSideEffects(context: IMapProviderContext,
×
107
    appSettings: Dictionary<string>,
×
108
    isReady: boolean,
×
109
    mapName: string | undefined,
×
110
    layers: ILayerInfo[] | undefined,
×
111
    initialExternalLayers: IInitialExternalLayer[] | undefined,
×
112
    agentUri: string | undefined = undefined,
×
113
    agentKind: ClientKind | undefined = undefined,
×
114
    selection: QueryMapFeaturesResponse | null = null) {
×
115
    const dispatch = useReduxDispatch();
×
116
    // Side-effect to pre-load external layers. Should only happen once per map name
117
    React.useEffect(() => {
×
118
        if (isReady) {
×
119
            if (mapName && !layers) {
×
120
                debug(`React.useEffect - Change of initial external layers for [${mapName}] (change should only happen once per mapName!)`);
×
121
                if (initialExternalLayers && initialExternalLayers.length > 0) {
×
122
                    debug(`React.useEffect - First-time loading of external layers for [${mapName}]`);
×
123
                    const layerManager = context.getLayerManager(mapName) as LayerManager;
×
124
                    for (const extLayer of initialExternalLayers) {
×
125
                        const added = layerManager.addExternalLayer(extLayer, true, appSettings);
×
126
                        if (added) {
×
127
                            dispatch(mapLayerAdded(mapName, added));
×
128
                        }
×
129
                    }
×
130
                } else {
×
131
                    //Even if no initial external layers were loaded, the layers state still needs to be set
132
                    //otherwise components that depend on this state (eg. External Layer Manager) will assume
133
                    //this is still not ready yet
134
                    debug(`React.useEffect - Signal that external layers are ready for [${mapName}]`);
×
135
                    dispatch(externalLayersReady(mapName));
×
136
                }
×
137
            }
×
138
        }
×
139
    }, [context, mapName, initialExternalLayers, layers, isReady]);
×
140
    // Side-effect to apply the current external layer list
141
    React.useEffect(() => {
×
142
        debug(`React.useEffect - Change of external layers`);
×
143
        if (context.isReady() && layers) {
×
144
            const layerManager = context.getLayerManager(mapName);
×
145
            layerManager.apply(layers);
×
146
        }
×
147
    }, [context, mapName, layers]);
×
148
    // Side-effect to set the viewer "instance" once the MapViewerBase component has been mounted.
149
    // Should only happen once.
150
    React.useEffect(() => {
×
151
        debug(`React.useEffect - Change of context and/or agent URI/kind`);
×
152
        setViewer(context);
×
153
        const browserWindow: any = window;
×
154
        browserWindow.getViewer = browserWindow.getViewer || getViewer;
×
155
        if (agentUri && agentKind) {
×
156
            browserWindow.getClient = browserWindow.getClient || (() => new Client(agentUri, agentKind));
×
157
        }
×
158
        debug(`React.useEffect - Attached runtime viewer instance and installed browser global APIs`);
×
159
    }, [context, agentUri, agentKind]);
×
160
    // Side-effect to imperatively refresh the map upon selection change
161
    React.useEffect(() => {
×
162
        debug(`React.useEffect - Change of selection`);
×
163
        context.refreshMap(RefreshMode.SelectionOnly);
×
164
    }, [context, selection]);
×
165
}
×
166

167
export interface IViewerComponent {
168
    /**
169
     * @since 0.14.8
170
     */
171
    isShiftKeyDown: () => boolean;
172
    isContextMenuOpen: () => boolean;
173
    setDigitizingType: (digitizingType: string | undefined) => void;
174
    onDispatch: ReduxDispatch;
175
    onHideContextMenu: () => void;
176
    addImageLoading(): void;
177
    addImageLoaded(): void;
178
    addSubscribers(props: ISubscriberProps[]): string[];
179
    removeSubscribers(names: string[]): boolean;
180
    getSubscribers(): string[];
181
    selectCanDragPan(): boolean;
182
}
183

184
export interface IMapProviderState {
185
    activeTool: ActiveMapTool;
186
    view: IMapView | undefined;
187
    viewRotation: number;
188
    viewRotationEnabled: boolean;
189
    mapName: string | undefined;
190
    busyWorkers: number;
191
    locale: string;
192
    externalBaseLayers: IExternalBaseLayer[] | undefined;
193
    cancelDigitizationKey: number;
194
    undoLastPointKey: number;
195
    overviewMapElementSelector?: () => Element | null;
196
    initialExternalLayers: IInitialExternalLayer[];
197
}
198

199
export interface IMapProviderStateExtras {
200
    isReady: boolean;
201
    bgColor?: string;
202
    layers: ILayerInfo[] | undefined;
203
}
204

205
/**
206
 * Defines a mapping provider
207
 * 
208
 * @since 0.14
209
 */
210
export interface IMapProviderContext extends IMapViewer, ISelectionPopupContentOverrideProvider {
211
    isReady(): boolean;
212
    getProviderName(): string;
213
    isDigitizing(): boolean;
214
    getActiveTool(): ActiveMapTool;
215
    isMouseOverTooltip(): boolean;
216
    incrementBusyWorker(): void;
217
    decrementBusyWorker(): void;
218
    attachToComponent(el: HTMLElement, comp: IViewerComponent): void;
219
    detachFromComponent(): void;
220
    setToasterRef(ref: React.RefObject<Toaster>): void;
221
    setProviderState(nextState: IMapProviderState): void;
222
    onKeyDown(e: GenericEvent): void;
223
    hideAllPopups(): void;
224
    getHookFunction(): () => IMapProviderState & IMapProviderStateExtras;
225
    addCustomSelectionPopupRenderer(mapName: string | undefined, layerName: string | undefined, renderer: SelectionPopupContentRenderer): void;
226
}
227

228
export type WmsQueryAugmentation = (getFeatureInfoUrl: string) => string;
229

230
export abstract class BaseMapProviderContext<TState extends IMapProviderState, TLayerSetGroup extends LayerSetGroupBase> implements IMapProviderContext {
×
231
    private _toasterRef: React.RefObject<Toaster> | undefined;
232
    private _baseTileSourceLoaders: Dictionary<Dictionary<TileLoadFunction>>;
233
    private _tileSourceLoaders: Dictionary<Dictionary<TileLoadFunction>>;
234
    private _imageSourceLoaders: Dictionary<Dictionary<ImageLoadFunction>>;
235
    private _wmsQueryAugmentations: Dictionary<Dictionary<WmsQueryAugmentation>>;
236
    private _globalCustomSelectionPopupRenderer: SelectionPopupContentRenderer | undefined;
237
    private _customSelectionPopupRenderers: Dictionary<Dictionary<SelectionPopupContentRenderer>>;
238
    protected _state: TState;
239
    /**
240
     * Indicates if touch events are supported.
241
     */
242
    protected _supportsTouch: boolean;
243
    /**
244
     * The internal OpenLayers map instance
245
     *
246
     * @private
247
     * @type {Map}
248
     */
249
    protected _map: Map | undefined;
250
    protected _ovMap: OverviewMap | undefined;
251

252
    protected _layerSetGroups: Dictionary<TLayerSetGroup>;
253
    protected _mouseTooltip: MouseTrackingTooltip | undefined;
254
    protected _selectTooltip: SelectedFeaturesTooltip | undefined;
255

256
    protected _comp: IViewerComponent | undefined;
257
    protected _zoomSelectBox: DragBox | undefined;
258

259
    protected _busyWorkers: number;
260
    protected _triggerZoomRequestOnMoveEnd: boolean;
261
    protected _select: Select | undefined;
262
    protected _activeDrawInteraction: Draw | null;
263

264
    constructor(private olFactory: OLFactory = new OLFactory()) {
×
265
        this._busyWorkers = 0;
×
266
        this._layerSetGroups = {};
×
267
        this._tileSourceLoaders = {};
×
268
        this._imageSourceLoaders = {};
×
269
        this._wmsQueryAugmentations = {};
×
270
        this._triggerZoomRequestOnMoveEnd = true;
×
NEW
271
        this._supportsTouch = supportsTouch();
×
272
        const baseInitialState: IMapProviderState = {
×
273
            activeTool: ActiveMapTool.None,
×
274
            view: undefined,
×
275
            viewRotation: 0,
×
276
            viewRotationEnabled: true,
×
277
            locale: DEFAULT_LOCALE,
×
278
            cancelDigitizationKey: KC_ESCAPE,
×
279
            undoLastPointKey: KC_U,
×
280
            busyWorkers: 0,
×
281
            mapName: undefined,
×
282
            externalBaseLayers: undefined,
×
283
            initialExternalLayers: []
×
284
        };
×
285
        this._state = {
×
286
            ...baseInitialState,
×
287
            ...this.getInitialProviderState()
×
288
        } as TState;
×
289
    }
×
290

291
    /**
292
     * Exports an image of the current map view
293
     *
294
     * @param {IMapImageExportOptions} options
295
     * @memberof IMapViewer
296
     * @since 0.14
297
     */
298
    public exportImage(options: IMapImageExportOptions): void {
×
299
        if (this._map) {
×
300
            const map = this._map;
×
301
            map.once('rendercomplete', function () {
×
302
                const mapCanvas = document.createElement('canvas');
×
303
                if (options.size) {
×
304
                    const [w, h] = options.size;
×
305
                    mapCanvas.width = w;
×
306
                    mapCanvas.height = h;
×
307
                } else {
×
308
                    const size = map.getSize();
×
309
                    if (size) {
×
310
                        mapCanvas.width = size[0];
×
311
                        mapCanvas.height = size[1];
×
312
                    }
×
313
                }
×
314
                const mapContext = mapCanvas.getContext('2d');
×
315
                if (mapContext) {
×
316
                    const canvasSelector = '.ol-layer canvas, .external-vector-layer canvas';
×
317
                    Array.prototype.forEach.call(document.querySelectorAll(canvasSelector), function (canvas: HTMLCanvasElement) {
×
318
                        if (canvas.width > 0) {
×
319
                            const parentNode = canvas.parentNode;
×
320
                            const opacity = (parentNode as any)?.style?.opacity ?? "";
×
321
                            mapContext.globalAlpha = opacity === '' ? 1 : Number(opacity);
×
322
                            const transform = canvas.style.transform;
×
323
                            // Get the transform parameters from the style's transform matrix
324
                            const matrix = transform.match(/^matrix\(([^\(]*)\)$/)?.[1]?.split(',')?.map(Number);
×
325
                            if (matrix) {
×
326
                                // Apply the transform to the export map context
327
                                CanvasRenderingContext2D.prototype.setTransform.apply(mapContext, matrix);
×
328
                                mapContext.drawImage(canvas, 0, 0);
×
329
                            }
×
330
                        }
×
331
                    });
×
332
                    options.callback(mapCanvas.toDataURL(options.exportMimeType));
×
333
                }
×
334
            });
×
335
            map.renderSync();
×
336
        }
×
337
    }
×
338

339
    /**
340
     * Adds a custom tile load function for a given base image tile layer.
341
     * 
342
     * NOTE: Unlike other load function registrations this must be done before the viewer is mounted. New load functions added at runtime will not be recognized
343
     * @param mapName
344
     * @param layerName The base layer this function should apply for
345
     * @param func The custom tile load function
346
     * @since 0.14
347
     */
348
    addBaseTileLoadFunction(mapName: string, layerName: string, func: TileLoadFunction) {
×
349
        if (!this._baseTileSourceLoaders) {
×
350
            this._baseTileSourceLoaders = {};
×
351
        }
×
352
        if (!this._baseTileSourceLoaders[mapName]) {
×
353
            this._baseTileSourceLoaders[mapName] = {};
×
354
        }
×
355
        this._baseTileSourceLoaders[mapName][layerName] = func;
×
356
    }
×
357

358
    /**
359
     * Adds a custom tile load function for a given overlay image tile layer
360
     * @param mapName
361
     * @param layerName The layer this function should apply for
362
     * @param func The custom tile load function
363
     * @since 0.14
364
     */
365
    addTileLoadFunction(mapName: string, layerName: string, func: TileLoadFunction) {
×
366
        if (!this._tileSourceLoaders) {
×
367
            this._tileSourceLoaders = {};
×
368
        }
×
369
        if (!this._tileSourceLoaders[mapName]) {
×
370
            this._tileSourceLoaders[mapName] = {};
×
371
        }
×
372
        this._tileSourceLoaders[mapName][layerName] = func;
×
373
    }
×
374

375
    /**
376
     * Adds a custom image load function for a given overlay image layer
377
     * @param mapName
378
     * @param layerName The layer this function should apply for
379
     * @param func The custom tile load function
380
     * @since 0.14
381
     */
382
    addImageLoadFunction(mapName: string, layerName: string, func: ImageLoadFunction) {
×
383
        if (!this._imageSourceLoaders) {
×
384
            this._imageSourceLoaders = {};
×
385
        }
×
386
        if (!this._imageSourceLoaders[mapName]) {
×
387
            this._imageSourceLoaders[mapName] = {};
×
388
        }
×
389
        this._imageSourceLoaders[mapName][layerName] = func;
×
390
    }
×
391

392
    /**
393
     * Adds a WMS query augmentation for the given WMS overlay layer
394
     * @param mapName
395
     * @param layerName The layer this function should apply for
396
     * @param func The WMS query augmentation
397
     * @since 0.14
398
     */
399
    addWmsQueryAugmentation(mapName: string, layerName: string, func: WmsQueryAugmentation) {
×
400
        if (!this._wmsQueryAugmentations) {
×
401
            this._wmsQueryAugmentations = {};
×
402
        }
×
403
        if (!this._wmsQueryAugmentations[mapName]) {
×
404
            this._wmsQueryAugmentations[mapName] = {};
×
405
        }
×
406
        this._wmsQueryAugmentations[mapName][layerName] = func;
×
407
    }
×
408

409
    public addCustomSelectionPopupRenderer(mapName: string | undefined, layerName: string | undefined, renderer: SelectionPopupContentRenderer) {
×
410
        if (mapName && layerName) {
×
411
            if (!this._customSelectionPopupRenderers) {
×
412
                this._customSelectionPopupRenderers = {};
×
413
            }
×
414
            if (!this._customSelectionPopupRenderers[mapName]) {
×
415
                this._customSelectionPopupRenderers[mapName] = {};
×
416
            }
×
417
            this._customSelectionPopupRenderers[mapName][layerName] = renderer;
×
418
        } else {
×
419
            this._globalCustomSelectionPopupRenderer = renderer;
×
420
        }
×
421
    }
×
422

423
    public getSelectionPopupRenderer(layerName: string): SelectionPopupContentRenderer | undefined {
×
424
        if (!this._customSelectionPopupRenderers) {
×
425
            this._customSelectionPopupRenderers = {};
×
426
        }
×
427
        const { mapName } = this._state;
×
428
        if (mapName) {
×
429
            if (!this._customSelectionPopupRenderers[mapName]) {
×
430
                this._customSelectionPopupRenderers[mapName] = {};
×
431
            }
×
432
            const r = this._customSelectionPopupRenderers[mapName][layerName];
×
433
            if (r) {
×
434
                return r;
×
435
            }
×
436
        }
×
437

438
        if (this._globalCustomSelectionPopupRenderer) {
×
439
            return this._globalCustomSelectionPopupRenderer;
×
440
        }
×
441

442
        return undefined;
×
443
    }
×
444

445
    /**
446
     * @virtual
447
     */
448
    public hideAllPopups() {
×
449
        this._mouseTooltip?.clear();
×
450
        this._selectTooltip?.hide();
×
451
    }
×
452

453
    public isReady(): boolean { return !!(this._map && this._comp); }
×
454

455
    //#region IMapViewer
456
    /**
457
     * @virtual
458
     * @returns {(IMapGuideViewerSupport | undefined)}
459
     * @memberof BaseMapProviderContext
460
     */
461
    mapguideSupport(): IMapGuideViewerSupport | undefined {
×
462
        return undefined;
×
463
    }
×
464
    setActiveTool(tool: ActiveMapTool): void {
×
465
        this._comp?.onDispatch(setActiveTool(tool));
×
466
    }
×
467
    getOLFactory(): IOLFactory {
×
468
        return this.olFactory;
×
469
    }
×
470
    getMapName(): string {
×
471
        return this._state.mapName!;
×
472
    }
×
473
    setViewRotation(rotation: number): void {
×
474
        this._comp?.onDispatch(setViewRotation(rotation));
×
475
    }
×
476
    getViewRotation(): number {
×
477
        return this._state.viewRotation;
×
478
    }
×
479
    isViewRotationEnabled(): boolean {
×
480
        return this._state.viewRotationEnabled;
×
481
    }
×
482
    setViewRotationEnabled(enabled: boolean): void {
×
483
        this._comp?.onDispatch(setViewRotationEnabled(enabled));
×
484
    }
×
485
    toastSuccess(iconName: string, message: string | JSX.Element): string | undefined {
×
486
        return this._toasterRef?.current?.show({ icon: (iconName as any), message: message, intent: Intent.SUCCESS });
×
487
    }
×
488
    toastWarning(iconName: string, message: string | JSX.Element): string | undefined {
×
489
        return this._toasterRef?.current?.show({ icon: (iconName as any), message: message, intent: Intent.WARNING });
×
490
    }
×
491
    toastError(iconName: string, message: string | JSX.Element): string | undefined {
×
492
        return this._toasterRef?.current?.show({ icon: (iconName as any), message: message, intent: Intent.DANGER });
×
493
    }
×
494
    toastPrimary(iconName: string, message: string | JSX.Element): string | undefined {
×
495
        return this._toasterRef?.current?.show({ icon: (iconName as any), message: message, intent: Intent.PRIMARY });
×
496
    }
×
497
    dismissToast(key: string): void {
×
498
        this._toasterRef?.current?.dismiss(key);
×
499
    }
×
500
    addImageLoading(): void {
×
501
        this._comp?.addImageLoading();
×
502
    }
×
503
    addImageLoaded(): void {
×
504
        this._comp?.addImageLoaded();
×
505
    }
×
506
    addSubscribers(props: ISubscriberProps[]): string[] {
×
507
        return this._comp?.addSubscribers(props) ?? [];
×
508
    }
×
509
    removeSubscribers(names: string[]): boolean {
×
510
        return this._comp?.removeSubscribers(names) ?? false;
×
511
    }
×
512
    getSubscribers(): string[] {
×
513
        return this._comp?.getSubscribers() ?? [];
×
514
    }
×
515
    dispatch(action: any): void {
×
516
        this._comp?.onDispatch(action);
×
517
    }
×
518
    getDefaultPointCircleStyle(): IBasicPointCircleStyle {
×
519
        return { ...DEFAULT_POINT_CIRCLE_STYLE };
×
520
    }
×
521
    getDefaultPointIconStyle(): IPointIconStyle {
×
522
        return { ...DEFAULT_POINT_ICON_STYLE };
×
523
    }
×
524
    getDefaultLineStyle(): IBasicVectorLineStyle {
×
525
        return { ...DEFAULT_LINE_STYLE };
×
526
    }
×
527
    getDefaultPolygonStyle(): IBasicVectorPolygonStyle {
×
528
        return { ...DEFAULT_POLY_STYLE };
×
529
    }
×
530
    //#endregion
531

532
    public abstract getHookFunction(): () => IMapProviderState & IMapProviderStateExtras;
533
    protected getBaseTileSourceLoaders(mapName: string): Dictionary<TileLoadFunction> {
×
534
        return this._baseTileSourceLoaders?.[mapName] ?? {};
×
535
    }
×
536
    protected getTileSourceLoaders(mapName: string): Dictionary<TileLoadFunction> {
×
537
        return this._tileSourceLoaders?.[mapName] ?? {};
×
538
    }
×
539
    protected getImageSourceLoaders(mapName: string): Dictionary<ImageLoadFunction> {
×
540
        return this._imageSourceLoaders?.[mapName] ?? {};
×
541
    }
×
542

543
    protected abstract getInitialProviderState(): Omit<TState, keyof IMapProviderState>;
544
    //#region IMapViewerContextCallback
545
    protected getMockMode(): MapGuideMockMode | undefined { return undefined; }
×
546
    protected addFeatureToHighlight(feat: OLFeature | undefined, bAppend: boolean): void {
×
547
        if (this._state.mapName) {
×
548
            // Features have to belong to layer in order to be visible and have the highlight style, 
549
            // so in addition to adding this new feature to the OL select observable collection, we 
550
            // need to also add the feature to a scratch vector layer dedicated for this purpose
551
            const layerSet = this.getLayerSetGroup(this._state.mapName);
×
552
            if (layerSet) {
×
553
                const sf = this._select?.getFeatures();
×
554
                if (sf) {
×
555
                    if (!bAppend) {
×
556
                        sf.clear();
×
557
                    }
×
558

559
                    if (feat) {
×
560
                        sf.push(feat);
×
561
                    }
×
562
                }
×
563
            }
×
564
        }
×
565
    }
×
566
    //#endregion
567

568
    //#region Map Context
569
    protected getScaleForExtent(bounds: Bounds): number {
×
570
        assertIsDefined(this._map);
×
571
        assertIsDefined(this._state.mapName);
×
572
        const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
573
        assertIsDefined(activeLayerSet);
×
574
        const size = this._map.getSize();
×
575
        assertIsDefined(size);
×
576
        const mcsW = olExtent.getWidth(bounds);
×
577
        const mcsH = olExtent.getHeight(bounds);
×
578
        const devW = size[0];
×
579
        const devH = size[1];
×
580
        const metersPerPixel = 0.0254 / activeLayerSet.getDpi();
×
581
        const metersPerUnit = activeLayerSet.getMetersPerUnit();
×
582
        //Scale calculation code from AJAX viewer
583
        let mapScale: number;
×
584
        if (devH * mcsW > devW * mcsH)
×
585
            mapScale = mcsW * metersPerUnit / (devW * metersPerPixel); // width-limited
×
586
        else
587
            mapScale = mcsH * metersPerUnit / (devH * metersPerPixel); // height-limited
×
588
        return mapScale;
×
589
    }
×
590
    public getViewForExtent(extent: Bounds): IMapView {
×
591
        assertIsDefined(this._map);
×
592

593
        let scale, center;
×
594
        // If this is a zero-width/height extent, we need to "inflate" it to something small
595
        // so that we do not enter an infinite loop due to attempting to get a x/y/scale from
596
        // a zero-width/height extent.
597
        //
598
        // This generally happens if we want to zoom to the bounds of a selected point
599
        if (olExtent.getWidth(extent) == 0 || olExtent.getHeight(extent) == 0) {
×
600
            const thisProj = this.getProjection();
×
601
            // Inflate the box by 20 meters
602
            const inflatedBounds = inflateBoundsByMeters(thisProj, extent, 20);
×
603
            // Now we can safely extract the scale/center
604
            scale = this.getScaleForExtent(inflatedBounds);
×
605
            center = olExtent.getCenter(inflatedBounds);
×
606
        } else {
×
607
            scale = this.getScaleForExtent(extent);
×
608
            center = olExtent.getCenter(extent);
×
609
        }
×
610

611
        return {
×
612
            x: center[0],
×
613
            y: center[1],
×
614
            scale: scale,
×
615
            resolution: this._map.getView().getResolution()
×
616
        };
×
617
    }
×
618
    protected onZoomSelectBox(e: GenericEvent) {
×
619
        if (this._comp) {
×
620
            const extent = this._zoomSelectBox?.getGeometry();
×
621
            if (!extent) {
×
622
                return;
×
623
            }
×
624
            switch (this._state.activeTool) {
×
625
                case ActiveMapTool.Zoom:
×
626
                    {
×
627
                        const ext: any = extent.getExtent();
×
628
                        this._comp.onDispatch(setCurrentView(this.getViewForExtent(ext)));
×
629
                    }
×
630
                    break;
×
631
                case ActiveMapTool.Select:
×
632
                    {
×
633
                        this.selectFeaturesByExtent(extent);
×
634
                    }
×
635
                    break;
×
636
            }
×
637
        }
×
638
    }
×
639
    /**
640
     * @virtual
641
     * @protected
642
     * @param {GenericEvent} e
643
     * @returns
644
     * @memberof BaseMapProviderContext
645
     */
646
    protected onMouseMove(e: GenericEvent) {
×
647
        if (this._comp) {
×
648
            this.handleMouseTooltipMouseMove(e);
×
649
            this.handleHighlightHover(e);
×
650
            if (this._comp.isContextMenuOpen()) {
×
651
                return;
×
652
            }
×
653
            if (this._state.mapName) {
×
654
                this._comp.onDispatch(setMouseCoordinates(this._state.mapName, e.coord));
×
655
            }
×
656
        }
×
657
    }
×
658
    public incrementBusyWorker() {
×
659
        this._busyWorkers++;
×
660
        this._comp?.onDispatch(setBusyCount(this._busyWorkers));
×
661
    }
×
662
    public decrementBusyWorker() {
×
663
        this._busyWorkers--;
×
664
        this._comp?.onDispatch(setBusyCount(this._busyWorkers));
×
665
    }
×
666
    protected applyView(layerSet: LayerSetGroupBase, vw: IMapView) {
×
667
        this._triggerZoomRequestOnMoveEnd = false;
×
668
        layerSet.getView().setCenter([vw.x, vw.y]);
×
669
        //Don't use this.scaleToResolution() as that uses this.props to determine
670
        //applicable layer set, but we already have that here
671
        const res = layerSet.scaleToResolution(vw.scale);
×
672
        layerSet.getView().setResolution(res);
×
673
        this._triggerZoomRequestOnMoveEnd = true;
×
674
    }
×
675
    protected removeActiveDrawInteraction() {
×
676
        if (this._activeDrawInteraction && this._map && this._comp) {
×
677
            this._map.removeInteraction(this._activeDrawInteraction);
×
678
            this._activeDrawInteraction = null;
×
679
            this._comp.setDigitizingType(undefined);
×
680
        }
×
681
    }
×
682

683
    public getActiveTool(): ActiveMapTool { return this._state.activeTool; }
×
684

685
    public cancelDigitization(): void {
×
686
        if (this.isDigitizing()) {
×
687
            this.removeActiveDrawInteraction();
×
688
            this.clearMouseTooltip();
×
689
            //this._mouseTooltip.clear();
690
        }
×
691
    }
×
692
    private onBeginDigitization = (callback: (cancelled: boolean) => void) => {
×
693
        this._comp?.onDispatch(setActiveTool(ActiveMapTool.None));
×
694
        //Could be a small timing issue here, but the active tool should generally
695
        //be "None" before the user clicks their first digitizing vertex/point
696
        callback(false);
×
697
    };
×
698
    protected pushDrawInteraction<T extends Geometry>(digitizingType: string, draw: Draw, handler: DigitizerCallback<T>, prompt?: string): void {
×
699
        assertIsDefined(this._comp);
×
700
        this.onBeginDigitization(cancel => {
×
701
            if (!cancel) {
×
702
                assertIsDefined(this._map);
×
703
                assertIsDefined(this._comp);
×
704
                this.removeActiveDrawInteraction();
×
705
                //this._mouseTooltip.clear();
706
                this.clearMouseTooltip();
×
707
                if (prompt) {
×
708
                    //this._mouseTooltip.setText(prompt);
709
                    this.setMouseTooltip(prompt);
×
710
                }
×
711
                this._activeDrawInteraction = draw;
×
712
                this._activeDrawInteraction.once("drawend", (e: GenericEvent) => {
×
713
                    const drawnFeature: OLFeature = e.feature;
×
714
                    const geom: T = drawnFeature.getGeometry() as T;
×
715
                    this.cancelDigitization();
×
716
                    handler(geom);
×
717
                })
×
718
                this._map.addInteraction(this._activeDrawInteraction);
×
719
                this._comp.setDigitizingType(digitizingType);
×
720
            }
×
721
        });
×
722
    }
×
723
    /**
724
     * @virtual
725
     * @protected
726
     * @param {Polygon} geom
727
     * @memberof BaseMapProviderContext
728
     */
729
    protected selectFeaturesByExtent(geom: Polygon) { }
×
730

731
    protected zoomByDelta(delta: number) {
×
732
        assertIsDefined(this._map);
×
733
        const view = this._map.getView();
×
734
        if (!view) {
×
735
            return;
×
736
        }
×
737
        const currentZoom = view.getZoom();
×
738
        if (currentZoom !== undefined) {
×
739
            const newZoom = view.getConstrainedZoom(currentZoom + delta);
×
740
            if (view.getAnimating()) {
×
741
                view.cancelAnimations();
×
742
            }
×
743
            view.animate({
×
744
                zoom: newZoom,
×
745
                duration: 250,
×
746
                easing: olEasing.easeOut
×
747
            });
×
748
        }
×
749
    }
×
750
    protected ensureAndGetLayerSetGroup(nextState: TState) {
×
751
        assertIsDefined(nextState.mapName);
×
752
        let layerSet = this._layerSetGroups[nextState.mapName];
×
753
        if (!layerSet) {
×
754
            layerSet = this.initLayerSet(nextState);
×
755
            this._layerSetGroups[nextState.mapName] = layerSet;
×
756
        }
×
757
        return layerSet;
×
758
    }
×
759
    //public getLayerSet(name: string, bCreate: boolean = false, props?: IMapViewerContextProps): MgLayerSet {
760
    public getLayerSetGroup(name: string | undefined): TLayerSetGroup | undefined {
×
761
        let layerSet: TLayerSetGroup | undefined;
×
762
        if (name) {
×
763
            layerSet = this._layerSetGroups[name];
×
764
            /*
765
            if (!layerSet && props && bCreate) {
766
                layerSet = this.initLayerSet(props);
767
                this._layerSets[props.map.Name] = layerSet;
768
                this._activeMapName = props.map.Name;
769
            }
770
            */
771
        }
×
772
        return layerSet;
×
773
    }
×
774
    /**
775
     * @virtual
776
     * @readonly
777
     * @memberof BaseMapProviderContext
778
     */
779
    public isMouseOverTooltip() { return this._selectTooltip?.isMouseOver ?? false; }
×
780
    protected clearMouseTooltip(): void {
×
781
        this._mouseTooltip?.clear();
×
782
    }
×
783
    protected setMouseTooltip(text: string) {
×
784
        this._mouseTooltip?.setText(text);
×
785
    }
×
786
    protected handleMouseTooltipMouseMove(e: GenericEvent) {
×
787
        this._mouseTooltip?.onMouseMove?.(e);
×
788
    }
×
789
    private _highlightedFeature: OLFeature | undefined;
790
    private isLayerHoverable(layer: OLLayer) {
×
791
        return !(layer?.get(LayerProperty.IS_HOVER_HIGHLIGHT) == true)
×
792
            && !(layer?.get(LayerProperty.IS_WMS_SELECTION_OVERLAY) == true)
×
793
            && !(layer?.get(LayerProperty.IS_HEATMAP) == true)
×
794
            && !(layer?.get(LayerProperty.IS_MEASURE) == true)
×
795
            && !(layer?.get(LayerProperty.DISABLE_HOVER) == true);
×
796
    }
×
797
    protected handleHighlightHover(e: GenericEvent) {
×
798
        if (e.dragging) {
×
799
            return;
×
800
        }
×
801
        if (this._state.busyWorkers > 0) {
×
802
            //console.log("Skip highlight hover due to busyWorkers > 0");
803
            return;
×
804
        }
×
805
        if (this._state.mapName && this._map) {
×
806
            const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
807
            if (activeLayerSet) {
×
808
                const pixel = this._map.getEventPixel(e.originalEvent);
×
809
                if (pixel) {
×
810
                    const featureToLayerMap = [] as [OLFeature, OLLayer][];
×
811
                    this._map.forEachFeatureAtPixel(pixel, (feature, layer) => {
×
812
                        if (this.isLayerHoverable(layer) && feature instanceof Feature) {
×
813
                            featureToLayerMap.push([feature, layer]);
×
814
                        }
×
815
                    });
×
816
                    const feature = featureToLayerMap.length ? featureToLayerMap[0][0] : undefined;
×
817

818
                    //const featuresAtPixel = this._map?.getFeaturesAtPixel(pixel);
819
                    //const feature = featuresAtPixel?.length ? featuresAtPixel[0] : undefined;
820
                    if (feature != this._highlightedFeature && feature instanceof Feature) {
×
821
                        if (this._highlightedFeature) {
×
822
                            activeLayerSet.removeHighlightedFeature(this._highlightedFeature);
×
823
                        }
×
824
                        if (feature) {
×
825
                            activeLayerSet.addHighlightedFeature(feature);
×
826
                        }
×
827
                        this._highlightedFeature = feature;
×
828
                    }
×
829
                }
×
830
            }
×
831
        }
×
832
    }
×
833
    protected hideSelectedVectorFeaturesTooltip() {
×
834
        this._selectTooltip?.hide();
×
835
    }
×
836
    protected showSelectedVectorFeatures(features: Collection<OLFeature>, pixel: [number, number], featureToLayerMap: [OLFeature, OLLayer][], locale?: string) {
×
837
        this._selectTooltip?.showSelectedVectorFeatures(features, pixel, featureToLayerMap, locale);
×
838
    }
×
839
    protected async queryWmsFeatures(mapName: string | undefined, coord: Coordinate2D, bAppendMode: boolean) {
×
840
        if (mapName && this._map) {
×
841
            const activeLayerSet = this.getLayerSetGroup(mapName);
×
842
            const layerMgr = this.getLayerManager(mapName);
×
843
            const res = this._map.getView().getResolution();
×
844
            if (res && this._selectTooltip) {
×
845
                return await this._selectTooltip.queryWmsFeatures(activeLayerSet, layerMgr, coord, res, bAppendMode, {
×
846
                    getLocale: () => this._state.locale,
×
847
                    addClientSelectedFeature: (feat, layer) => this.addClientSelectedFeature(feat, layer),
×
848
                    addFeatureToHighlight: (feat, bAppend) => this.addFeatureToHighlight(feat, bAppend),
×
849
                    getWmsRequestAugmentations: () => this._wmsQueryAugmentations[mapName] ?? {}
×
850
                });
×
851
            }
×
852
        }
×
853
        return false;
×
854
    }
×
855
    /**
856
     * @virtual
857
     * @protected
858
     * @param {GenericEvent} e
859
     * @memberof BaseMapProviderContext
860
     */
861
    protected onImageError(e: GenericEvent) { }
×
862

863
    private addClientSelectedFeature(f: OLFeature, l: LayerBase) {
×
864
        if (this._select)
×
865
            this._select.getFeatures().push(f);
×
866
        if (this._state.mapName) {
×
867
            const features = f.get("features");
×
868
            let theFeature: OLFeature;
×
869
            //Are we clustered?
870
            if (Array.isArray(features)) {
×
871
                // Only proceeed with dispatch if single item array
872
                if (features.length == 1) {
×
873
                    theFeature = features[0];
×
874
                } else {
×
875
                    return;
×
876
                }
×
877
            } else {
×
878
                theFeature = f;
×
879
            }
×
880
            const p = { ...theFeature.getProperties() };
×
881
            delete p[theFeature.getGeometryName()];
×
882
            const feat: ClientSelectionFeature = {
×
883
                bounds: theFeature.getGeometry()?.getExtent() as Bounds,
×
884
                properties: p
×
885
            };
×
886
            this.dispatch(addClientSelectedFeature(this._state.mapName, l.get(LayerProperty.LAYER_NAME), feat));
×
887
        }
×
888
    }
×
889

890
    private clearClientSelectedFeatures() {
×
891
        if (this._select)
×
892
            this._select.getFeatures().clear();
×
893
        if (this._state.mapName) {
×
894
            this.dispatch(clearClientSelection(this._state.mapName));
×
895
        }
×
896
    }
×
897

898
    protected onMapClick(e: MapBrowserEvent<any>) {
×
899
        if (!this._comp || !this._map) {
×
900
            return;
×
901
        }
×
902

903
        if (this._comp.isContextMenuOpen()) {
×
904
            // We're going on the assumption that due to element placement
905
            // if this event is fired, it meant that the user clicked outside
906
            // the context menu, otherwise the context menu itself would've handled
907
            // the event
908
            this._comp.onHideContextMenu?.();
×
909
        }
×
910
        if (this.isDigitizing()) {
×
911
            return;
×
912
        }
×
913

914
        //TODO: Our selected feature tooltip only shows properties of a single feature
915
        //and displays upon said feature being selected. As a result, although we can
916
        //(and should) allow for multiple features to be selected, we need to figure
917
        //out the proper UI for such a case before we enable multiple selection.
918
        const bAppendMode = false;
×
919

920
        const featureToLayerMap = [] as [OLFeature, OLLayer][];
×
921
        if ((this._state.activeTool == ActiveMapTool.Select) && this._select) {
×
922
            if (!bAppendMode) {
×
923
                this.clearClientSelectedFeatures();
×
924
            }
×
925
            this._map.forEachFeatureAtPixel(e.pixel, (feature, layer) => {
×
926
                if (featureToLayerMap.length == 0) { //See TODO above
×
927
                    if (layer.get(LayerProperty.IS_SELECTABLE) == true && feature instanceof Feature) {
×
928
                        featureToLayerMap.push([feature, layer]);
×
929
                    }
×
930
                }
×
931
            });
×
932
            if (this._select && featureToLayerMap.length == 1) {
×
933
                const [f, l] = featureToLayerMap[0];
×
934
                if (isClusteredFeature(f) && getClusterSubFeatures(f).length > 1 && (l.get(LayerProperty.VECTOR_STYLE) as OLStyleMapSet)?.getClusterClickAction() == ClusterClickAction.ZoomToClusterExtents) {
×
935
                    const zoomBounds = getClusterSubFeatures(f).reduce((bounds, currentFeatures) => {
×
936
                        const g = currentFeatures.getGeometry();
×
937
                        if (g) {
×
938
                            return olExtent.extend(bounds, g.getExtent());
×
939
                        } else {
×
940
                            return bounds;
×
941
                        }
×
942
                    }, olExtent.createEmpty()) as Bounds;
×
943

944
                    // Inflate the bounds by 20 meters so that the new view has some "breathing space" and you don't see points
945
                    // of the cluster on the edge of the view
946
                    const inflatedBounds = inflateBoundsByMeters(this.getProjection(), zoomBounds, 20);
×
947
                    this.zoomToExtent(inflatedBounds);
×
948
                } else {
×
949
                    this.addClientSelectedFeature(f, l);
×
950
                }
×
951
            }
×
952
        }
×
953
        // We'll only fall through the normal map selection query route if no 
954
        // vector features were selected as part of this click
955
        const px = e.pixel as [number, number];
×
956
        if (featureToLayerMap.length == 0) {
×
957
            this.hideSelectedVectorFeaturesTooltip();
×
958
            if (this._state.activeTool == ActiveMapTool.Select) {
×
959
                this.queryWmsFeatures(this._state.mapName, e.coordinate as Coordinate2D, bAppendMode).then(madeSelection => {
×
960
                    if (!madeSelection) {
×
961
                        this.onProviderMapClick(px);
×
962
                    } else {
×
963
                        console.log("Made WMS selection. Skipping provider click event");
×
964
                    }
×
965
                })
×
966
            } else {
×
967
                this.onProviderMapClick(px);
×
968
            }
×
969
        } else {
×
970
            if (this._select) {
×
971
                if (!bAppendMode) {
×
972
                    if (this._state.mapName) {
×
973
                        const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
974
                        activeLayerSet?.clearWmsSelectionOverlay();
×
975
                    }
×
976
                }
×
977
                this.showSelectedVectorFeatures(this._select.getFeatures(), px, featureToLayerMap, this._state.locale);
×
978
            }
×
979
        }
×
980
    }
×
981
    protected abstract onProviderMapClick(px: [number, number]): void;
982
    protected abstract initLayerSet(nextState: TState): TLayerSetGroup;
983
    public abstract getProviderName(): string;
984

985
    protected initContext(layerSet: TLayerSetGroup, locale?: string, overviewMapElementSelector?: () => (Element | null)) {
×
986
        if (this._map) {
×
987
            // HACK: className property not documented. This needs to be fixed in OL api doc.
988
            const overviewMapOpts: any = {
×
989
                className: 'ol-overviewmap ol-custom-overviewmap',
×
990
                layers: layerSet.getLayersForOverviewMap(),
×
991
                view: new View({
×
992
                    projection: layerSet.getProjection()
×
993
                }),
×
994
                tipLabel: tr("OL_OVERVIEWMAP_TIP", locale),
×
995
                collapseLabel: String.fromCharCode(187), //'\u00BB',
×
996
                label: String.fromCharCode(171) //'\u00AB'
×
997
            };
×
998

999
            if (overviewMapElementSelector) {
×
1000
                const el = overviewMapElementSelector();
×
1001
                if (el) {
×
1002
                    overviewMapOpts.target = ReactDOM.findDOMNode(el);
×
1003
                    overviewMapOpts.collapsed = false;
×
1004
                    overviewMapOpts.collapsible = false;
×
1005
                }
×
1006
            }
×
1007
            this._ovMap = new OverviewMap(overviewMapOpts);
×
1008
            this._map.addControl(this._ovMap);
×
1009
            this.onBeforeAttachingLayerSetGroup(layerSet);
×
1010
            layerSet.attach(this._map, this._ovMap, false);
×
1011
        }
×
1012
    }
×
1013
    //#endregion
1014

1015
    /**
1016
     * @virtual
1017
     * @protected
1018
     * @param {TLayerSetGroup} layerSetGroup
1019
     * @memberof BaseMapProviderContext
1020
     */
1021
    protected onBeforeAttachingLayerSetGroup(layerSetGroup: TLayerSetGroup): void { }
×
1022
    public setToasterRef(ref: React.RefObject<Toaster>) {
×
1023
        this._toasterRef = ref;
×
1024
    }
×
1025
    public abstract setProviderState(nextState: TState): void;
1026

1027
    public onKeyDown(e: GenericEvent) {
×
1028
        const cancelKey = this._state.cancelDigitizationKey ?? KC_ESCAPE;
×
1029
        const undoKey = this._state.undoLastPointKey ?? KC_U;
×
1030
        if (e.keyCode == cancelKey) {
×
1031
            this.cancelDigitization();
×
1032
        } else if (e.keyCode == undoKey && this._activeDrawInteraction) {
×
1033
            this._activeDrawInteraction.removeLastPoint();
×
1034
        }
×
1035
    }
×
1036

1037
    public isDigitizing(): boolean {
×
1038
        if (!this._map)
×
1039
            return false;
×
1040
        const activeDraw = this._map.getInteractions().getArray().filter(inter => inter instanceof Draw)[0];
×
1041
        return activeDraw != null;
×
1042
    }
×
1043

1044
    public detachFromComponent(): void {
×
1045
        this._comp = undefined;
×
1046
        this._select?.dispose();
×
1047
        this._select = undefined;
×
1048

1049
        if (this._boundZoomSelectBox) {
×
1050
            this._zoomSelectBox?.un("boxend", this._boundZoomSelectBox as any);
×
1051
            this._boundZoomSelectBox = undefined;
×
1052
        }
×
1053
        if (this._boundClick) {
×
1054
            this._map?.un("click", this._boundClick as any);
×
1055
            this._boundClick = undefined;
×
1056
        }
×
1057
        if (this._boundMouseMove) {
×
1058
            this._map?.un("pointermove", this._boundMouseMove as any);
×
1059
            this._boundMouseMove = undefined;
×
1060
        }
×
1061
        if (this._boundResize) {
×
1062
            this._map?.un("change:size", this._boundResize as any);
×
1063
            this._boundResize = undefined;
×
1064
        }
×
1065
        if (this._boundMoveEnd) {
×
1066
            this._map?.un("moveend", this._boundMoveEnd as any);
×
1067
            this._boundMoveEnd = undefined;
×
1068
        }
×
1069

1070
        this._zoomSelectBox?.dispose();
×
1071
        this._zoomSelectBox = undefined;
×
1072
        this._activeDrawInteraction?.dispose();
×
1073
        this._activeDrawInteraction = null;
×
1074
        this._selectTooltip?.dispose();
×
1075
        this._selectTooltip = undefined;
×
1076
        this._mouseTooltip?.dispose()
×
1077
        this._mouseTooltip = undefined;
×
1078
        this._map?.setTarget(undefined);
×
1079
        this._ovMap?.setMap(undefined as any); //HACK: Typings workaround
×
1080
        this._map = undefined;
×
1081
        this._ovMap = undefined;
×
1082
        debug(`Map provider context detached from component and reset to initial state`);
×
1083
    }
×
1084

1085
    private onMoveEnd(e: GenericEvent) {
×
1086
        //HACK:
1087
        //
1088
        //What we're hoping here is that when the view has been broadcasted back up
1089
        //and flowed back in through new view props, that the resulting zoom/pan
1090
        //operation in componentDidUpdate() is effectively a no-op as the intended
1091
        //zoom/pan location has already been reached by this event right here
1092
        //
1093
        //If we look at this through Redux DevTools, we see 2 entries for Map/SET_VIEW
1094
        //for the initial view (un-desirable), but we still only get one map image request
1095
        //for the initial view (good!). Everything is fine after that.
1096
        if (this._triggerZoomRequestOnMoveEnd) {
×
1097
            const cv = this.getCurrentView();
×
1098
            if (isValidView(cv)) {
×
1099
                this._comp?.onDispatch(setCurrentView(cv));
×
1100
            } else {
×
1101
                console.warn("Attempt to set invalid view",cv);
×
1102
            }
×
1103
        } else {
×
1104
            info("Triggering zoom request on moveend suppresseed");
×
1105
        }
×
1106
        if (e.frameState.viewState.rotation != this._state.viewRotation) {
×
1107
            this._comp?.onDispatch(setViewRotation(e.frameState.viewState.rotation));
×
1108
        }
×
1109
    }
×
1110

1111
    private _boundZoomSelectBox: Function | undefined;
1112
    private _boundMouseMove: Function | undefined;
1113
    private _boundResize: Function | undefined;
1114
    private _boundClick: Function | undefined;
1115
    private _boundMoveEnd: Function | undefined;
1116

1117
    /**
1118
     * @virtual
1119
     * @param {HTMLElement} el
1120
     * @param {IViewerComponent} comp
1121
     * @memberof BaseMapProviderContext
1122
     */
1123
    public attachToComponent(el: HTMLElement, comp: IViewerComponent): void {
×
1124
        this._comp = comp;
×
1125
        this._select = new Select({
×
1126
            condition: (e) => false,
×
1127
            layers: (layer) => layer.get(LayerProperty.IS_SELECTABLE) == true || layer.get(LayerProperty.IS_SCRATCH) == true
×
1128
        });
×
1129
        this._zoomSelectBox = new DragBox({
×
1130
            condition: (e) => {
×
1131
                // DragBox needs to be suppressed if the select tool can drag pan
1132
                if (this._state.activeTool == ActiveMapTool.Select && this._comp?.selectCanDragPan() === true) {
×
1133
                    return false;
×
1134
                }
×
1135
                const startingMiddleMouseDrag = e.type == "pointerdown" && isMiddleMouseDownEvent((e as any).originalEvent);
×
1136
                return !this.isDigitizing() && !startingMiddleMouseDrag && (this._state.activeTool === ActiveMapTool.Select || this._state.activeTool === ActiveMapTool.Zoom)
×
1137
            }
×
1138
        });
×
1139
        this._boundZoomSelectBox = this.onZoomSelectBox.bind(this);
×
1140
        this._boundMouseMove = this.onMouseMove.bind(this);
×
1141
        this._boundResize = this.onResize.bind(this);
×
1142
        this._boundClick = this.onMapClick.bind(this);
×
1143
        this._boundMoveEnd = this.onMoveEnd.bind(this);
×
1144

1145
        this._zoomSelectBox.on("boxend", this._boundZoomSelectBox as any);
×
1146
        const mapOptions: MapOptions = {
×
1147
            target: el as any,
×
1148
            //layers: layers,
1149
            //view: view,
1150
            controls: [
×
1151
                new Attribution({
×
1152
                    tipLabel: tr("OL_ATTRIBUTION_TIP", this._state.locale)
×
1153
                }),
×
1154
                new Rotate({
×
1155
                    tipLabel: tr("OL_RESET_ROTATION_TIP", this._state.locale)
×
1156
                })
×
1157
            ],
×
1158
            interactions: [
×
1159
                this._select,
×
1160
                new DragRotate(),
×
1161
                new DragPan({
×
1162
                    condition: (e) => {
×
1163
                        // We'll allow for select tool to pan if instructed from above
1164
                        if (this._state.activeTool == ActiveMapTool.Select && this._comp?.selectCanDragPan() === true) {
×
1165
                            return true;
×
1166
                        }
×
1167
                        const startingMiddleMouseDrag = e.type == "pointerdown" && isMiddleMouseDownEvent((e as any).originalEvent);
×
1168
                        const enabled = (startingMiddleMouseDrag || this._supportsTouch || this._state.activeTool === ActiveMapTool.Pan);
×
1169
                        //console.log(e);
1170
                        //console.log(`Allow Pan - ${enabled} (middle mouse: ${startingMiddleMouseDrag})`);
1171
                        return enabled;
×
1172
                    }
×
1173
                }),
×
1174
                new PinchRotate(),
×
1175
                new PinchZoom(),
×
1176
                new KeyboardPan(),
×
1177
                new KeyboardZoom(),
×
1178
                new MouseWheelZoom(),
×
1179
                this._zoomSelectBox
×
1180
            ]
×
1181
        };
×
1182
        this._map = new Map(mapOptions);
×
1183
        const activeLayerSet = this.ensureAndGetLayerSetGroup(this._state);
×
1184
        this.initContext(activeLayerSet, this._state.locale, this._state.overviewMapElementSelector);
×
1185
        this._mouseTooltip = new MouseTrackingTooltip(this._map, this._comp.isContextMenuOpen);
×
1186
        this._selectTooltip = new SelectedFeaturesTooltip(this._map, this);
×
1187
        this._map.on("pointermove", this._boundMouseMove as any);
×
1188
        this._map.on("change:size", this._boundResize as any);
×
1189
        this._map.on("click", this._boundClick as any);
×
1190
        this._map.on("moveend", this._boundMoveEnd as any);
×
1191

1192
        if (this._state.view) {
×
1193
            const { x, y, scale } = this._state.view;
×
1194
            this.zoomToView(x, y, scale);
×
1195
        } else {
×
1196
            const extents = activeLayerSet.getExtent();
×
1197
            this._map.getView().fit(extents);
×
1198
        }
×
1199
        this.onResize(this._map.getSize());
×
1200
    }
×
1201
    private onResize = (e: GenericEvent) => {
×
1202
        if (this._map) {
×
1203
            const size = this._map.getSize();
×
1204
            if (size) {
×
1205
                const [w, h] = size;
×
1206
                this._comp?.onDispatch(mapResized(w, h));
×
1207
            }
×
1208
        }
×
1209
    }
×
1210
    public scaleToResolution(scale: number): number {
×
1211
        assertIsDefined(this._state.mapName);
×
1212
        const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
1213
        assertIsDefined(activeLayerSet);
×
1214
        return activeLayerSet.scaleToResolution(scale);
×
1215
    }
×
1216

1217
    public resolutionToScale(resolution: number): number {
×
1218
        assertIsDefined(this._state.mapName);
×
1219
        const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
1220
        assertIsDefined(activeLayerSet);
×
1221
        return activeLayerSet.resolutionToScale(resolution);
×
1222
    }
×
1223

1224
    public getCurrentView(): IMapView {
×
1225
        const ov = this.getOLView();
×
1226
        const center = ov.getCenter();
×
1227
        const resolution = ov.getResolution();
×
1228
        const scale = this.resolutionToScale(resolution!);
×
1229
        return {
×
1230
            x: center![0],
×
1231
            y: center![1],
×
1232
            scale: scale,
×
1233
            resolution: resolution
×
1234
        };
×
1235
    }
×
1236
    public getCurrentExtent(): Bounds {
×
1237
        assertIsDefined(this._map);
×
1238
        return this._map.getView().calculateExtent(this._map.getSize()) as Bounds;
×
1239
    }
×
1240
    public getSize(): Size2 {
×
1241
        assertIsDefined(this._map);
×
1242
        return this._map.getSize() as Size2;
×
1243
    }
×
1244
    public getOLView(): View {
×
1245
        assertIsDefined(this._map);
×
1246
        return this._map.getView();
×
1247
    }
×
1248
    public zoomToView(x: number, y: number, scale: number): void {
×
1249
        if (this._map) {
×
1250
            const view = this._map.getView();
×
1251
            view.setCenter([x, y]);
×
1252
            view.setResolution(this.scaleToResolution(scale));
×
1253
        }
×
1254
    }
×
1255
    /**
1256
     * @virtual
1257
     * @param {RefreshMode} [mode=RefreshMode.LayersOnly | RefreshMode.SelectionOnly]
1258
     * @memberof BaseMapProviderContext
1259
     */
1260
    public refreshMap(mode: RefreshMode = RefreshMode.LayersOnly | RefreshMode.SelectionOnly): void { }
×
1261
    public getMetersPerUnit(): number {
×
1262
        assertIsDefined(this._state.mapName);
×
1263
        const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
1264
        assertIsDefined(activeLayerSet);
×
1265
        return activeLayerSet.getMetersPerUnit();
×
1266
    }
×
1267
    public initialView(): void {
×
1268
        assertIsDefined(this._comp);
×
1269
        assertIsDefined(this._state.mapName);
×
1270
        const activeLayerSet = this.getLayerSetGroup(this._state.mapName);
×
1271
        assertIsDefined(activeLayerSet);
×
1272
        this._comp.onDispatch(setCurrentView(this.getViewForExtent(activeLayerSet.getExtent())));
×
1273
    }
×
1274

1275
    public zoomDelta(delta: number): void {
×
1276
        //TODO: To conform to redux uni-directional data flow, this should
1277
        //broadcast the new desired view back up and flow it back through to this
1278
        //component as new props
1279
        this.zoomByDelta(delta);
×
1280
    }
×
1281
    public zoomToExtent(extent: Bounds): void {
×
1282
        this._comp?.onDispatch(setCurrentView(this.getViewForExtent(extent)));
×
1283
    }
×
1284
    public digitizePoint(handler: DigitizerCallback<Point>, prompt?: string): void {
×
1285
        assertIsDefined(this._comp);
×
1286
        const draw = new Draw({
×
1287
            type: "Point"
×
1288
        });
×
1289
        this.pushDrawInteraction("Point", draw, handler, prompt || tr("DIGITIZE_POINT_PROMPT", this._state.locale));
×
1290
    }
×
1291
    public digitizeLine(handler: DigitizerCallback<LineString>, prompt?: string): void {
×
1292
        assertIsDefined(this._comp);
×
1293
        const draw = new Draw({
×
1294
            type: "LineString",
×
1295
            minPoints: 2,
×
1296
            maxPoints: 2
×
1297
        });
×
1298
        this.pushDrawInteraction("Line", draw, handler, prompt || tr("DIGITIZE_LINE_PROMPT", this._state.locale));
×
1299
    }
×
1300
    public digitizeLineString(handler: DigitizerCallback<LineString>, prompt?: string): void {
×
1301
        assertIsDefined(this._comp);
×
1302
        const draw = new Draw({
×
1303
            type: "LineString",
×
1304
            minPoints: 2
×
1305
        });
×
1306
        this.pushDrawInteraction("LineString", draw, handler, prompt || tr("DIGITIZE_LINESTRING_PROMPT", this._state.locale, {
×
1307
            key: String.fromCharCode(this._state.undoLastPointKey ?? KC_U) //Pray that a sane (printable) key was bound
×
1308
        }));
×
1309
    }
×
1310
    public digitizeCircle(handler: DigitizerCallback<Circle>, prompt?: string): void {
×
1311
        assertIsDefined(this._comp);
×
1312
        const draw = new Draw({
×
1313
            type: "Circle"
×
1314
        });
×
1315
        this.pushDrawInteraction("Circle", draw, handler, prompt || tr("DIGITIZE_CIRCLE_PROMPT", this._state.locale));
×
1316
    }
×
1317
    public digitizeRectangle(handler: DigitizerCallback<Polygon>, prompt?: string): void {
×
1318
        assertIsDefined(this._comp);
×
1319
        const geomFunc: GeometryFunction = (coordinates, geometry) => {
×
1320
            if (!geometry) {
×
1321
                geometry = new Polygon([]);
×
1322
            }
×
1323
            const start: any = coordinates[0];
×
1324
            const end: any = coordinates[1];
×
1325
            (geometry as any).setCoordinates([
×
1326
                [start, [start[0], end[1]], end, [end[0], start[1]], start]
×
1327
            ]);
×
1328
            return geometry;
×
1329
        };
×
1330
        const draw = new Draw({
×
1331
            type: "LineString",
×
1332
            maxPoints: 2,
×
1333
            geometryFunction: geomFunc
×
1334
        });
×
1335
        this.pushDrawInteraction("Rectangle", draw, handler, prompt || tr("DIGITIZE_RECT_PROMPT", this._state.locale));
×
1336
    }
×
1337
    public digitizePolygon(handler: DigitizerCallback<Polygon>, prompt?: string): void {
×
1338
        assertIsDefined(this._comp);
×
1339
        const draw = new Draw({
×
1340
            type: "Polygon"
×
1341
        });
×
1342
        this.pushDrawInteraction("Polygon", draw, handler, prompt || tr("DIGITIZE_POLYGON_PROMPT", this._state.locale, {
×
1343
            key: String.fromCharCode(this._state.undoLastPointKey ?? KC_U) //Pray that a sane (printable) key was bound
×
1344
        }));
×
1345
    }
×
1346

1347
    public addInteraction<T extends Interaction>(interaction: T): T {
×
1348
        assertIsDefined(this._map);
×
1349
        this._map.addInteraction(interaction);
×
1350
        return interaction;
×
1351
    }
×
1352
    public removeInteraction<T extends Interaction>(interaction: T): void {
×
1353
        this._map?.removeInteraction(interaction);
×
1354
    }
×
1355
    public addOverlay(overlay: Overlay): void {
×
1356
        this._map?.addOverlay(overlay);
×
1357
    }
×
1358
    public removeOverlay(overlay: Overlay): void {
×
1359
        this._map?.removeOverlay(overlay);
×
1360
    }
×
1361
    public getProjection(): ProjectionLike {
×
1362
        assertIsDefined(this._map);
×
1363
        return this._map.getView().getProjection();
×
1364
    }
×
1365
    public addHandler(eventName: string, handler: Function) {
×
1366
        this._map?.on(eventName as any, handler as any);
×
1367
    }
×
1368
    public removeHandler(eventName: string, handler: Function) {
×
1369
        this._map?.un(eventName as any, handler as any);
×
1370
    }
×
1371
    public updateSize() {
×
1372
        this._map?.updateSize();
×
1373
    }
×
1374
    protected getLayerManagerForLayerSet(layerSet: TLayerSetGroup) {
×
1375
        assertIsDefined(this._map);
×
1376
        return new LayerManager(this._map, layerSet);
×
1377
    }
×
1378
    public getLayerManager(mapName?: string): ILayerManager {
×
1379
        assertIsDefined(this._map);
×
1380
        assertIsDefined(this._state.mapName);
×
1381
        const layerSet = this.ensureAndGetLayerSetGroup(this._state); // this.getLayerSet(mapName ?? this._state.mapName, true, this._comp as any);
×
1382
        return this.getLayerManagerForLayerSet(layerSet);
×
1383
    }
×
1384
    public screenToMapUnits(x: number, y: number): [number, number] {
×
1385
        let bAllowOutsideWindow = false;
×
1386
        const [mapDevW, mapDevH] = this.getSize();
×
1387
        const [extX1, extY1, extX2, extY2] = this.getCurrentExtent();
×
1388
        if (!bAllowOutsideWindow) {
×
1389
            if (x > mapDevW - 1) x = mapDevW - 1;
×
1390
            else if (x < 0) x = 0;
×
1391

1392
            if (y > mapDevH - 1) y = mapDevH - 1;
×
1393
            else if (y < 0) y = 0;
×
1394
        }
×
1395
        x = extX1 + (extX2 - extX1) * (x / mapDevW);
×
1396
        y = extY1 - (extY1 - extY2) * (y / mapDevH);
×
1397
        return [x, y];
×
1398
    }
×
1399
    public getSelectedFeatures() {
×
1400
        return this._select?.getFeatures();
×
1401
    }
×
1402
    public getPointSelectionBox(point: Coordinate2D, ptBuffer: number): Bounds {
×
1403
        assertIsDefined(this._map);
×
1404
        const ll = this._map.getCoordinateFromPixel([point[0] - ptBuffer, point[1] - ptBuffer]);
×
1405
        const ur = this._map.getCoordinateFromPixel([point[0] + ptBuffer, point[1] + ptBuffer]);
×
1406
        return [ll[0], ll[1], ur[0], ur[1]];
×
1407
    }
×
1408
    public getResolution(): number | undefined {
×
1409
        assertIsDefined(this._map)
×
1410
        return this._map.getView().getResolution();
×
1411
    }
×
1412
    public updateOverviewMapElement(overviewMapElementSelector: () => (Element | null)) {
×
1413
        if (this._ovMap) {
×
1414
            const el = overviewMapElementSelector();
×
1415
            if (el) {
×
1416
                this._ovMap.setCollapsed(false);
×
1417
                this._ovMap.setCollapsible(false);
×
1418
                this._ovMap.setTarget(ReactDOM.findDOMNode(el) as any);
×
1419
            } else {
×
1420
                this._ovMap.setCollapsed(true);
×
1421
                this._ovMap.setCollapsible(true);
×
1422
                this._ovMap.setTarget(null as any);
×
1423
            }
×
1424
        }
×
1425
    }
×
1426
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc