@geospatial-sdk/openlayers 0.0.5-dev.54 → 0.0.5-dev.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/map/apply-context-diff.d.ts.map +1 -1
  2. package/dist/map/apply-context-diff.js +9 -1
  3. package/dist/map/create-map.d.ts +1 -1
  4. package/dist/map/create-map.d.ts.map +1 -1
  5. package/dist/map/create-map.js +73 -30
  6. package/dist/map/feature-hover.js +1 -1
  7. package/dist/map/handle-errors.d.ts +0 -7
  8. package/dist/map/handle-errors.d.ts.map +1 -1
  9. package/dist/map/handle-errors.js +10 -14
  10. package/dist/map/index.d.ts +2 -1
  11. package/dist/map/index.d.ts.map +1 -1
  12. package/dist/map/index.js +2 -1
  13. package/dist/map/layer-update.d.ts.map +1 -1
  14. package/dist/map/layer-update.js +1 -1
  15. package/dist/map/listen.d.ts.map +1 -1
  16. package/dist/map/listen.js +13 -26
  17. package/dist/map/register-events.d.ts +16 -2
  18. package/dist/map/register-events.d.ts.map +1 -1
  19. package/dist/map/register-events.js +172 -81
  20. package/lib/map/apply-context-diff.ts +16 -5
  21. package/lib/map/create-map.test.ts +172 -60
  22. package/lib/map/create-map.ts +100 -37
  23. package/lib/map/feature-hover.ts +1 -1
  24. package/lib/map/handle-errors.test.ts +13 -36
  25. package/lib/map/handle-errors.ts +10 -28
  26. package/lib/map/index.ts +2 -1
  27. package/lib/map/layer-update.ts +3 -2
  28. package/lib/map/listen.test.ts +977 -0
  29. package/lib/map/listen.ts +123 -0
  30. package/lib/map/register-events.ts +229 -109
  31. package/lib/map/resolved-map-state.ts +38 -0
  32. package/package.json +3 -3
  33. package/dist/map/feature-selection.d.ts +0 -8
  34. package/dist/map/feature-selection.d.ts.map +0 -1
  35. package/dist/map/feature-selection.js +0 -76
  36. package/dist/map/styles.d.ts +0 -16
  37. package/dist/map/styles.d.ts.map +0 -1
  38. package/dist/map/styles.js +0 -77
  39. package/dist/resolved-state/resolved-map-state.d.ts +0 -2
  40. package/dist/resolved-state/resolved-map-state.d.ts.map +0 -1
  41. package/dist/resolved-state/resolved-map-state.js +0 -1
  42. package/lib/map/register-events.test.ts +0 -259
