@geospatial-sdk/maplibre 0.0.5-dev.48 → 0.0.5-dev.50

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.
@@ -1,26 +1,83 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { MapContextDiff } from "@geospatial-sdk/core";
1
+ import type { Map as MapLibreMap, RasterLayerSpecification } from "maplibre-gl";
3
2
  import {
3
+ MapContextDiff,
4
+ MapContextLayer,
5
+ MapContextLayerGeojson,
6
+ } from "@geospatial-sdk/core";
7
+ import {
8
+ SAMPLE_CONTEXT,
4
9
  SAMPLE_LAYER1,
5
10
  SAMPLE_LAYER2,
6
11
  SAMPLE_LAYER3,
7
12
  SAMPLE_LAYER4,
13
+ SAMPLE_LAYER5,
8
14
  } from "@geospatial-sdk/core/fixtures/map-context.fixtures.js";
9
15
  import { applyContextDiffToMap } from "./apply-context-diff.js";
10
- import * as mapHelpers from "../helpers/map.helpers.js";
16
+ import { generateLayerHashWithoutUpdatableProps } from "../helpers/map.helpers.js";
17
+ import { resetMapFromContext } from "./create-map.js";
18
+ import { MockInstance } from "vitest";
11
19
 
12
20
  // Helper to create a fresh mock Map instance for each test
13
- function createMockMap() {
21
+ function createMockMap(): MapLibreMap {
22
+ const state = {
23
+ layers: [] as any[],
24
+ source: {} as any,
25
+ };
14
26
  return {
15
- addLayer: vi.fn(),
16
- addSource: vi.fn(),
17
- removeLayer: vi.fn(),
18
- removeSource: vi.fn(),
27
+ addLayer: vi.fn((layer, beforeId) => {
28
+ if (beforeId) {
29
+ const beforeIndex = state.layers.findIndex(
30
+ (layer) => layer.id === beforeId,
31
+ );
32
+ if (beforeIndex === -1) {
33
+ throw new Error(
34
+ "Could not add before non existent layer: " + beforeId,
35
+ );
36
+ }
37
+ state.layers.splice(beforeIndex, 0, layer);
38
+ } else {
39
+ state.layers.push(layer);
40
+ }
41
+ }),
42
+ addSource: vi.fn((sourceId, source) => {
43
+ state.source[sourceId] = source;
44
+ }),
45
+ removeLayer: vi.fn((layerId) => {
46
+ state.layers = state.layers.filter((layer) => layer.id !== layerId);
47
+ }),
48
+ removeSource: vi.fn((sourceId) => {
49
+ delete state.source[sourceId];
50
+ }),
19
51
  setZoom: vi.fn(),
20
52
  setCenter: vi.fn(),
21
53
  fitBounds: vi.fn(),
22
- getStyle: vi.fn(() => ({ layers: [], sources: {} })),
23
- };
54
+ getStyle: vi.fn(() => state),
55
+ moveLayer: vi.fn((layerId, beforeId) => {
56
+ const layerIndex = state.layers.findIndex(
57
+ (layer) => layer.id === layerId,
58
+ );
59
+ const layer = state.layers[layerIndex];
60
+ state.layers.splice(layerIndex, 1);
61
+ if (beforeId) {
62
+ const beforeIndex = state.layers.findIndex(
63
+ (layer) => layer.id === beforeId,
64
+ );
65
+ if (beforeIndex === -1) {
66
+ throw new Error(
67
+ "Could not move before non existent layer: " + beforeId,
68
+ );
69
+ }
70
+ state.layers.splice(beforeIndex, 0, layer);
71
+ } else {
72
+ state.layers.push(layer);
73
+ }
74
+ }),
75
+ getLayer: vi.fn((layerId) => {
76
+ return state.layers.find((layer) => layer.id === layerId);
77
+ }),
78
+ setLayoutProperty: vi.fn(),
79
+ setPaintProperty: vi.fn(),
80
+ } as unknown as MapLibreMap;
24
81
  }
25
82
 
