@geospatial-sdk/openlayers 0.0.5-dev.55 → 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 (37) 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 +56 -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 +4 -0
  16. package/dist/map/listen.d.ts.map +1 -0
  17. package/dist/map/listen.js +70 -0
  18. package/dist/map/register-events.d.ts +16 -2
  19. package/dist/map/register-events.d.ts.map +1 -1
  20. package/dist/map/register-events.js +172 -81
  21. package/dist/map/resolved-map-state.d.ts +8 -0
  22. package/dist/map/resolved-map-state.d.ts.map +1 -0
  23. package/dist/map/resolved-map-state.js +26 -0
  24. package/lib/map/apply-context-diff.ts +16 -5
  25. package/lib/map/create-map.test.ts +127 -38
  26. package/lib/map/create-map.ts +82 -37
  27. package/lib/map/feature-hover.ts +1 -1
  28. package/lib/map/handle-errors.test.ts +13 -36
  29. package/lib/map/handle-errors.ts +10 -28
  30. package/lib/map/index.ts +2 -1
  31. package/lib/map/layer-update.ts +3 -2
  32. package/lib/map/listen.test.ts +977 -0
  33. package/lib/map/listen.ts +123 -0
  34. package/lib/map/register-events.ts +229 -109
  35. package/lib/map/resolved-map-state.ts +38 -0
  36. package/package.json +3 -3
  37. package/lib/map/register-events.test.ts +0 -259
