@geospatial-sdk/maplibre 0.0.5-dev.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,287 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import {
3
+ MAP_CTX_LAYER_GEOJSON_FIXTURE,
4
+ MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE,
5
+ MAP_CTX_LAYER_OGCAPI_FIXTURE,
6
+ MAP_CTX_LAYER_WFS_FIXTURE,
7
+ MAP_CTX_LAYER_WMS_FIXTURE,
8
+ } from "@geospatial-sdk/core/fixtures/map-context.fixtures";
9
+ import { LayerGeojsonWithData, MapContextLayer } from "@geospatial-sdk/core";
10
+ import { createLayer } from "./create-map";
11
+ import {
12
+ FillLayerSpecification,
13
+ GeoJSONSourceSpecification,
14
+ RasterLayerSpecification,
15
+ } from "@maplibre/maplibre-gl-style-spec";
16
+ import {
17
+ FEATURE_COLLECTION_LINESTRING_FIXTURE_4326,
18
+ FEATURE_COLLECTION_POLYGON_FIXTURE_4326,
19
+ } from "@geospatial-sdk/core/fixtures/geojson.fixtures";
20
+ import { PartialStyleSpecification } from "../maplibre.models";
21
+ import {
22
+ CircleLayerSpecification,
23
+ LayerSpecification,
24
+ LineLayerSpecification,
25
+ RasterSourceSpecification,
26
+ } from "maplibre-gl";
27
+
28
+ describe("MapContextService", () => {
29
+ describe("#createLayer", () => {
30
+ let layerModel: MapContextLayer, style: PartialStyleSpecification;
31
+
32
+ describe("WMS", () => {
33
+ beforeEach(async () => {
34
+ (layerModel = MAP_CTX_LAYER_WMS_FIXTURE),
35
+ (style = await createLayer(layerModel, 0));
36
+ });
37
+ it("create a tile layer", () => {
38
+ expect(style).toBeTruthy();
39
+ });
40
+ it("create a source", () => {
41
+ const sourcesIds = Object.keys(style.sources);
42
+ expect(sourcesIds.length).toBe(1);
43
+ const id = sourcesIds[0];
44
+ const source = style.sources[id] as RasterSourceSpecification;
45
+ expect(id).toBe("1046815418");
46
+ expect(source.tiles).toEqual([
47
+ "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}",
48
+ ]);
49
+ });
50
+ it("create a layer", () => {
51
+ expect(style.layers).toBeTruthy();
52
+ expect(style.layers.length).toBe(1);
53
+
54
+ const layer = style.layers[0] as RasterLayerSpecification;
55
+ expect(layer.id).toBe("1046815418");
56
+ expect(layer.source).toBe("1046815418");
57
+ expect(layer.type).toBe(`raster`);
58
+ });
59
+ });
60
+ describe("GEOJSON", () => {
61
+ describe("with inline data", () => {
62
+ beforeEach(async () => {
63
+ layerModel = MAP_CTX_LAYER_GEOJSON_FIXTURE;
64
+ style = await createLayer(layerModel, 0);
65
+ Math.random = vi.fn(() => 0.027404);
66
+ });
67
+ it("create a VectorLayer", () => {
68
+ expect(style).toBeTruthy();
69
+ });
70
+ it("create a source", () => {
71
+ const sourcesIds = Object.keys(style.sources);
72
+ expect(sourcesIds.length).toBe(1);
73
+ expect(sourcesIds[0]).toBe("2792250259");
74
+ });
75
+ it("create 3 layers", () => {
76
+ expect(style.layers).toBeTruthy();
77
+ expect(style.layers.length).toBe(3);
78
+
79
+ let layer = style.layers[0] as RasterLayerSpecification;
80
+ expect(layer.id).toBe("2792250259-fill");
81
+ expect(layer.source).toBe("2792250259");
82
+
83
+ layer = style.layers[1] as RasterLayerSpecification;
84
+ expect(layer.id).toBe("2792250259-line");
85
+ expect(layer.source).toBe("2792250259");
86
+
87
+ layer = style.layers[2] as RasterLayerSpecification;
88
+ expect(layer.id).toBe("2792250259-circle");
89
+ expect(layer.source).toBe("2792250259");
90
+ });
91
+
92
+ it("set correct source properties", () => {
93
+ const sourcesIds = Object.keys(style.sources);
94
+ const source = style.sources[
95
+ sourcesIds[0]
96
+ ] as GeoJSONSourceSpecification;
97
+ expect(source.type).toBe("geojson");
98
+ expect(source.data).toBe((layerModel as LayerGeojsonWithData).data);
99
+ });
100
+ it("set correct layer properties", () => {
101
+ const layer = style.layers[0] as FillLayerSpecification;
102
+ expect(layer.type).toBe(`fill`);
103
+ expect(layer.paint["fill-color"]).toBe("rgba(255,255,255,0.4)");
104
+ });
105
+ });
106
+ describe("with inline data as string", () => {
107
+ beforeEach(async () => {
108
+ layerModel = { ...MAP_CTX_LAYER_GEOJSON_FIXTURE };
109
+ layerModel.data = JSON.stringify(layerModel.data);
110
+ style = await createLayer(layerModel, 0);
111
+ });
112
+ it("create a VectorLayer", () => {
113
+ expect(style).toBeTruthy();
114
+ });
115
+ it("create a source", () => {
116
+ const sourcesIds = Object.keys(style.sources);
117
+ expect(sourcesIds.length).toBe(1);
118
+ });
119
+ it("create 3 layers", () => {
120
+ expect(style.layers).toBeTruthy();
121
+ expect(style.layers.length).toBe(3);
122
+ });
123
+
124
+ it("set correct source properties", () => {
125
+ const sourcesIds = Object.keys(style.sources);
126
+ const source = style.sources[
127
+ sourcesIds[0]
128
+ ] as GeoJSONSourceSpecification;
129
+ expect(source.type).toBe("geojson");
130
+ expect(source.data).toEqual(MAP_CTX_LAYER_GEOJSON_FIXTURE.data);
131
+ });
132
+
133
+ it("set correct layer properties", () => {
134
+ const layer = style.layers[0] as FillLayerSpecification;
135
+ expect(layer.type).toBe(`fill`);
136
+ expect(layer.paint["fill-color"]).toBe("rgba(255,255,255,0.4)");
137
+ expect(layer.paint["fill-opacity"]).toBeUndefined();
138
+ });
139
+ });
140
+ describe("with invalid inline data as string", () => {
141
+ beforeEach(async () => {
142
+ const spy = vi.spyOn(window.console, "warn");
143
+ spy.mockClear();
144
+ layerModel = {
145
+ ...MAP_CTX_LAYER_GEOJSON_FIXTURE,
146
+ url: undefined,
147
+ data: "blargz",
148
+ } as LayerGeojsonWithData;
149
+ style = await createLayer(layerModel, 0);
150
+ });
151
+ it("create a VectorLayer", () => {
152
+ expect(style).toBeTruthy();
153
+ });
154
+ it("outputs error in the console", () => {
155
+ expect(window.console.warn).toHaveBeenCalled();
156
+ });
157
+ it("create an empty VectorSource source", () => {
158
+ expect(style.sources).toEqual({
159
+ "3631250040": {
160
+ data: {
161
+ features: [],
162
+ type: "FeatureCollection",
163
+ },
164
+ type: "geojson",
165
+ },
166
+ });
167
+ });
168
+ describe("with remote file url", () => {
169
+ beforeEach(async () => {
170
+ layerModel = MAP_CTX_LAYER_GEOJSON_REMOTE_FIXTURE;
171
+ global.fetch = vi.fn(() =>
172
+ Promise.resolve({
173
+ ok: true,
174
+ json: () =>
175
+ Promise.resolve(FEATURE_COLLECTION_POLYGON_FIXTURE_4326),
176
+ }),
177
+ );
178
+ style = await createLayer(layerModel, 0);
179
+ });
180
+ it("create a VectorLayer", () => {
181
+ expect(style).toBeTruthy();
182
+ });
183
+ it("create a source", () => {
184
+ const sourcesIds = Object.keys(style.sources);
185
+ expect(sourcesIds.length).toBe(1);
186
+ });
187
+ it("create 3 layers", () => {
188
+ expect(style.layers).toBeTruthy();
189
+ expect(style.layers.length).toBe(3);
190
+ });
191
+
192
+ it("set correct source properties", () => {
193
+ const sourcesIds = Object.keys(style.sources);
194
+ const source = style.sources[
195
+ sourcesIds[0]
196
+ ] as GeoJSONSourceSpecification;
197
+ expect(source.type).toBe("geojson");
198
+ expect(source.data).toEqual(
199
+ "https://my.host.com/data/regions.json",
200
+ );
201
+ });
202
+
203
+ it("set correct layer properties", () => {
204
+ const layer = style.layers[0] as FillLayerSpecification;
205
+ expect(layer.type).toBe(`fill`);
206
+ expect(layer.paint["fill-color"]).toBe("rgba(255,255,255,0.4)");
207
+ expect(layer.paint["fill-opacity"]).toBeUndefined();
208
+ });
209
+ });
210
+ });
211
+ });
212
+ describe("WFS", () => {
213
+ beforeEach(async () => {
214
+ (layerModel = MAP_CTX_LAYER_WFS_FIXTURE),
215
+ (style = await createLayer(layerModel, 1));
216
+ });
217
+ it("create a vector layer", () => {
218
+ expect(style).toBeTruthy();
219
+ });
220
+ it("create a source", () => {
221
+ const sourcesIds = Object.keys(style.sources);
222
+ expect(sourcesIds.length).toBe(1);
223
+ const id = sourcesIds[0];
224
+ const source = style.sources[id] as GeoJSONSourceSpecification;
225
+ expect(id).toBe("985400327");
226
+ expect(source.data).toEqual(
227
+ "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",
228
+ );
229
+ });
230
+ it("create 3 layers", () => {
231
+ expect(style.layers).toBeTruthy();
232
+ expect(style.layers.length).toBe(3);
233
+
234
+ let layer: LayerSpecification = style
235
+ .layers[0] as FillLayerSpecification;
236
+ expect(layer.id).toBe("985400327-fill");
237
+ expect(layer.source).toBe("985400327");
238
+ expect(layer.metadata.sourcePosition).toBe(1);
239
+
240
+ layer = style.layers[1] as LineLayerSpecification;
241
+ expect(layer.id).toBe("985400327-line");
242
+ expect(layer.source).toBe("985400327");
243
+ expect(layer.metadata.sourcePosition).toBe(1);
244
+
245
+ layer = style.layers[2] as CircleLayerSpecification;
246
+ expect(layer.id).toBe("985400327-circle");
247
+ expect(layer.source).toBe("985400327");
248
+ expect(layer.metadata.sourcePosition).toBe(1);
249
+ });
250
+ });
251
+ describe("OGCAPI", () => {
252
+ beforeEach(async () => {
253
+ global.fetch = vi.fn(() =>
254
+ Promise.resolve({
255
+ ok: true,
256
+ json: () =>
257
+ Promise.resolve(FEATURE_COLLECTION_LINESTRING_FIXTURE_4326),
258
+ }),
259
+ );
260
+
261
+ (layerModel = MAP_CTX_LAYER_OGCAPI_FIXTURE),
262
+ (style = await createLayer(layerModel, 0));
263
+ });
264
+ it("create a vector layer", () => {
265
+ expect(style).toBeTruthy();
266
+ });
267
+ it("create a source", () => {
268
+ const sourcesIds = Object.keys(style.sources);
269
+ expect(sourcesIds.length).toBe(1);
270
+ const id = sourcesIds[0];
271
+ const source = style.sources[id] as GeoJSONSourceSpecification;
272
+ expect(id).toBe("504003385");
273
+ expect(source.data).toEqual(
274
+ "https://demo.ldproxy.net/zoomstack/collections/airports/items?f=json",
275
+ );
276
+ });
277
+ it("create 3 layers", () => {
278
+ expect(style.layers).toBeTruthy();
279
+ expect(style.layers.length).toBe(3);
280
+ const layer = style.layers[0] as FillLayerSpecification;
281
+ expect(layer.id).toBe("504003385-fill");
282
+ expect(layer.source).toBe("504003385");
283
+ expect(layer.metadata.sourcePosition).toBe(0);
284
+ });
285
+ });
286
+ });
287
+ });
@@ -0,0 +1,173 @@
1
+ import {
2
+ MapContext,
3
+ MapContextLayer,
4
+ removeSearchParams,
5
+ ViewByZoomAndCenter,
6
+ } from "@geospatial-sdk/core";
7
+
8
+ import {
9
+ LayerSpecification,
10
+ Map,
11
+ MapOptions,
12
+ StyleSpecification,
13
+ } from "maplibre-gl";
14
+ import { FeatureCollection, Geometry } from "geojson";
15
+ import {
16
+ OgcApiEndpoint,
17
+ WfsEndpoint,
18
+ WmsEndpoint,
19
+ } from "@camptocamp/ogc-client";
20
+ import {
21
+ createDatasetFromGeoJsonLayer,
22
+ generateLayerId,
23
+ } from "../helpers/map.helpers";
24
+ import { Dataset, PartialStyleSpecification } from "../maplibre.models";
25
+
26
+ const featureCollection: FeatureCollection<Geometry | null> = {
27
+ type: "FeatureCollection",
28
+ features: [],
29
+ };
30
+
31
+ export async function createLayer(
32
+ layerModel: MapContextLayer,
33
+ sourcePosition: number,
34
+ ): Promise<PartialStyleSpecification> {
35
+ const { type } = layerModel;
36
+
37
+ switch (type) {
38
+ case "wms": {
39
+ const layerId = generateLayerId(layerModel);
40
+ const sourceId = layerId;
41
+
42
+ const endpoint = await new WmsEndpoint(layerModel.url).isReady();
43
+ let url = endpoint.getMapUrl([layerModel.name], {
44
+ widthPx: 256,
45
+ heightPx: 256,
46
+ extent: [0, 0, 0, 0], // will be replaced by maplibre-gl
47
+ outputFormat: "image/png",
48
+ crs: "EPSG:3857",
49
+ });
50
+ url = removeSearchParams(url, ["bbox"]);
51
+ url = `${url.toString()}&BBOX={bbox-epsg-3857}`;
52
+
53
+ const dataset: Dataset = {
54
+ sources: {
55
+ [sourceId]: {
56
+ type: "raster",
57
+ tiles: [url],
58
+ tileSize: 256,
59
+ },
60
+ },
61
+ layers: [
62
+ {
63
+ id: layerId,
64
+ type: "raster",
65
+ source: sourceId,
66
+ paint: {},
67
+ metadata: {
68
+ sourcePosition,
69
+ },
70
+ },
71
+ ],
72
+ };
73
+ return dataset;
74
+ }
75
+ case "wfs": {
76
+ const entryPoint = await new WfsEndpoint(layerModel.url).isReady();
77
+ const url = entryPoint.getFeatureUrl(layerModel.featureType, {
78
+ asJson: true,
79
+ outputCrs: "EPSG:4326",
80
+ });
81
+ return createDatasetFromGeoJsonLayer(layerModel, url, sourcePosition);
82
+ }
83
+ case "geojson": {
84
+ let geojson;
85
+ if (layerModel.url !== undefined) {
86
+ geojson = layerModel.url;
87
+ } else {
88
+ const data = layerModel.data;
89
+ if (typeof data === "string") {
90
+ try {
91
+ geojson = JSON.parse(data) as FeatureCollection;
92
+ } catch (e) {
93
+ console.warn("A layer could not be created", layerModel, e);
94
+ geojson = featureCollection;
95
+ }
96
+ } else {
97
+ geojson = data;
98
+ }
99
+ }
100
+ return createDatasetFromGeoJsonLayer(layerModel, geojson, sourcePosition);
101
+ }
102
+ case "ogcapi": {
103
+ const ogcEndpoint = new OgcApiEndpoint(layerModel.url);
104
+ let layerUrl: string;
105
+ if (layerModel.useTiles) {
106
+ console.warn("[Warning] OGC API - Tiles not yet implemented.");
107
+ } else {
108
+ layerUrl = await ogcEndpoint.getCollectionItemsUrl(
109
+ layerModel.collection,
110
+ { ...layerModel.options, asJson: true },
111
+ );
112
+ return createDatasetFromGeoJsonLayer(
113
+ layerModel,
114
+ layerUrl,
115
+ sourcePosition,
116
+ );
117
+ }
118
+ break;
119
+ }
120
+ case "maplibre-style": {
121
+ console.warn("[Warning] Maplibre style - Not yet fully implemented.");
122
+ const style = await fetch(layerModel.styleUrl).then((res) => res.json());
123
+ style.layers?.forEach(
124
+ (layer: LayerSpecification) => (layer.metadata = { sourcePosition }),
125
+ );
126
+ return style;
127
+ }
128
+ }
129
+ return {} as StyleSpecification;
130
+ }
131
+
132
+ /**
133
+ * Create an Maplibre map from a context; optionally specify a target (root element) for the map
134
+ * @param context
135
+ * @param target
136
+ */
137
+ export async function createMapFromContext(
138
+ context: MapContext,
139
+ mapOptions: MapOptions,
140
+ ): Promise<Map> {
141
+ const map = new Map(mapOptions);
142
+ return await resetMapFromContext(map, context);
143
+ }
144
+
145
+ /**
146
+ * Resets a Maplibre map from a context; existing content will be cleared
147
+ * @param map
148
+ * @param context
149
+ */
150
+ export async function resetMapFromContext(
151
+ map: Map,
152
+ context: MapContext,
153
+ ): Promise<Map> {
154
+ map.setZoom((context.view as ViewByZoomAndCenter).zoom);
155
+ map.setCenter((context.view as ViewByZoomAndCenter).center);
156
+
157
+ for (let i = 0; i < context.layers.length; i++) {
158
+ const layerModel = context.layers[i];
159
+ const partialMLStyle = await createLayer(layerModel, i);
160
+
161
+ if (partialMLStyle.glyphs) {
162
+ map.setGlyphs(partialMLStyle.glyphs);
163
+ }
164
+ if (partialMLStyle.sprite) {
165
+ map.setSprite(partialMLStyle.sprite as string);
166
+ }
167
+ Object.keys(partialMLStyle.sources).forEach((sourceId) =>
168
+ map.addSource(sourceId, partialMLStyle.sources[sourceId]),
169
+ );
170
+ partialMLStyle.layers.map((layer) => map.addLayer(layer));
171
+ }
172
+ return map;
173
+ }
@@ -0,0 +1,2 @@
1
+ export { createMapFromContext, resetMapFromContext } from "./create-map";
2
+ export { applyContextDiffToMap } from "./apply-context-diff";
@@ -0,0 +1,31 @@
1
+ import {
2
+ BackgroundLayerSpecification,
3
+ LayerSpecification,
4
+ StyleSpecification,
5
+ } from "maplibre-gl";
6
+ import {
7
+ MapContextLayerGeojson,
8
+ MapContextLayerOgcApi,
9
+ MapContextLayerWfs,
10
+ } from "@geospatial-sdk/core";
11
+
12
+ export type LayerSpecificationWithSource = Exclude<
13
+ LayerSpecification,
14
+ BackgroundLayerSpecification
15
+ >;
16
+
17
+ export interface LayerMetadataSpecification {
18
+ sourcePosition: number;
19
+ }
20
+
21
+ export type LayerContextWithStyle =
22
+ | MapContextLayerWfs
23
+ | MapContextLayerOgcApi
24
+ | MapContextLayerGeojson;
25
+
26
+ export type Dataset = Pick<StyleSpecification, "sources" | "layers">;
27
+
28
+ export type PartialStyleSpecification = Dataset & {
29
+ glyphs?: StyleSpecification["glyphs"];
30
+ sprite?: StyleSpecification["sprite"];
31
+ };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@geospatial-sdk/maplibre",
3
+ "version": "0.0.5-dev.41+dba0603",
4
+ "description": "Maplibre-related utilities",
5
+ "keywords": [
6
+ "maplibre",
7
+ "map"
8
+ ],
9
+ "author": "Florent Gravin <florent.gravin@camptocamp.com>",
10
+ "homepage": "",
11
+ "license": "BSD-3-Clause",
12
+ "main": "dist/index.js",
13
+ "typings": "dist/index.d.ts",
14
+ "type": "module",
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "directories": {
19
+ "lib": "lib"
20
+ },
21
+ "files": [
22
+ "lib",
23
+ "dist"
24
+ ],
25
+ "scripts": {
26
+ "test": "vitest",
27
+ "build": "tsc"
28
+ },
29
+ "devDependencies": {
30
+ "maplibre-gl": "^5.7.3"
31
+ },
32
+ "peerDependencies": {
33
+ "maplibre-gl": "^5.7.3"
34
+ },
35
+ "dependencies": {
36
+ "@geospatial-sdk/core": "^0.0.5-dev.41+dba0603"
37
+ },
38
+ "gitHead": "dba060374167251ca486946c95e8a885c121ccd5"
39
+ }