26
83
  describe("applyContextDiffToMap (mocked Map)", () => {
@@ -29,7 +86,10 @@ describe("applyContextDiffToMap (mocked Map)", () => {
29
86
 
30
87
  beforeEach(async () => {
31
88
  map = createMockMap();
32
- map.getStyle.mockReturnValue({ layers: [], sources: {} });
89
+ });
90
+
91
+ afterEach(() => {
92
+ vi.clearAllMocks();
33
93
  });
34
94
 
35
95
  it("does not call any mutating methods for no change", async () => {
@@ -39,7 +99,7 @@ describe("applyContextDiffToMap (mocked Map)", () => {
39
99
  layersRemoved: [],
40
100
  layersReordered: [],
41
101
  };
42
- await applyContextDiffToMap(map as any, diff);
102
+ await applyContextDiffToMap(map, diff);
43
103
  expect(map.addLayer).not.toHaveBeenCalled();
44
104
  expect(map.addSource).not.toHaveBeenCalled();
45
105
  expect(map.removeLayer).not.toHaveBeenCalled();
@@ -56,15 +116,17 @@ describe("applyContextDiffToMap (mocked Map)", () => {
56
116
  layersRemoved: [],
57
117
  layersReordered: [],
58
118
  };
59
- await applyContextDiffToMap(map as any, diff);
119
+ await applyContextDiffToMap(map, diff);
60
120
  expect(map.addSource).toHaveBeenCalled();
61
121
  expect(map.addLayer).toHaveBeenCalled();
62
122
  });
63
123
 
64
124
  it("calls removeLayer and removeSource for layers removed", async () => {
65
- // Simulate getLayersAtPosition returning a mock layer with id/source
66
- const mockLayer = { id: "layerid", source: "sourceid" };
67
- vi.spyOn(mapHelpers, "getLayersAtPosition").mockReturnValue([mockLayer]);
125
+ const context = {
126
+ ...SAMPLE_CONTEXT,
127
+ layers: [SAMPLE_LAYER2, SAMPLE_LAYER1],
128
+ };
129
+ await resetMapFromContext(map, context);
68
130
  diff = {
69
131
  layersAdded: [],
70
132
  layersChanged: [],
@@ -74,52 +136,175 @@ describe("applyContextDiffToMap (mocked Map)", () => {
74
136
  ],
75
137
  layersReordered: [],
76
138
  };
77
- await applyContextDiffToMap(map as any, diff);
78
- expect(map.removeLayer).toHaveBeenCalledWith("layerid");
79
- expect(map.removeSource).toHaveBeenCalledWith("sourceid");
139
+ const layer1: RasterLayerSpecification = map.getStyle()
140
+ .layers[1] as RasterLayerSpecification;
141
+ const layer2: RasterLayerSpecification = map.getStyle()
142
+ .layers[0] as RasterLayerSpecification;
143
+ await applyContextDiffToMap(map, diff);
144
+ expect(map.removeLayer).toHaveBeenCalledWith(layer1.id);
145
+ expect(map.removeLayer).toHaveBeenCalledWith(layer2.id);
146
+ expect(map.removeSource).toHaveBeenCalledWith(layer1.source);
147
+ expect(map.removeSource).toHaveBeenCalledWith(layer2.source);
80
148
  });
81
149
 
82
- it("calls addLayer for changed layers", async () => {
83
- const generateLayerIdSpy = vi
84
- .spyOn(mapHelpers, "generateLayerId")
85
- .mockReturnValue("azreza");
86
- const removeLayersFromSourceSpy = vi.spyOn(
87
- mapHelpers,
88
- "removeLayersFromSource",
89
- );
150
+ describe("layer changes", () => {
151
+ let randomMock: MockInstance;
152
+ beforeEach(async () => {
153
+ const context = {
154
+ ...SAMPLE_CONTEXT,
155
+ layers: [SAMPLE_LAYER3, SAMPLE_LAYER1],
156
+ };
157
+ await resetMapFromContext(map, context);
158
+ vi.clearAllMocks();
90
159
 
91
- diff = {
92
- layersAdded: [],
93
- layersChanged: [
94
- { layer: { ...SAMPLE_LAYER3, url: "http://changed/" }, position: 0 },
95
- {
96
- layer: {
97
- ...SAMPLE_LAYER1,
98
- url: "http://changed/",
99
- extras: { changed: true },
100
- },
101
- position: 1,
160
+ // this is to get reliable source ids
161
+ let callCount = 0;
162
+ randomMock = vi.spyOn(Math, "random").mockImplementation(() => {
163
+ callCount++;
164
+ return 0.999 + 0.000001 * callCount;
165
+ });
166
+ });
167
+
168
+ afterEach(() => {
169
+ randomMock.mockRestore();
170
+ });
171
+
172
+ it("replace layers when non-updatable properties are changed", async () => {
173
+ const layer3Changed = {
174
+ ...SAMPLE_LAYER3,
175
+ data: '{ "type": "Feature", "properties": { "changed": true}}',
176
+ } as MapContextLayerGeojson;
177
+ const layer1Changed = {
178
+ ...SAMPLE_LAYER1,
179
+ url: "http://changed/",
180
+ extras: { changed: true },
181
+ };
182
+
183
+ diff = {
184
+ layersAdded: [],
185
+ layersChanged: [
186
+ {
187
+ layer: layer3Changed,
188
+ previousLayer: SAMPLE_LAYER3,
189
+ position: 0,
190
+ },
191
+ {
192
+ layer: layer1Changed,
193
+ previousLayer: SAMPLE_LAYER1,
194
+ position: 1,
195
+ },
196
+ ],
197
+ layersRemoved: [],
198
+ layersReordered: [],
199
+ };
200
+ await applyContextDiffToMap(map, diff);
201
+
202
+ expect(map.addLayer).toHaveBeenCalledTimes(4); // 3 for vector layer, 1 for raster layer
203
+ expect(map.removeLayer).toHaveBeenCalledTimes(4); // same as above
204
+ expect(map.addSource).toHaveBeenCalledTimes(2);
205
+ expect(map.removeSource).toHaveBeenCalledTimes(2);
206
+
207
+ expect(map.getStyle().layers.length).toBe(4);
208
+ expect(map.getStyle()).toEqual({
209
+ layers: [
210
+ expect.objectContaining({
211
+ source: "999002",
212
+ metadata: {
213
+ layerHash: generateLayerHashWithoutUpdatableProps(layer3Changed),
214
+ },
215
+ }),
216
+ expect.objectContaining({
217
+ source: "999002",
218
+ metadata: {
219
+ layerHash: generateLayerHashWithoutUpdatableProps(layer3Changed),
220
+ },
221
+ }),
222
+ expect.objectContaining({
223
+ source: "999002",
224
+ metadata: {
225
+ layerHash: generateLayerHashWithoutUpdatableProps(layer3Changed),
226
+ },
227
+ }),
228
+ expect.objectContaining({
229
+ source: "999003",
230
+ metadata: {
231
+ layerHash: generateLayerHashWithoutUpdatableProps(layer1Changed),
232
+ },
233
+ }),
234
+ ],
235
+ source: {
236
+ "999002": {
237
+ data: JSON.parse(layer3Changed.data as string),
238
+ type: "geojson",
239
+ },
240
+ "999003": expect.objectContaining({
241
+ tileSize: 256,
242
+ tiles: [
243
+ "https://www.datagrandest.fr/geoserver/region-grand-est/ows?REQUEST=GetMap&SERVICE=WMS&layers=commune_actuelle_3857&styles=&format=image%2Fpng&transparent=true&version=1.1.1&height=256&width=256&srs=EPSG%3A3857&BBOX={bbox-epsg-3857}",
244
+ ],
245
+ type: "raster",
246
+ }),
102
247
  },
103
- ],
104
- layersRemoved: [],
105
- layersReordered: [],
106
- };
107
- await applyContextDiffToMap(map as any, diff);
248
+ });
249
+ });
250
+ it("simply updates the layer if updatable properties are changed", async () => {
251
+ diff = {
252
+ layersAdded: [],
253
+ layersChanged: [
254
+ {
255
+ layer: {
256
+ ...SAMPLE_LAYER3,
257
+ visibility: false,
258
+ } as MapContextLayer,
259
+ previousLayer: SAMPLE_LAYER3,
260
+ position: 0,
261
+ },
262
+ {
263
+ layer: {
264
+ ...SAMPLE_LAYER1,
265
+ opacity: 0.9,
266
+ extras: { changed: true },
267
+ },
268
+ previousLayer: SAMPLE_LAYER1,
269
+ position: 1,
270
+ },
271
+ ],
272
+ layersRemoved: [],
273
+ layersReordered: [],
274
+ };
275
+ await applyContextDiffToMap(map, diff);
108
276
 
109
- try {
110
- expect(generateLayerIdSpy).toHaveBeenCalledWith(
111
- diff.layersChanged[1].layer,
112
- );
113
- expect(generateLayerIdSpy).toHaveBeenCalledWith(
114
- diff.layersChanged[0].layer,
115
- );
116
- expect(map.addLayer).toHaveBeenCalled();
117
- expect(removeLayersFromSourceSpy).toHaveBeenCalled();
118
- expect(removeLayersFromSourceSpy).toHaveBeenCalledTimes(2);
119
- } finally {
120
- generateLayerIdSpy.mockRestore();
121
- removeLayersFromSourceSpy.mockRestore();
122
- }
277
+ expect(map.setLayoutProperty).toHaveBeenCalledTimes(3); // 3 for vector layer
278
+ expect(map.setPaintProperty).toHaveBeenCalledTimes(1);
279
+ expect(map.addLayer).not.toHaveBeenCalled();
280
+ expect(map.removeLayer).not.toHaveBeenCalled();
281
+ expect(map.addSource).not.toHaveBeenCalled();
282
+ expect(map.removeSource).not.toHaveBeenCalled();
283
+
284
+ expect(map.getStyle().layers.length).toBe(4);
285
+ expect(map.getStyle().layers).toEqual([
286
+ expect.objectContaining({
287
+ metadata: {
288
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
289
+ },
290
+ }),
291
+ expect.objectContaining({
292
+ metadata: {
293
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
294
+ },
295
+ }),
296
+ expect.objectContaining({
297
+ metadata: {
298
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
299
+ },
300
+ }),
301
+ expect.objectContaining({
302
+ metadata: {
303
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER1),
304
+ },
305
+ }),
306
+ ]);
307
+ });
123
308
  });
124
309
 
125
310
  it("calls fitBounds for viewChanges with extent", async () => {
@@ -130,7 +315,7 @@ describe("applyContextDiffToMap (mocked Map)", () => {
130
315
  layersReordered: [],
131
316
  viewChanges: { extent: [-10, -10, 20, 20] },
132
317
  };
133
- await applyContextDiffToMap(map as any, diff);
318
+ await applyContextDiffToMap(map, diff);
134
319
  expect(map.fitBounds).toHaveBeenCalledWith(
135
320
  [
136
321
  [-10, -10],
@@ -139,4 +324,237 @@ describe("applyContextDiffToMap (mocked Map)", () => {
139
324
  expect.objectContaining({ padding: 20, duration: 1000 }),
140
325
  );
141
326
  });
327
+
328
+ describe("reordering", () => {
329
+ describe("2 layers inverted", () => {
330
+ beforeEach(async () => {
331
+ const context = {
332
+ ...SAMPLE_CONTEXT,
333
+ layers: [SAMPLE_LAYER1, SAMPLE_LAYER3, SAMPLE_LAYER2],
334
+ };
335
+ await resetMapFromContext(map, context);
336
+ vi.clearAllMocks();
337
+
338
+ diff = {
339
+ layersAdded: [],
340
+ layersChanged: [],
341
+ layersRemoved: [],
342
+ layersReordered: [
343
+ {
344
+ layer: SAMPLE_LAYER2,
345
+ newPosition: 0,
346
+ previousPosition: 2,
347
+ },
348
+ {
349
+ layer: SAMPLE_LAYER1,
350
+ newPosition: 2,
351
+ previousPosition: 0,
352
+ },
353
+ ],
354
+ };
355
+ await applyContextDiffToMap(map, diff);
356
+ });
357
+ it("moves the layers accordingly", () => {
358
+ expect(map.getStyle().layers.length).toBe(5);
359
+ expect(map.getStyle().layers).toEqual([
360
+ expect.objectContaining({
361
+ metadata: {
362
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER2),
363
+ },
364
+ }),
365
+ expect.objectContaining({
366
+ metadata: {
367
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
368
+ },
369
+ }),
370
+ expect.objectContaining({
371
+ metadata: {
372
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
373
+ },
374
+ }),
375
+ expect.objectContaining({
376
+ metadata: {
377
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
378
+ },
379
+ }),
380
+ expect.objectContaining({
381
+ metadata: {
382
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER1),
383
+ },
384
+ }),
385
+ ]);
386
+ });
387
+ });
388
+ describe("4 layers moved", () => {
389
+ let layer1WithId: MapContextLayer;
390
+
391
+ beforeEach(async () => {
392
+ layer1WithId = {
393
+ ...SAMPLE_LAYER1,
394
+ id: "layer-1-id",
395
+ };
396
+ const context = {
397
+ ...SAMPLE_CONTEXT,
398
+ layers: [layer1WithId, SAMPLE_LAYER3, SAMPLE_LAYER4, SAMPLE_LAYER2],
399
+ };
400
+ await resetMapFromContext(map, context);
401
+ vi.clearAllMocks();
402
+
403
+ diff = {
404
+ layersAdded: [],
405
+ layersChanged: [],
406
+ layersRemoved: [],
407
+ layersReordered: [
408
+ {
409
+ layer: SAMPLE_LAYER3,
410
+ newPosition: 3,
411
+ previousPosition: 1,
412
+ },
413
+ {
414
+ layer: SAMPLE_LAYER2,
415
+ newPosition: 2,
416
+ previousPosition: 3,
417
+ },
418
+ {
419
+ layer: layer1WithId,
420
+ newPosition: 1,
421
+ previousPosition: 0,
422
+ },
423
+ {
424
+ layer: SAMPLE_LAYER4,
425
+ newPosition: 0,
426
+ previousPosition: 2,
427
+ },
428
+ ],
429
+ };
430
+ await applyContextDiffToMap(map, diff);
431
+ });
432
+ it("moves the layers accordingly", () => {
433
+ expect(map.getStyle().layers.length).toBe(8);
434
+ expect(map.getStyle().layers).toEqual([
435
+ expect.objectContaining({
436
+ metadata: {
437
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER4),
438
+ },
439
+ }),
440
+ expect.objectContaining({
441
+ metadata: {
442
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER4),
443
+ },
444
+ }),
445
+ expect.objectContaining({
446
+ metadata: {
447
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER4),
448
+ },
449
+ }),
450
+ expect.objectContaining({
451
+ metadata: { layerId: layer1WithId.id },
452
+ }),
453
+ expect.objectContaining({
454
+ metadata: {
455
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER2),
456
+ },
457
+ }),
458
+ expect.objectContaining({
459
+ metadata: {
460
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
461
+ },
462
+ }),
463
+ expect.objectContaining({
464
+ metadata: {
465
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
466
+ },
467
+ }),
468
+ expect.objectContaining({
469
+ metadata: {
470
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER3),
471
+ },
472
+ }),
473
+ ]);
474
+ });
475
+ });
476
+ });
477
+
478
+ describe("combined changes", () => {
479
+ let changedLayer: MapContextLayer;
480
+ beforeEach(async () => {
481
+ changedLayer = { ...SAMPLE_LAYER3, extras: { prop: true } };
482
+ const context = {
483
+ ...SAMPLE_CONTEXT,
484
+ layers: [SAMPLE_LAYER1, SAMPLE_LAYER5, SAMPLE_LAYER3, SAMPLE_LAYER4],
485
+ };
486
+ await resetMapFromContext(map, context);
487
+ vi.clearAllMocks();
488
+
489
+ diff = {
490
+ layersAdded: [
491
+ {
492
+ layer: SAMPLE_LAYER2,
493
+ position: 0,
494
+ },
495
+ ],
496
+ layersChanged: [
497
+ {
498
+ layer: changedLayer,
499
+ previousLayer: SAMPLE_LAYER3,
500
+ position: 1,
501
+ },
502
+ ],
503
+ layersRemoved: [
504
+ {
505
+ layer: SAMPLE_LAYER1,
506
+ position: 0,
507
+ },
508
+ {
509
+ layer: SAMPLE_LAYER4,
510
+ position: 3,
511
+ },
512
+ ],
513
+ layersReordered: [
514
+ {
515
+ layer: changedLayer,
516
+ newPosition: 1,
517
+ previousPosition: 2,
518
+ },
519
+ {
520
+ layer: SAMPLE_LAYER5,
521
+ newPosition: 2,
522
+ previousPosition: 1,
523
+ },
524
+ ],
525
+ };
526
+ await applyContextDiffToMap(map, diff);
527
+ });
528
+
529
+ it("moves the layers accordingly", () => {
530
+ expect(map.getStyle().layers.length).toBe(5);
531
+ expect(map.getStyle().layers).toEqual([
532
+ expect.objectContaining({
533
+ metadata: {
534
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER2),
535
+ },
536
+ }),
537
+ expect.objectContaining({
538
+ metadata: {
539
+ layerHash: generateLayerHashWithoutUpdatableProps(changedLayer),
540
+ },
541
+ }),
542
+ expect.objectContaining({
543
+ metadata: {
544
+ layerHash: generateLayerHashWithoutUpdatableProps(changedLayer),
545
+ },
546
+ }),
547
+ expect.objectContaining({
548
+ metadata: {
549
+ layerHash: generateLayerHashWithoutUpdatableProps(changedLayer),
550
+ },
551
+ }),
552
+ expect.objectContaining({
553
+ metadata: {
554
+ layerHash: generateLayerHashWithoutUpdatableProps(SAMPLE_LAYER5),
555
+ },
556
+ }),
557
+ ]);
558
+ });
559
+ });
142
560
  });