@@ -7,9 +7,9 @@ import {
7
7
  import {
8
8
  MAP_CTX_EXTENT_FIXTURE,
9
9
  MAP_CTX_FIXTURE,
10
- MAP_CTX_LAYER_GEOTIFF_FIXTURE,
11
10
  MAP_CTX_LAYER_GEOJSON_FIXTURE,
12
11
  MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE,
12
+ MAP_CTX_LAYER_GEOTIFF_FIXTURE,
13
13
  MAP_CTX_LAYER_MAPBLIBRE_STYLE_FIXTURE,
14
14
  MAP_CTX_LAYER_MVT_FIXTURE,
15
15
  MAP_CTX_LAYER_OGCAPI_FIXTURE,
@@ -44,10 +44,8 @@ import {
44
44
  createView,
45
45
  resetMapFromContext,
46
46
  } from "./create-map.js";
47
- import {
48
- handleEndpointError,
49
- tileLoadErrorCatchFunction,
50
- } from "./handle-errors.js";
47
+ import { tileLoadErrorCatchFunction } from "./handle-errors.js";
48
+ import { GEOSPATIAL_SDK_PREFIX } from "./constants.js";
51
49
 
52
50
  vi.mock("./handle-errors", async (importOriginal) => {
53
51
  const actual =
@@ -59,18 +57,22 @@ vi.mock("./handle-errors", async (importOriginal) => {
59
57
  };
60
58
  });
61
59
 
62
- afterEach(() => {
60
+ beforeEach(() => {
61
+ vi.useFakeTimers();
63
62
  vi.clearAllMocks();
64
63
  });
65
64
 
66
65
  describe("MapContextService", () => {
67
66
  describe("#createLayer", () => {
68
67
  let layerModel: MapContextLayer, layer: Layer;
68
+ const eventCallback = vi.fn();
69
69
 
70
70
  describe("XYZ", () => {
71
71
  beforeEach(async () => {
72
72
  layerModel = MAP_CTX_LAYER_XYZ_FIXTURE;
73
73
  layer = await createLayer(layerModel);
74
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
75
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
74
76
  });
75
77
  it("create a tile layer", () => {
76
78
  expect(layer).toBeTruthy();
@@ -108,13 +110,25 @@ describe("MapContextService", () => {
108
110
  tileLoadFunction(tile, "http://example.com/tile");
109
111
  expect(tileLoadErrorCatchFunction).toHaveBeenCalled();
110
112
  });
113
+ it("emits a loaded event initially", async () => {
114
+ await vi.runAllTimersAsync();
115
+ expect(eventCallback).toHaveBeenCalledWith({
116
+ layerState: {
117
+ loaded: true,
118
+ },
119
+ target: layer,
120
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
121
+ });
122
+ });
111
123
  });
112
- describe("OGCAPI", () => {
124
+ describe("OGCAPI (as geojson items)", () => {
113
125
  beforeEach(async () => {
114
126
  layerModel = MAP_CTX_LAYER_OGCAPI_FIXTURE;
115
127
  layer = await createLayer(layerModel);
128
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
129
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
116
130
  });
117
- it("create a vector tile layer", () => {
131
+ it("create a vector layer", () => {
118
132
  expect(layer).toBeTruthy();
119
133
  expect(layer).toBeInstanceOf(VectorLayer);
120
134
  });
@@ -124,7 +138,7 @@ describe("MapContextService", () => {
124
138
  expect(layer.get("label")).toBeUndefined();
125
139
  expect(layer.getSource()?.getAttributions()).toBeNull();
126
140
  });
127
- it("create a OGCVectorTile source", () => {
141
+ it("create a vector source", () => {
128
142
  const source = layer.getSource();
129
143
  expect(source).toBeInstanceOf(VectorSource);
130
144
  });
@@ -135,11 +149,23 @@ describe("MapContextService", () => {
135
149
  "https://demo.ldproxy.net/zoomstack/collections/airports/items?f=json",
136
150
  );
137
151
  });
152
+ it("emits a loaded event initially", async () => {
153
+ await vi.runAllTimersAsync();
154
+ expect(eventCallback).toHaveBeenCalledWith({
155
+ layerState: {
156
+ loaded: true,
157
+ },
158
+ target: layer,
159
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
160
+ });
161
+ });
138
162
  });
139
163
  describe("WMS", () => {
140
164
  beforeEach(async () => {
141
- (layerModel = MAP_CTX_LAYER_WMS_FIXTURE),
142
- (layer = await createLayer(layerModel));
165
+ layerModel = MAP_CTX_LAYER_WMS_FIXTURE;
166
+ layer = await createLayer(layerModel);
167
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
168
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
143
169
  });
144
170
  it("create a tile layer", () => {
145
171
  expect(layer).toBeTruthy();
@@ -189,12 +215,24 @@ describe("MapContextService", () => {
189
215
  tileLoadFunction(tile, "http://example.com/tile");
190
216
  expect(tileLoadErrorCatchFunction).toHaveBeenCalled();
191
217
  });
218
+ it("emits a loaded event initially", async () => {
219
+ await vi.runAllTimersAsync();
220
+ expect(eventCallback).toHaveBeenCalledWith({
221
+ layerState: {
222
+ loaded: true,
223
+ },
224
+ target: layer,
225
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
226
+ });
227
+ });
192
228
  });
193
229
 
194
230
  describe("WFS", () => {
195
231
  beforeEach(async () => {
196
- (layerModel = MAP_CTX_LAYER_WFS_FIXTURE),
197
- (layer = await createLayer(layerModel));
232
+ layerModel = MAP_CTX_LAYER_WFS_FIXTURE;
233
+ layer = await createLayer(layerModel);
234
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
235
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
198
236
  });
199
237
  it("create a vector layer", () => {
200
238
  expect(layer).toBeTruthy();
@@ -224,17 +262,15 @@ describe("MapContextService", () => {
224
262
  "https://www.datagrandest.fr/geoserver/region-grand-est/ows?service=WFS&version=1.1.0&request=GetFeature&outputFormat=application%2Fjson&typename=ms%3Acommune_actuelle_3857&srsname=EPSG%3A3857&bbox=10%2C20%2C30%2C40%2CEPSG%3A3857&maxFeatures=10000",
225
263
  );
226
264
  });
227
- it("should NOT call handleEndpointError", () => {
228
- expect(handleEndpointError).not.toHaveBeenCalled();
229
- });
230
- });
231
- describe("WFS error", () => {
232
- beforeEach(async () => {
233
- layerModel = { ...MAP_CTX_LAYER_WFS_FIXTURE, url: "https://wfs/error" };
234
- layer = await createLayer(layerModel);
235
- });
236
- it("should call handleEndpointError", () => {
237
- expect(handleEndpointError).toHaveBeenCalled();
265
+ it("emits a loaded event initially", async () => {
266
+ await vi.runAllTimersAsync();
267
+ expect(eventCallback).toHaveBeenCalledWith({
268
+ layerState: {
269
+ loaded: true,
270
+ },
271
+ target: layer,
272
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
273
+ });
238
274
  });
239
275
  });
240
276
 
@@ -243,6 +279,11 @@ describe("MapContextService", () => {
243
279
  beforeEach(async () => {
244
280
  layerModel = MAP_CTX_LAYER_GEOJSON_FIXTURE;
245
281
  layer = await createLayer(layerModel);
282
+ layer.on(
283
+ `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
284
+ eventCallback,
285
+ );
286
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
246
287
  });
247
288
  it("create a VectorLayer", () => {
248
289
  expect(layer).toBeTruthy();
@@ -264,6 +305,16 @@ describe("MapContextService", () => {
264
305
  .data as FeatureCollection;
265
306
  expect(features.length).toBe(data.features.length);
266
307
  });
308
+ it("emits a loaded event", async () => {
309
+ await vi.runAllTimersAsync();
310
+ expect(eventCallback).toHaveBeenCalledWith({
311
+ layerState: {
312
+ loaded: true,
313
+ },
314
+ target: layer,
315
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
316
+ });
317
+ });
267
318
  });
268
319
  describe("with inline data as string", () => {
269
320
  beforeEach(async () => {
@@ -316,6 +367,11 @@ describe("MapContextService", () => {
316
367
  beforeEach(async () => {
317
368
  layerModel = MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE;
318
369
  layer = await createLayer(layerModel);
370
+ layer.on(
371
+ `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
372
+ eventCallback,
373
+ );
374
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
319
375
  });
320
376
  it("create a VectorLayer", () => {
321
377
  expect(layer).toBeTruthy();
@@ -335,6 +391,16 @@ describe("MapContextService", () => {
335
391
  (layerModel as MapContextLayerGeojson).url,
336
392
  );
337
393
  });
394
+ it("emits a loaded event as well as data info eventually", async () => {
395
+ await vi.runAllTimersAsync();
396
+ expect(eventCallback).toHaveBeenCalledWith({
397
+ layerState: {
398
+ loaded: true,
399
+ },
400
+ target: layer,
401
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
402
+ });
403
+ });
338
404
  });
339
405
  });
340
406
 
@@ -342,6 +408,8 @@ describe("MapContextService", () => {
342
408
  beforeEach(async () => {
343
409
  layerModel = MAP_CTX_LAYER_WMTS_FIXTURE;
344
410
  layer = await createLayer(layerModel);
411
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
412
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
345
413
  });
346
414
  it("create a tile layer", () => {
347
415
  expect(layer).toBeTruthy();
@@ -366,20 +434,15 @@ describe("MapContextService", () => {
366
434
  "https://services.geo.sg.ch/wss/service/SG00066_WMTS/guest/tile/1.0.0/SG00066/{Style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}",
367
435
  ]);
368
436
  });
369
- it("should NOT call handleEndpointError", () => {
370
- expect(handleEndpointError).not.toHaveBeenCalled();
371
- });
372
- });
373
- describe("WMTS error", () => {
374
- beforeEach(async () => {
375
- layerModel = {
376
- ...MAP_CTX_LAYER_WMTS_FIXTURE,
377
- url: "https://wmts/error",
378
- };
379
- layer = await createLayer(layerModel);
380
- });
381
- it("should call handleEndpointError", () => {
382
- expect(handleEndpointError).toHaveBeenCalled();
437
+ it("emits a loaded event initially", async () => {
438
+ await vi.runAllTimersAsync();
439
+ expect(eventCallback).toHaveBeenCalledWith({
440
+ layerState: {
441
+ loaded: true,
442
+ },
443
+ target: layer,
444
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
445
+ });
383
446
  });
384
447
  });
385
448
  describe("WMTS without default style given", () => {
@@ -389,6 +452,8 @@ describe("MapContextService", () => {
389
452
  url: "https://wmts/no-default-style",
390
453
  };
391
454
  layer = await createLayer(layerModel);
455
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
456
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
392
457
  });
393
458
  it("uses the first style as default", async () => {
394
459
  const source = layer.getSource() as WMTS;
@@ -400,6 +465,8 @@ describe("MapContextService", () => {
400
465
  beforeEach(async () => {
401
466
  layerModel = MAP_CTX_LAYER_MAPBLIBRE_STYLE_FIXTURE;
402
467
  layer = await createLayer(layerModel);
468
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
469
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
403
470
  });
404
471
  it("create a tile layer", () => {
405
472
  expect(layer).toBeTruthy();
@@ -414,12 +481,24 @@ describe("MapContextService", () => {
414
481
  const source = layer.getSource();
415
482
  expect(source).toBeInstanceOf(VectorTile);
416
483
  });
484
+ it("emits a loaded event initially", async () => {
485
+ await vi.runAllTimersAsync();
486
+ expect(eventCallback).toHaveBeenCalledWith({
487
+ layerState: {
488
+ loaded: true,
489
+ },
490
+ target: layer,
491
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
492
+ });
493
+ });
417
494
  });
418
495
 
419
496
  describe("MVT", () => {
420
497
  beforeEach(async () => {
421
498
  layerModel = MAP_CTX_LAYER_MVT_FIXTURE;
422
499
  layer = await createLayer(layerModel);
500
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-loading-status`, eventCallback);
501
+ layer.on(`${GEOSPATIAL_SDK_PREFIX}layer-data-info`, eventCallback);
423
502
  });
424
503
  it("create a VectorTileLayer", () => {
425
504
  expect(layer).toBeTruthy();
@@ -434,6 +513,16 @@ describe("MapContextService", () => {
434
513
  expect(layer.getOpacity()).toBe(1);
435
514
  expect(layer.get("label")).toBeUndefined();
436
515
  });
516
+ it("emits a loaded event initially", async () => {
517
+ await vi.runAllTimersAsync();
518
+ expect(eventCallback).toHaveBeenCalledWith({
519
+ layerState: {
520
+ loaded: true,
521
+ },
522
+ target: layer,
523
+ type: `${GEOSPATIAL_SDK_PREFIX}layer-loading-status`,
524
+ });
525
+ });
437
526
  });
438
527
 
439
528
  describe("GeoTIFF", () => {
@@ -25,16 +25,14 @@ import OGCVectorTile from "ol/source/OGCVectorTile.js";
25
25
  import WMTS from "ol/source/WMTS.js";
26
26
  import MVT from "ol/format/MVT.js";
27
27
  import {
28
+ EndpointError,
28
29
  OgcApiEndpoint,
29
30
  WfsEndpoint,
30
31
  WmtsEndpoint,
31
32
  } from "@camptocamp/ogc-client";
32
33
  import { MapboxVectorLayer } from "ol-mapbox-style";
33
34
  import { Tile } from "ol";
34
- import {
35
- handleEndpointError,
36
- tileLoadErrorCatchFunction,
37
- } from "./handle-errors.js";
35
+ import { tileLoadErrorCatchFunction } from "./handle-errors.js";
38
36
  import VectorTile from "ol/source/VectorTile.js";
39
37
  import GeoTIFF from "ol/source/GeoTIFF.js";
40
38
  import WebGLTileLayer from "ol/layer/WebGLTile.js";
@@ -45,6 +43,13 @@ import {
45
43
  updateLayerProperties,
46
44
  } from "./layer-update.js";
47
45
  import { initHoverLayer } from "./feature-hover.js";
46
+ import {
47
+ emitLayerCreationError,
48
+ emitLayerLoadingError,
49
+ emitLayerLoadingStatusSuccess,
50
+ propagateLayerStateChangeEventToMap,
51
+ } from "./register-events.js";
52
+ import { GEOSPATIAL_SDK_PREFIX } from "./constants.js";
48
53
 
49
54
  // Register proj4 with OpenLayers so that arbitrary EPSG codes
50
55
  // (e.g., UTM zones from GeoTIFF metadata) can be reprojected to the map projection
@@ -53,9 +58,15 @@ register(proj4);
53
58
  const GEOJSON = new GeoJSON();
54
59
  const WFS_MAX_FEATURES = 10000;
55
60
 
61
+ // We need to defer some events being dispatched to make sure they are caught by the map
62
+ // where the layers sit
63
+ // FIXME: this should be better handled in a separate module!
64
+ const defer = () => new Promise((resolve) => setTimeout(resolve, 0));
65
+
56
66
  export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
57
67
  const { type } = layerModel;
58
- let layer: Layer | undefined;
68
+ let layer: Layer;
69
+
59
70
  switch (type) {
60
71
  case "xyz":
61
72
  {
@@ -82,8 +93,10 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
82
93
  });
83
94
  layer.setSource(source);
84
95
  }
96
+ defer().then(() => emitLayerLoadingStatusSuccess(layer));
85
97
  }
86
98
  break;
99
+
87
100
  case "wms":
88
101
  {
89
102
  layer = new TileLayer({});
@@ -104,9 +117,10 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
104
117
  );
105
118
  });
106
119
  layer.setSource(source);
120
+ defer().then(() => emitLayerLoadingStatusSuccess(layer));
107
121
  }
108
-
109
122
  break;
123
+
110
124
  case "wmts": {
111
125
  const olLayer = new TileLayer({});
112
126
  const endpoint = new WmtsEndpoint(layerModel.url);
@@ -138,11 +152,16 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
138
152
  }),
139
153
  );
140
154
  })
155
+ .then(() => emitLayerLoadingStatusSuccess(olLayer))
141
156
  .catch((e) => {
142
- handleEndpointError(olLayer, e);
157
+ const httpStatus =
158
+ e instanceof EndpointError ? e.httpStatus : undefined;
159
+ emitLayerLoadingError(olLayer, e, httpStatus);
143
160
  });
144
- return olLayer;
161
+ layer = olLayer;
162
+ break;
145
163
  }
164
+
146
165
  case "wfs": {
147
166
  const olLayer = new VectorLayer({
148
167
  style: layerModel.style ?? defaultStyle,
@@ -169,28 +188,35 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
169
188
  }),
170
189
  );
171
190
  })
191
+ .then(() => emitLayerLoadingStatusSuccess(olLayer))
172
192
  .catch((e) => {
173
- handleEndpointError(olLayer, e);
193
+ const httpStatus =
194
+ e instanceof EndpointError ? e.httpStatus : undefined;
195
+ emitLayerLoadingError(olLayer, e, httpStatus);
174
196
  });
175
197
  layer = olLayer;
176
198
  break;
177
199
  }
200
+
178
201
  case "maplibre-style": {
179
202
  layer = new MapboxVectorLayer({
180
203
  styleUrl: layerModel.styleUrl,
181
204
  accessToken: layerModel.accessToken,
182
205
  }) as unknown as Layer;
206
+ defer().then(() => emitLayerLoadingStatusSuccess(layer));
183
207
  break;
184
208
  }
209
+
185
210
  case "geojson": {
211
+ layer = new VectorLayer({
212
+ style: layerModel.style ?? defaultStyle,
213
+ });
214
+ let source: VectorSource;
186
215
  if (layerModel.url !== undefined) {
187
- layer = new VectorLayer({
188
- source: new VectorSource({
189
- format: new GeoJSON(),
190
- url: layerModel.url,
191
- attributions: layerModel.attributions,
192
- }),
193
- style: layerModel.style ?? defaultStyle,
216
+ source = new VectorSource({
217
+ format: new GeoJSON(),
218
+ url: layerModel.url,
219
+ attributions: layerModel.attributions,
194
220
  });
195
221
  } else {
196
222
  let geojson = layerModel.data;
@@ -206,16 +232,17 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
206
232
  featureProjection: "EPSG:3857",
207
233
  dataProjection: "EPSG:4326",
208
234
  }) as Feature<Geometry>[];
209
- layer = new VectorLayer({
210
- source: new VectorSource({
211
- features,
212
- attributions: layerModel.attributions,
213
- }),
214
- style: layerModel.style ?? defaultStyle,
235
+ source = new VectorSource({
236
+ features,
237
+ attributions: layerModel.attributions,
215
238
  });
216
239
  }
240
+ layer.setSource(source);
241
+ // FIXME: actually track layer loading and data info
242
+ defer().then(() => emitLayerLoadingStatusSuccess(layer));
217
243
  break;
218
244
  }
245
+
219
246
  case "ogcapi": {
220
247
  const ogcEndpoint = new OgcApiEndpoint(layerModel.url);
221
248
  let layerUrl: string;
@@ -249,17 +276,21 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
249
276
  layerModel.collection,
250
277
  layerModel.options,
251
278
  );
279
+ const source = new VectorSource({
280
+ format: new GeoJSON(),
281
+ url: layerUrl,
282
+ attributions: layerModel.attributions,
283
+ });
252
284
  layer = new VectorLayer({
253
- source: new VectorSource({
254
- format: new GeoJSON(),
255
- url: layerUrl,
256
- attributions: layerModel.attributions,
257
- }),
285
+ source,
258
286
  style: layerModel.style ?? defaultStyle,
259
287
  });
260
288
  }
289
+ // FIXME: actually track layer loading
290
+ defer().then(() => emitLayerLoadingStatusSuccess(layer));
261
291
  break;
262
292
  }
293
+
263
294
  case "geotiff": {
264
295
  const geoTiffSource = new GeoTIFF({
265
296
  sources: [{ url: layerModel.url }],
@@ -268,26 +299,38 @@ export async function createLayer(layerModel: MapContextLayer): Promise<Layer> {
268
299
  layer = new WebGLTileLayer({
269
300
  source: geoTiffSource,
270
301
  });
302
+ // FIXME: actually track tile loading
303
+ defer().then(() => emitLayerLoadingStatusSuccess(layer));
271
304
  break;
272
305
  }
273
- default:
274
- throw new Error(`Unrecognized layer type: ${JSON.stringify(layerModel)}`);
275
- }
276
- if (!layer) {
277
- throw new Error(`Layer could not be created for type: ${layerModel.type}`);
306
+
307
+ default: {
308
+ // we create an empty placeholder layer so that we still have a corresponding layer in OL
309
+ layer = new VectorLayer({
310
+ properties: {
311
+ [`${GEOSPATIAL_SDK_PREFIX}layer-with-error`]: true,
312
+ },
313
+ });
314
+ defer().then(() =>
315
+ emitLayerCreationError(
316
+ layer,
317
+ new Error(`Unrecognized layer type: ${JSON.stringify(layerModel)}`),
318
+ ),
319
+ );
320
+ }
278
321
  }
279
322
 
280
- updateLayerProperties(layerModel, layer);
323
+ updateLayerProperties(layerModel, layer!);
281
324
 
282
- return layer;
325
+ return layer!;
283
326
  }
284
327
 
285
- export function updateLayerInMap(
328
+ export async function updateLayerInMap(
286
329
  map: Map,
287
330
  layerModel: MapContextLayer,
288
331
  layerPosition: number,
289
332
  previousLayerModel: MapContextLayer,
290
- ) {
333
+ ): Promise<void> {
291
334
  const layers = map.getLayers();
292
335
  const updatedLayer = layers.item(layerPosition) as Layer;
293
336
 
@@ -299,8 +342,9 @@ export function updateLayerInMap(
299
342
 
300
343
  // dispose and recreate layer
301
344
  updatedLayer.dispose();
302
- createLayer(layerModel).then((layer) => {
345
+ await createLayer(layerModel).then((layer) => {
303
346
  layers.setAt(layerPosition, layer);
347
+ propagateLayerStateChangeEventToMap(map, layer);
304
348
  });
305
349
  }
306
350
 
@@ -369,6 +413,7 @@ export async function resetMapFromContext(
369
413
  for (const layerModel of context.layers) {
370
414
  const layer = await createLayer(layerModel);
371
415
  map.addLayer(layer);
416
+ propagateLayerStateChangeEventToMap(map, layer);
372
417
  }
373
418
  initHoverLayer(map);
374
419
  return map;
@@ -108,7 +108,7 @@ export function initHoverLayer(map: OlMap) {
108
108
  );
109
109
  const features = Array.from(featuresByLayer.values()).flat();
110
110
  map.dispatchEvent({
111
- type: FeaturesHoverEventType,
111
+ type: `${GEOSPATIAL_SDK_PREFIX}${FeaturesHoverEventType}`,
112
112
  features,
113
113
  featuresByLayer,
114
114
  } as unknown as BaseEvent);
@@ -1,13 +1,6 @@
1
- import { ImageTile, Tile } from "ol";
2
- import TileState from "ol/TileState.js";
3
- import { SourceLoadErrorEvent } from "@geospatial-sdk/core";
4
- import {
5
- handleTileError,
6
- tileLoadErrorCatchFunction,
7
- } from "./handle-errors.js";
8
1
  import TileLayer from "ol/layer/Tile.js";
9
2
  import VectorLayer from "ol/layer/Vector.js";
10
- import { EndpointError } from "@camptocamp/ogc-client";
3
+ import { emitLayerLoadingError } from "./register-events.js";
11
4
 
12
5
  globalThis.URL.createObjectURL = vi.fn(() => "blob:http://example.com/blob");
13
6
 
@@ -26,45 +19,29 @@ globalThis.fetch = vi.fn().mockImplementation((url: string) => {
26
19
  });
27
20
 
28
21
  describe("handle-errors", () => {
29
- let tile: Tile;
30
-
31
- beforeEach(() => {
32
- tile = new ImageTile(
33
- [0, 0, 0],
34
- TileState.IDLE,
35
- "",
36
- null,
37
- () => tileLoadErrorCatchFunction,
38
- );
39
- });
40
-
41
22
  describe("handleEndpointError", () => {
42
23
  it("should dispatch SourceLoadErrorEvent", () => {
43
- const endpointErrorMock: EndpointError = {
44
- name: "Error",
45
- message: "FORBIDDEN",
46
- httpStatus: 403,
47
- };
48
24
  const layer = new VectorLayer({});
49
25
  const dispatchEventSpy = vi.spyOn(layer, "dispatchEvent");
50
- handleTileError(endpointErrorMock, tile, layer);
51
- expect(dispatchEventSpy).toHaveBeenCalledWith(
52
- new SourceLoadErrorEvent(endpointErrorMock),
53
- );
26
+ emitLayerLoadingError(layer, new Error("FORBIDDEN"), 403);
27
+ expect(dispatchEventSpy).toHaveBeenCalledWith({
28
+ type: "--geospatial-sdk-layer-loading-error",
29
+ error: new Error("FORBIDDEN"),
30
+ httpStatus: 403,
31
+ });
54
32
  });
55
33
  });
56
34
 
57
35
  describe("handleTileError", () => {
58
36
  it("should set tile state to ERROR and dispatch SourceLoadErrorEvent", () => {
59
- const response = new Response("Forbidden", { status: 403 });
60
37
  const layer = new TileLayer({});
61
38
  const dispatchEventSpy = vi.spyOn(layer, "dispatchEvent");
62
- const setStateEventSpy = vi.spyOn(tile, "setState");
63
- handleTileError(response, tile, layer);
64
- expect(setStateEventSpy).toHaveBeenCalledWith(TileState.ERROR);
65
- expect(dispatchEventSpy).toHaveBeenCalledWith(
66
- new SourceLoadErrorEvent(response),
67
- );
39
+ emitLayerLoadingError(layer, new Error("Forbidden"), 403);
40
+ expect(dispatchEventSpy).toHaveBeenCalledWith({
41
+ type: "--geospatial-sdk-layer-loading-error",
42
+ error: new Error("Forbidden"),
43
+ httpStatus: 403,
44
+ });
68
45
  });
69
46
  });
70
47
  });