@@ -0,0 +1,977 @@
1
+ import OlMap, { FrameState } from "ol/Map.js";
2
+ import { afterAll, beforeEach, Mock } from "vitest";
3
+ import { listen } from "./listen.js";
4
+ import TileQueue, { getTilePriority } from "ol/TileQueue.js";
5
+ import View from "ol/View.js";
6
+ import Collection from "ol/Collection.js";
7
+ import { get as getProjection, toLonLat } from "ol/proj.js";
8
+ import { FeaturesHoverEventType } from "@geospatial-sdk/core";
9
+ import BaseEvent from "ol/events/Event.js";
10
+ import { GEOSPATIAL_SDK_PREFIX } from "./constants.js";
11
+ import { createLayer, resetMapFromContext } from "./create-map.js";
12
+ import {
13
+ MAP_CTX_LAYER_GEOJSON_FIXTURE,
14
+ MAP_CTX_LAYER_WFS_FIXTURE,
15
+ MAP_CTX_LAYER_WMS_FIXTURE,
16
+ MAP_CTX_LAYER_WMTS_FIXTURE,
17
+ MAP_CTX_LAYER_XYZ_FIXTURE,
18
+ } from "@geospatial-sdk/core/fixtures/map-context.fixtures.js";
19
+ import { applyContextDiffToMap } from "./apply-context-diff.js";
20
+ import { propagateLayerStateChangeEventToMap } from "./register-events.js";
21
+ import MapBrowserEvent from "ol/MapBrowserEvent.js";
22
+ import BaseObject from "ol/Object.js";
23
+ import Layer from "ol/layer/Layer.js";
24
+ import { EndpointError } from "@camptocamp/ogc-client";
25
+
26
+ vi.mock("./get-features.js", () => ({
27
+ readFeaturesAtPixel() {
28
+ return Promise.resolve(
29
+ new Map([
30
+ [
31
+ 0,
32
+ [
33
+ {
34
+ geometry: {
35
+ coordinates: [100, 200],
36
+ type: "Point",
37
+ },
38
+ properties: null,
39
+ type: "Feature",
40
+ },
41
+ ],
42
+ ],
43
+ [
44
+ 1,
45
+ [
46
+ {
47
+ geometry: null,
48
+ properties: {
49
+ density: 123,
50
+ },
51
+ type: "Feature",
52
+ },
53
+ ],
54
+ ],
55
+ ]),
56
+ );
57
+ },
58
+ }));
59
+
60
+ const featureA = {
61
+ geometry: {
62
+ coordinates: [100, 200],
63
+ type: "Point",
64
+ },
65
+ properties: null,
66
+ type: "Feature",
67
+ };
68
+ const featureB = {
69
+ geometry: null,
70
+ properties: {
71
+ density: 123,
72
+ },
73
+ type: "Feature",
74
+ };
75
+
76
+ const EXPECTED_MAP_EXTENT_EPSG4326 = [
77
+ -0.0035932611364780857, -0.0026949458513598756, 0.0035932611364780857,
78
+ 0.0026949458513740865,
79
+ ];
80
+
81
+ async function createMap() {
82
+ const view = new View({
83
+ projection: "EPSG:3857",
84
+ resolution: 1,
85
+ center: [0, 0],
86
+ });
87
+ const layer1 = await createLayer(MAP_CTX_LAYER_WMS_FIXTURE);
88
+ const layer2 = await createLayer(MAP_CTX_LAYER_WMS_FIXTURE);
89
+ const layer3 = await createLayer(MAP_CTX_LAYER_GEOJSON_FIXTURE);
90
+ const layers = new Collection([layer1, layer2, layer3]);
91
+ const map = new BaseObject() as OlMap;
92
+ Object.defineProperties(map, {
93
+ getView: { value: vi.fn(() => view) },
94
+ setView: { value: vi.fn() },
95
+ getEventPixel: { value: vi.fn(() => [10, 10]) },
96
+ getCoordinateFromPixel: { value: vi.fn(() => [123, 123]) },
97
+ getSize: { value: vi.fn(() => [800, 600]) },
98
+ addLayer: { value: vi.fn((layer) => layers.push(layer)) },
99
+ getLayers: {
100
+ value: () => layers,
101
+ },
102
+ render: { value: vi.fn() },
103
+ getTargetElement: { value: vi.fn() },
104
+ });
105
+ propagateLayerStateChangeEventToMap(map, layer1);
106
+ propagateLayerStateChangeEventToMap(map, layer2);
107
+ propagateLayerStateChangeEventToMap(map, layer3);
108
+
109
+ // simulate hover feature initialization
110
+ map.on("pointermove", () => {
111
+ map.dispatchEvent({
112
+ type: `${GEOSPATIAL_SDK_PREFIX}${FeaturesHoverEventType}`,
113
+ features: [featureB],
114
+ featuresByLayer: new Map([[0, [featureB]]]),
115
+ } as unknown as BaseEvent);
116
+ });
117
+
118
+ // wait out all pending timers
119
+ await vi.runAllTimersAsync();
120
+ return map;
121
+ }
122
+ function createMapEvent(map: OlMap, type: string) {
123
+ return new MapBrowserEvent(
124
+ type,
125
+ map,
126
+ new MouseEvent(type, {
127
+ clientX: 10,
128
+ clientY: 10,
129
+ }) as PointerEvent,
130
+ false,
131
+ );
132
+ }
133
+
134
+ describe("event listener registration", () => {
135
+ let map: OlMap;
136
+ beforeEach(async () => {
137
+ vi.useFakeTimers();
138
+ map = await createMap();
139
+ vi.clearAllMocks();
140
+ });
141
+ describe("features hover event", () => {
142
+ let callback: Mock;
143
+ beforeEach(async () => {
144
+ callback = vi.fn();
145
+ listen(map, "features-hover", callback);
146
+ map.dispatchEvent(createMapEvent(map, "pointermove"));
147
+ await vi.runAllTimersAsync();
148
+ });
149
+ it("registers the event on the map", () => {
150
+ expect(callback).toHaveBeenCalledWith({
151
+ type: "features-hover",
152
+ features: [featureB],
153
+ featuresByLayer: new Map([[0, [featureB]]]),
154
+ });
155
+ });
156
+ });
157
+ describe("features click event", () => {
158
+ let callback: Mock;
159
+ beforeEach(async () => {
160
+ callback = vi.fn();
161
+ listen(map, "features-click", callback);
162
+ map.dispatchEvent(createMapEvent(map, "click"));
163
+ await vi.runAllTimersAsync();
164
+ });
165
+ it("registers the event on the map", () => {
166
+ expect(callback).toHaveBeenCalledWith({
167
+ type: "features-click",
168
+ features: [featureA, featureB],
169
+ featuresByLayer: new Map([
170
+ [0, [featureA]],
171
+ [1, [featureB]],
172
+ ]),
173
+ });
174
+ });
175
+ });
176
+ describe("map click event", () => {
177
+ let callback: Mock;
178
+ beforeEach(() => {
179
+ callback = vi.fn();
180
+ listen(map, "map-click", callback);
181
+ map.dispatchEvent(createMapEvent(map, "click"));
182
+ });
183
+ it("registers the event on the map", () => {
184
+ expect(callback).toHaveBeenCalledWith({
185
+ coordinate: toLonLat([123, 123]),
186
+ type: "map-click",
187
+ });
188
+ });
189
+ });
190
+ describe("map view state change event", () => {
191
+ let callback: Mock;
192
+
193
+ beforeEach(() => {
194
+ callback = vi.fn();
195
+ listen(map, "map-view-state-change", callback);
196
+ });
197
+
198
+ it("tracks change in center, resolution & rotation", () => {
199
+ map.getView().setCenter([1000, 1000]);
200
+ map.getView().setResolution(100);
201
+ map.getView().setRotation(Math.PI / 2);
202
+ callback.mockReset();
203
+
204
+ map.getView().setCenter([0, 0]);
205
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
206
+ map.getView().dispatchEvent(createMapEvent(map, "change:resolution"));
207
+ map.getView().dispatchEvent(createMapEvent(map, "change:rotation"));
208
+ map.dispatchEvent(createMapEvent(map, "change:size"));
209
+
210
+ expect(callback).toHaveBeenCalledOnce();
211
+ expect(callback).toHaveBeenCalledWith({
212
+ type: "map-view-state-change",
213
+ viewState: {
214
+ center: [0, 0],
215
+ bearing: 180,
216
+ extent: [
217
+ -0.26949458523585645, -0.3593237582430078, 0.26949458523585645,
218
+ 0.3593237582430078,
219
+ ],
220
+ resolution: 100,
221
+ scaleDenominator: (100 * 1000) / 0.28, // metersPerUnit * resolution * (1000 / mmPerPixel)
222
+ },
223
+ });
224
+ });
225
+ });
226
+ describe("map layer state change", () => {
227
+ let callback: Mock;
228
+
229
+ beforeEach(() => {
230
+ callback = vi.fn();
231
+ listen(map, "map-layer-state-change", callback);
232
+ });
233
+
234
+ describe("layer creation", () => {
235
+ it("emits an error without layer index if something goes wrong when applying a context", async () => {
236
+ await resetMapFromContext(map, {
237
+ layers: [{ type: "doesnt-exist" } as any],
238
+ view: null,
239
+ });
240
+ await vi.runAllTimersAsync();
241
+
242
+ expect(callback).toHaveBeenCalledOnce();
243
+ expect(callback).toHaveBeenCalledWith({
244
+ layerIndex: 0,
245
+ layerState: {
246
+ creationError: true,
247
+ creationErrorMessage:
248
+ 'Unrecognized layer type: {"type":"doesnt-exist"}',
249
+ },
250
+ type: "map-layer-state-change",
251
+ });
252
+ });
253
+ it("emits an error without layer index if something goes wrong when applying a context diff", async () => {
254
+ await applyContextDiffToMap(map, {
255
+ layersAdded: [
256
+ {
257
+ layer: { type: "doesnt-exist" } as any,
258
+ position: 3,
259
+ },
260
+ ],
261
+ layersRemoved: [],
262
+ layersReordered: [],
263
+ layersChanged: [
264
+ {
265
+ // the change here needs a recreation of the layer
266
+ layer: { type: "also-doesnt-exist" } as any,
267
+ previousLayer: MAP_CTX_LAYER_GEOJSON_FIXTURE,
268
+ position: 2,
269
+ },
270
+ ],
271
+ });
272
+ await vi.runAllTimersAsync();
273
+
274
+ expect(callback).toHaveBeenCalledTimes(2);
275
+ expect(callback).toHaveBeenCalledWith({
276
+ layerIndex: 3,
277
+ layerState: {
278
+ creationError: true,
279
+ creationErrorMessage:
280
+ 'Unrecognized layer type: {"type":"doesnt-exist"}',
281
+ },
282
+ type: "map-layer-state-change",
283
+ });
284
+ expect(callback).toHaveBeenCalledWith({
285
+ layerIndex: 2,
286
+ layerState: {
287
+ creationError: true,
288
+ creationErrorMessage:
289
+ 'Unrecognized layer type: {"type":"also-doesnt-exist"}',
290
+ },
291
+ type: "map-layer-state-change",
292
+ });
293
+ });
294
+ it("emits a status when everything worked well (context diff)", async () => {
295
+ await resetMapFromContext(map, {
296
+ layers: [MAP_CTX_LAYER_XYZ_FIXTURE],
297
+ view: null,
298
+ });
299
+ await vi.runAllTimersAsync();
300
+
301
+ expect(callback).toHaveBeenCalledOnce();
302
+ expect(callback).toHaveBeenCalledWith({
303
+ layerIndex: 0,
304
+ layerState: {
305
+ created: true,
306
+ loaded: true,
307
+ },
308
+ type: "map-layer-state-change",
309
+ });
310
+ });
311
+ it("emits a status when everything works well (context diff)", async () => {
312
+ await applyContextDiffToMap(map, {
313
+ layersAdded: [
314
+ {
315
+ layer: MAP_CTX_LAYER_WMTS_FIXTURE,
316
+ position: 3,
317
+ },
318
+ ],
319
+ layersRemoved: [],
320
+ layersReordered: [],
321
+ layersChanged: [
322
+ {
323
+ // the change here needs a recreation of the layer
324
+ layer: MAP_CTX_LAYER_WFS_FIXTURE,
325
+ previousLayer: MAP_CTX_LAYER_GEOJSON_FIXTURE,
326
+ position: 2,
327
+ },
328
+ ],
329
+ });
330
+ await vi.runAllTimersAsync();
331
+
332
+ expect(callback).toHaveBeenCalledTimes(2);
333
+ expect(callback).toHaveBeenCalledWith({
334
+ layerIndex: 3,
335
+ layerState: {
336
+ created: true,
337
+ loaded: true,
338
+ },
339
+ type: "map-layer-state-change",
340
+ });
341
+ expect(callback).toHaveBeenCalledWith({
342
+ layerIndex: 2,
343
+ layerState: {
344
+ created: true,
345
+ loaded: true,
346
+ },
347
+ type: "map-layer-state-change",
348
+ });
349
+ });
350
+ });
351
+
352
+ describe("layer loading & data info", () => {
353
+ it("transmits updated layer state as they update", async () => {
354
+ await resetMapFromContext(map, {
355
+ layers: [MAP_CTX_LAYER_XYZ_FIXTURE, MAP_CTX_LAYER_WMS_FIXTURE],
356
+ view: null,
357
+ });
358
+ await vi.runAllTimersAsync();
359
+ callback.mockClear();
360
+
361
+ const layer2 = map.getLayers().item(1);
362
+ layer2.dispatchEvent({
363
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
364
+ layerState: {
365
+ loading: true,
366
+ },
367
+ } as unknown as BaseEvent);
368
+ layer2.dispatchEvent({
369
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-data-info`,
370
+ layerState: {
371
+ geometryTypes: ["LineString"],
372
+ },
373
+ } as unknown as BaseEvent);
374
+ layer2.dispatchEvent({
375
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
376
+ layerState: {
377
+ loaded: true,
378
+ },
379
+ } as unknown as BaseEvent);
380
+ layer2.dispatchEvent({
381
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-data-info`,
382
+ layerState: {
383
+ featuresCount: 123,
384
+ },
385
+ } as unknown as BaseEvent);
386
+
387
+ expect(callback).toHaveBeenCalledTimes(4);
388
+ expect(callback).toHaveBeenCalledWith({
389
+ layerState: { created: true, loading: true },
390
+ layerIndex: 1,
391
+ type: "map-layer-state-change",
392
+ });
393
+ expect(callback).toHaveBeenCalledWith({
394
+ layerState: {
395
+ created: true,
396
+ loading: true,
397
+ geometryTypes: ["LineString"],
398
+ },
399
+ layerIndex: 1,
400
+ type: "map-layer-state-change",
401
+ });
402
+ expect(callback).toHaveBeenCalledWith({
403
+ layerState: {
404
+ created: true,
405
+ loaded: true,
406
+ geometryTypes: ["LineString"],
407
+ },
408
+ layerIndex: 1,
409
+ type: "map-layer-state-change",
410
+ });
411
+ expect(callback).toHaveBeenCalledWith({
412
+ layerState: {
413
+ created: true,
414
+ loaded: true,
415
+ geometryTypes: ["LineString"],
416
+ featuresCount: 123,
417
+ },
418
+ layerIndex: 1,
419
+ type: "map-layer-state-change",
420
+ });
421
+ });
422
+ });
423
+ });
424
+
425
+ describe("layer creation error", () => {
426
+ let callback: Mock;
427
+
428
+ beforeEach(() => {
429
+ callback = vi.fn();
430
+ listen(map, "layer-creation-error", callback);
431
+ });
432
+
433
+ it("emits an error if something goes wrong when applying a context", async () => {
434
+ await resetMapFromContext(map, {
435
+ layers: [{ type: "doesnt-exist" } as any],
436
+ view: null,
437
+ });
438
+ await vi.runAllTimersAsync();
439
+
440
+ expect(callback).toHaveBeenCalledOnce();
441
+ expect(callback).toHaveBeenCalledWith({
442
+ error: new Error('Unrecognized layer type: {"type":"doesnt-exist"}'),
443
+ type: "layer-creation-error",
444
+ });
445
+ });
446
+ it("emits an error if something goes wrong when applying a context diff", async () => {
447
+ await applyContextDiffToMap(map, {
448
+ layersAdded: [
449
+ {
450
+ layer: { type: "doesnt-exist" } as any,
451
+ position: 3,
452
+ },
453
+ ],
454
+ layersRemoved: [],
455
+ layersReordered: [],
456
+ layersChanged: [
457
+ {
458
+ // the change here needs a recreation of the layer
459
+ layer: { type: "also-doesnt-exist" } as any,
460
+ previousLayer: MAP_CTX_LAYER_GEOJSON_FIXTURE,
461
+ position: 2,
462
+ },
463
+ ],
464
+ });
465
+ await vi.runAllTimersAsync();
466
+
467
+ expect(callback).toHaveBeenCalledTimes(2);
468
+ expect(callback).toHaveBeenCalledWith({
469
+ error: new Error('Unrecognized layer type: {"type":"doesnt-exist"}'),
470
+ type: "layer-creation-error",
471
+ });
472
+ expect(callback).toHaveBeenCalledWith({
473
+ error: new Error(
474
+ 'Unrecognized layer type: {"type":"also-doesnt-exist"}',
475
+ ),
476
+ type: "layer-creation-error",
477
+ });
478
+ });
479
+ });
480
+
481
+ describe("layer loading error", () => {
482
+ let errorEventCallback: Mock;
483
+ let mapStateCallback: Mock;
484
+ let sourceLoadErrorCallback: Mock;
485
+ let fetchMock: Mock;
486
+ let layer: Layer;
487
+ const mockCanvas = document.createElement("canvas");
488
+
489
+ const getFrameState = (layer: Layer): FrameState => {
490
+ const frameState = {
491
+ animate: false,
492
+ coordinateToPixelTransform: [1, 0, 0, 1, 0, 0],
493
+ declutterItems: [],
494
+ extent: [
495
+ -696165.0132013096, 5090855.383524774, 3367832.7922398755,
496
+ 7122854.286245367,
497
+ ],
498
+ index: 0,
499
+ layerIndex: 0,
500
+ pixelRatio: 1,
501
+ pixelToCoordinateTransform: [1, 0, 0, 1, 0, 0],
502
+ postRenderFunctions: [],
503
+ size: [800, 400],
504
+ time: 1604056713131,
505
+ usedTiles: {},
506
+ wantedTiles: {},
507
+ viewHints: [0, 0],
508
+ viewState: {
509
+ center: [0, 0],
510
+ resolution: 5079.997256801481,
511
+ projection: getProjection("EPSG:3857"),
512
+ rotation: 0,
513
+ },
514
+ layerStatesArray: [
515
+ {
516
+ layer,
517
+ managed: true,
518
+ maxResolution: null,
519
+ maxZoom: null,
520
+ minResolution: 0,
521
+ minZoom: null,
522
+ opacity: 1,
523
+ sourceState: "",
524
+ visible: true,
525
+ zIndex: 0,
526
+ },
527
+ ],
528
+ tileQueue: new TileQueue(
529
+ (tile, tileSourceKey, tileCenter, tileResolution) =>
530
+ getTilePriority(
531
+ frameState,
532
+ tile,
533
+ tileSourceKey,
534
+ tileCenter,
535
+ tileResolution,
536
+ ),
537
+ () => {},
538
+ ),
539
+ } as unknown as FrameState;
540
+ return frameState;
541
+ };
542
+
543
+ beforeAll(() => {
544
+ fetchMock = vi
545
+ .spyOn(globalThis, "fetch")
546
+ .mockImplementation((input, _options) => {
547
+ if (input.toString().includes("give.me/network/error")) {
548
+ return Promise.reject(new Error("Network error"));
549
+ }
550
+ if (input.toString().includes("give.me/http/error")) {
551
+ return Promise.resolve(
552
+ new Response("An error happened on this tile", {
553
+ status: 403,
554
+ statusText: "Some HTTP error",
555
+ }),
556
+ );
557
+ }
558
+ return Promise.resolve(
559
+ new Response("fake image blob", { status: 200 }),
560
+ );
561
+ });
562
+ });
563
+
564
+ afterAll(() => {
565
+ fetchMock.mockRestore();
566
+ });
567
+
568
+ beforeEach(() => {
569
+ errorEventCallback = vi.fn();
570
+ mapStateCallback = vi.fn();
571
+ sourceLoadErrorCallback = vi.fn();
572
+ listen(map, "layer-loading-error", errorEventCallback);
573
+ listen(map, "map-state-change", mapStateCallback);
574
+ listen(map, "source-load-error", sourceLoadErrorCallback);
575
+ });
576
+
577
+ describe("WMTS endpoint error", () => {
578
+ beforeEach(async () => {
579
+ layer = await createLayer({
580
+ type: "wmts",
581
+ url: "http://give.me/error",
582
+ name: "abcd",
583
+ });
584
+ map.getLayers().setAt(0, layer);
585
+ propagateLayerStateChangeEventToMap(map, layer);
586
+ await vi.runAllTimersAsync();
587
+ });
588
+ it("emits an error", async () => {
589
+ expect(errorEventCallback).toHaveBeenCalledOnce();
590
+ expect(errorEventCallback).toHaveBeenCalledWith({
591
+ error: new EndpointError("Something went wrong", 305),
592
+ type: "layer-loading-error",
593
+ httpStatus: 305,
594
+ });
595
+ });
596
+ it("updates the map state accordingly", async () => {
597
+ expect(mapStateCallback).toHaveBeenLastCalledWith({
598
+ type: "map-state-change",
599
+ mapState: {
600
+ layers: [
601
+ {
602
+ created: true,
603
+ loadingError: true,
604
+ loadingErrorHttpStatus: 305,
605
+ loadingErrorMessage: "Something went wrong",
606
+ },
607
+ null,
608
+ null,
609
+ ],
610
+ view: null,
611
+ },
612
+ });
613
+ });
614
+ it("DEPRECATED: emits a source load error", async () => {
615
+ expect(sourceLoadErrorCallback).toHaveBeenCalledOnce();
616
+ const mockSourceLoadError = {
617
+ message: "Something went wrong",
618
+ httpStatus: 305,
619
+ };
620
+ expect(sourceLoadErrorCallback).toHaveBeenCalledWith(
621
+ expect.objectContaining(mockSourceLoadError),
622
+ );
623
+ });
624
+ });
625
+
626
+ describe("WFS endpoint error", () => {
627
+ beforeEach(async () => {
628
+ layer = await createLayer({
629
+ type: "wfs",
630
+ url: "http://give.me/error",
631
+ featureType: "abcd",
632
+ });
633
+ map.getLayers().setAt(0, layer);
634
+ propagateLayerStateChangeEventToMap(map, layer);
635
+ await vi.runAllTimersAsync();
636
+ });
637
+ it("emits an error", async () => {
638
+ expect(errorEventCallback).toHaveBeenCalledOnce();
639
+ expect(errorEventCallback).toHaveBeenCalledWith({
640
+ error: new EndpointError("Something went wrong", 305),
641
+ type: "layer-loading-error",
642
+ httpStatus: 305,
643
+ });
644
+ });
645
+ it("updates the map state accordingly", async () => {
646
+ expect(mapStateCallback).toHaveBeenLastCalledWith({
647
+ type: "map-state-change",
648
+ mapState: {
649
+ layers: [
650
+ {
651
+ created: true,
652
+ loadingError: true,
653
+ loadingErrorHttpStatus: 305,
654
+ loadingErrorMessage: "Something went wrong",
655
+ },
656
+ null,
657
+ null,
658
+ ],
659
+ view: null,
660
+ },
661
+ });
662
+ });
663
+ it("DEPRECATED: emits a source load error", async () => {
664
+ expect(sourceLoadErrorCallback).toHaveBeenCalledOnce();
665
+ const mockSourceLoadError = {
666
+ message: "Something went wrong",
667
+ httpStatus: 305,
668
+ };
669
+ expect(sourceLoadErrorCallback).toHaveBeenCalledWith(
670
+ expect.objectContaining(mockSourceLoadError),
671
+ );
672
+ });
673
+ });
674
+
675
+ describe("Tile loading HTTP error", () => {
676
+ beforeEach(async () => {
677
+ layer = await createLayer({
678
+ type: "xyz",
679
+ url: "http://give.me/http/error",
680
+ });
681
+ map.getLayers().setAt(0, layer);
682
+ propagateLayerStateChangeEventToMap(map, layer);
683
+ await vi.runAllTimersAsync();
684
+
685
+ // this will trigger tile loading
686
+ const frameState = getFrameState(layer);
687
+ layer.render(frameState, mockCanvas);
688
+ frameState.tileQueue.loadMoreTiles(1, 1);
689
+ layer.render(frameState, mockCanvas);
690
+ });
691
+ it("emits an error", async () => {
692
+ expect(errorEventCallback).toHaveBeenCalledOnce();
693
+ expect(errorEventCallback).toHaveBeenCalledWith({
694
+ error: new Error("An error happened on this tile"),
695
+ type: "layer-loading-error",
696
+ httpStatus: 403,
697
+ });
698
+ });
699
+ it("updates the map state accordingly", async () => {
700
+ expect(mapStateCallback).toHaveBeenLastCalledWith({
701
+ type: "map-state-change",
702
+ mapState: {
703
+ layers: [
704
+ {
705
+ created: true,
706
+ loadingError: true,
707
+ loadingErrorHttpStatus: 403,
708
+ loadingErrorMessage: "An error happened on this tile",
709
+ },
710
+ null,
711
+ null,
712
+ ],
713
+ view: null,
714
+ },
715
+ });
716
+ });
717
+ it("DEPRECATED: emits a source load error", async () => {
718
+ expect(sourceLoadErrorCallback).toHaveBeenCalledOnce();
719
+ const mockSourceLoadError = {
720
+ message: "An error happened on this tile",
721
+ httpStatus: 403,
722
+ };
723
+ expect(sourceLoadErrorCallback).toHaveBeenCalledWith(
724
+ expect.objectContaining(mockSourceLoadError),
725
+ );
726
+ });
727
+ });
728
+
729
+ describe("Tile loading network error", () => {
730
+ beforeEach(async () => {
731
+ layer = await createLayer({
732
+ type: "xyz",
733
+ url: "http://give.me/network/error",
734
+ });
735
+ map.getLayers().setAt(0, layer);
736
+ propagateLayerStateChangeEventToMap(map, layer);
737
+ await vi.runAllTimersAsync();
738
+
739
+ // this will trigger tile loading
740
+ const frameState = getFrameState(layer);
741
+ layer.render(frameState, mockCanvas);
742
+ frameState.tileQueue.loadMoreTiles(1, 1);
743
+ layer.render(frameState, mockCanvas);
744
+ });
745
+ it("emits an error", async () => {
746
+ expect(errorEventCallback).toHaveBeenCalledOnce();
747
+ expect(errorEventCallback).toHaveBeenCalledWith({
748
+ error: new Error("Network error"),
749
+ type: "layer-loading-error",
750
+ });
751
+ });
752
+ it("updates the map state accordingly", async () => {
753
+ expect(mapStateCallback).toHaveBeenLastCalledWith({
754
+ type: "map-state-change",
755
+ mapState: {
756
+ layers: [
757
+ {
758
+ created: true,
759
+ loadingError: true,
760
+ loadingErrorMessage: "Network error",
761
+ },
762
+ null,
763
+ null,
764
+ ],
765
+ view: null,
766
+ },
767
+ });
768
+ });
769
+ it("DEPRECATED: emits a source load error", async () => {
770
+ expect(sourceLoadErrorCallback).toHaveBeenCalledOnce();
771
+ const mockSourceLoadError = {
772
+ message: "Network error",
773
+ };
774
+ expect(sourceLoadErrorCallback).toHaveBeenCalledWith(
775
+ expect.objectContaining(mockSourceLoadError),
776
+ );
777
+ });
778
+ });
779
+ });
780
+
781
+ describe("global map state change", () => {
782
+ let callback: Mock;
783
+
784
+ beforeEach(async () => {
785
+ // we're skipping all the initial events in order to track only the ones we control
786
+ await vi.runAllTimersAsync();
787
+ callback = vi.fn();
788
+ listen(map, "map-state-change", callback);
789
+ });
790
+
791
+ it("emits an updated state after changes to layers and view", async () => {
792
+ map
793
+ .getLayers()
794
+ .item(0)
795
+ .dispatchEvent({
796
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
797
+ layerState: {
798
+ loading: true,
799
+ },
800
+ } as unknown as BaseEvent);
801
+ map
802
+ .getLayers()
803
+ .item(1)
804
+ .dispatchEvent({
805
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-data-info`,
806
+ layerState: {
807
+ featuresCount: 123,
808
+ },
809
+ } as unknown as BaseEvent);
810
+ map.getView().setCenter([0, 0]);
811
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
812
+
813
+ expect(callback).toHaveBeenCalledTimes(3); // 1 per each layer + 1 for the view
814
+ expect(callback).toHaveBeenLastCalledWith({
815
+ type: "map-state-change",
816
+ mapState: {
817
+ layers: [
818
+ {
819
+ created: true,
820
+ loading: true,
821
+ },
822
+ {
823
+ created: true,
824
+ featuresCount: 123,
825
+ loaded: true,
826
+ },
827
+ null,
828
+ ],
829
+ view: {
830
+ bearing: 90,
831
+ center: [0, 0],
832
+ extent: [
833
+ -0.0035932611364780857, -0.0026949458513598756,
834
+ 0.0035932611364780857, 0.0026949458513740865,
835
+ ],
836
+ resolution: 1,
837
+ scaleDenominator: 3571.428571428571,
838
+ },
839
+ },
840
+ });
841
+ });
842
+
843
+ it("emits a new state when layers are deleted/changed", async () => {
844
+ await applyContextDiffToMap(map, {
845
+ layersAdded: [],
846
+ layersRemoved: [
847
+ {
848
+ layer: MAP_CTX_LAYER_WMTS_FIXTURE,
849
+ position: 2,
850
+ },
851
+ ],
852
+ layersReordered: [],
853
+ layersChanged: [
854
+ {
855
+ layer: MAP_CTX_LAYER_WFS_FIXTURE,
856
+ previousLayer: MAP_CTX_LAYER_GEOJSON_FIXTURE,
857
+ position: 1,
858
+ },
859
+ ],
860
+ });
861
+ await vi.runAllTimersAsync();
862
+
863
+ expect(callback).toHaveBeenLastCalledWith({
864
+ type: "map-state-change",
865
+ mapState: {
866
+ layers: [
867
+ null,
868
+ {
869
+ // this is the new WFS layer
870
+ created: true,
871
+ loaded: true,
872
+ },
873
+ ],
874
+ view: null,
875
+ },
876
+ });
877
+ });
878
+ });
879
+
880
+ describe("map extent change event (deprecated)", () => {
881
+ let callback: Mock;
882
+
883
+ beforeEach(() => {
884
+ callback = vi.fn();
885
+ listen(map, "map-extent-change", callback);
886
+ });
887
+
888
+ it("should registers the event on the map when center changed", () => {
889
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
890
+
891
+ expect(callback).toHaveBeenCalledOnce();
892
+ expect(callback).toHaveBeenCalledWith({
893
+ type: "map-extent-change",
894
+ extent: EXPECTED_MAP_EXTENT_EPSG4326,
895
+ });
896
+ });
897
+
898
+ it("should registers the event on the map when resolution changed", () => {
899
+ map.getView().dispatchEvent(createMapEvent(map, "change:resolution"));
900
+
901
+ expect(callback).toHaveBeenCalledOnce();
902
+ expect(callback).toHaveBeenCalledWith({
903
+ type: "map-extent-change",
904
+ extent: EXPECTED_MAP_EXTENT_EPSG4326,
905
+ });
906
+ });
907
+
908
+ it("should registers the event on the map when rotation changed", () => {
909
+ map.getView().dispatchEvent(createMapEvent(map, "change:rotation"));
910
+
911
+ expect(callback).toHaveBeenCalledOnce();
912
+ expect(callback).toHaveBeenCalledWith({
913
+ type: "map-extent-change",
914
+ extent: EXPECTED_MAP_EXTENT_EPSG4326,
915
+ });
916
+ });
917
+
918
+ it("should registers the event on the map when size changed", () => {
919
+ map.dispatchEvent(createMapEvent(map, "change:size"));
920
+
921
+ expect(callback).toHaveBeenCalledOnce();
922
+ expect(callback).toHaveBeenCalledWith({
923
+ type: "map-extent-change",
924
+ extent: EXPECTED_MAP_EXTENT_EPSG4326,
925
+ });
926
+ });
927
+
928
+ it("should send map-extent-change only once when multiple events occur with same extent", () => {
929
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
930
+ map.getView().dispatchEvent(createMapEvent(map, "change:resolution"));
931
+ map.getView().dispatchEvent(createMapEvent(map, "change:rotation"));
932
+ map.dispatchEvent(createMapEvent(map, "change:size"));
933
+
934
+ expect(callback).toHaveBeenCalledOnce();
935
+ expect(callback).toHaveBeenCalledWith({
936
+ type: "map-extent-change",
937
+ extent: EXPECTED_MAP_EXTENT_EPSG4326,
938
+ });
939
+ });
940
+
941
+ it("should send map-extent-change twice when view properties actually change", () => {
942
+ map.getView().setCenter([1000, 1000]);
943
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
944
+
945
+ map.getView().setResolution(2);
946
+ map.getView().dispatchEvent(createMapEvent(map, "change:resolution"));
947
+
948
+ expect(callback).toHaveBeenCalledTimes(2);
949
+ expect(callback).toHaveBeenCalledWith(
950
+ expect.objectContaining({
951
+ type: "map-extent-change",
952
+ extent: expect.any(Array),
953
+ }),
954
+ );
955
+
956
+ const firstCall = callback.mock.calls[0][0];
957
+ const lastCall = callback.mock.calls[1][0];
958
+ expect(firstCall.extent).not.toEqual(lastCall.extent);
959
+ });
960
+
961
+ it("should send map-extent-change only once when same view property is set multiple times to same value", () => {
962
+ map.getView().setCenter([1000, 1000]);
963
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
964
+
965
+ map.getView().setCenter([1000, 1000]);
966
+ map.getView().dispatchEvent(createMapEvent(map, "change:center"));
967
+
968
+ expect(callback).toHaveBeenCalledTimes(1);
969
+ expect(callback).toHaveBeenCalledWith(
970
+ expect.objectContaining({
971
+ type: "map-extent-change",
972
+ extent: expect.any(Array),
973
+ }),
974
+ );
975
+ });
976
+ });
977
+ });