@geospatial-sdk/core 0.0.5-dev.0 → 0.0.5-dev.0.sha-5830a6a
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.
- package/README.md +38 -5
- package/dist/constant/projections.d.ts +2 -0
- package/dist/constant/projections.d.ts.map +1 -0
- package/dist/constant/projections.js +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/model/events.d.ts +86 -0
- package/dist/model/events.d.ts.map +1 -0
- package/dist/model/events.js +37 -0
- package/dist/model/index.d.ts +9 -2
- package/dist/model/index.d.ts.map +1 -1
- package/dist/model/index.js +9 -2
- package/dist/model/map-context-diff.d.ts +11 -3
- package/dist/model/map-context-diff.d.ts.map +1 -1
- package/dist/model/map-context.d.ts +174 -22
- package/dist/model/map-context.d.ts.map +1 -1
- package/dist/model/resolved-map-state.d.ts +49 -0
- package/dist/model/resolved-map-state.d.ts.map +1 -0
- package/dist/model/resolved-map-state.js +1 -0
- package/dist/model/style.d.ts +3 -0
- package/dist/model/style.d.ts.map +1 -0
- package/dist/model/style.js +1 -0
- package/dist/utils/hash.d.ts +2 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/utils/hash.js +28 -0
- package/dist/utils/index.d.ts +12 -4
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +12 -4
- package/dist/utils/map-context-diff.d.ts +1 -1
- package/dist/utils/map-context-diff.d.ts.map +1 -1
- package/dist/utils/map-context-diff.js +19 -11
- package/dist/utils/map-context-layer.d.ts +13 -0
- package/dist/utils/map-context-layer.d.ts.map +1 -0
- package/dist/utils/map-context-layer.js +44 -0
- package/dist/utils/map-context.d.ts +48 -4
- package/dist/utils/map-context.d.ts.map +1 -1
- package/dist/utils/map-context.js +80 -29
- package/dist/utils/style.d.ts +4 -0
- package/dist/utils/style.d.ts.map +1 -0
- package/dist/utils/style.js +68 -0
- package/dist/utils/view.d.ts +25 -0
- package/dist/utils/view.d.ts.map +1 -0
- package/dist/utils/view.js +126 -0
- package/lib/constant/projections.ts +1 -0
- package/lib/index.ts +8 -3
- package/lib/model/events.ts +113 -0
- package/lib/model/index.ts +10 -2
- package/lib/model/map-context-diff.ts +12 -3
- package/lib/model/map-context.ts +219 -26
- package/lib/model/resolved-map-state.ts +67 -0
- package/lib/model/style.ts +3 -0
- package/lib/utils/freeze.test.ts +1 -1
- package/lib/utils/hash.test.ts +180 -0
- package/lib/utils/hash.ts +28 -0
- package/lib/utils/index.ts +20 -4
- package/lib/utils/map-context-diff.test.ts +240 -10
- package/lib/utils/map-context-diff.ts +23 -14
- package/lib/utils/map-context-layer.test.ts +357 -0
- package/lib/utils/map-context-layer.ts +66 -0
- package/lib/utils/map-context.test.ts +136 -272
- package/lib/utils/map-context.ts +108 -39
- package/lib/utils/style.ts +71 -0
- package/lib/utils/url.test.ts +1 -1
- package/lib/utils/view.test.ts +146 -0
- package/lib/utils/view.ts +163 -0
- package/package.json +20 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { createViewFromLayer } from "./view.js";
|
|
2
|
+
import {
|
|
3
|
+
MapContextLayerGeojson,
|
|
4
|
+
MapContextLayerWfs,
|
|
5
|
+
MapContextLayerWms,
|
|
6
|
+
MapContextLayerWmts,
|
|
7
|
+
} from "../model/index.js";
|
|
8
|
+
|
|
9
|
+
vitest.mock("@camptocamp/ogc-client", () => ({
|
|
10
|
+
WmsEndpoint: class {
|
|
11
|
+
constructor() {}
|
|
12
|
+
isReady() {
|
|
13
|
+
return Promise.resolve({
|
|
14
|
+
getLayerByName: (name: string) => {
|
|
15
|
+
if (name.includes("error")) {
|
|
16
|
+
throw new Error("Something went wrong");
|
|
17
|
+
}
|
|
18
|
+
let boundingBoxes;
|
|
19
|
+
if (name.includes("nobbox")) {
|
|
20
|
+
boundingBoxes = {};
|
|
21
|
+
} else if (name.includes("4326")) {
|
|
22
|
+
boundingBoxes = {
|
|
23
|
+
"EPSG:4326": [1, 2.6, 3.3, 4.2],
|
|
24
|
+
"CRS:84": [2.3, 50.6, 2.8, 50.9],
|
|
25
|
+
};
|
|
26
|
+
} else if (name.includes("2154")) {
|
|
27
|
+
boundingBoxes = {
|
|
28
|
+
"EPSG:2154": [650796.4, 7060330.6, 690891.3, 7090402.2],
|
|
29
|
+
};
|
|
30
|
+
} else {
|
|
31
|
+
boundingBoxes = {
|
|
32
|
+
"CRS:84": [2.3, 50.6, 2.8, 50.9],
|
|
33
|
+
"EPSG:2154": [650796.4, 7060330.6, 690891.3, 7090402.2],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
name,
|
|
38
|
+
boundingBoxes,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
WmtsEndpoint: class {
|
|
45
|
+
constructor() {}
|
|
46
|
+
isReady() {
|
|
47
|
+
return Promise.resolve({
|
|
48
|
+
getSingleLayerName: () => "layerName",
|
|
49
|
+
getLayerByName: () => ({
|
|
50
|
+
latLonBoundingBox: [1, 2.6, 3.3, 4.2],
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
WfsEndpoint: class {
|
|
56
|
+
constructor(private url: string) {}
|
|
57
|
+
isReady() {
|
|
58
|
+
return Promise.resolve({
|
|
59
|
+
getFeatureTypeSummary: (featureType: string) => {
|
|
60
|
+
return {
|
|
61
|
+
name: featureType,
|
|
62
|
+
boundingBox: [1.33, 48.81, 4.3, 51.1],
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
describe("view", () => {
|
|
71
|
+
describe("createViewFromLayer", () => {
|
|
72
|
+
it("should return view for WMS layer", async () => {
|
|
73
|
+
const layer = {
|
|
74
|
+
type: "wms",
|
|
75
|
+
name: "mock_4326",
|
|
76
|
+
url: "http://mock/wms",
|
|
77
|
+
} as MapContextLayerWms;
|
|
78
|
+
const view = await createViewFromLayer(layer);
|
|
79
|
+
expect(view).toEqual({
|
|
80
|
+
extent: [1, 2.6, 3.3, 4.2],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should return view for WMTS layer", async () => {
|
|
85
|
+
const layer = {
|
|
86
|
+
type: "wmts",
|
|
87
|
+
name: "mock",
|
|
88
|
+
url: "http://mock/wmts",
|
|
89
|
+
} as MapContextLayerWmts;
|
|
90
|
+
const view = await createViewFromLayer(layer);
|
|
91
|
+
expect(view).toEqual({
|
|
92
|
+
extent: [1, 2.6, 3.3, 4.2],
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("should return view for WFS layer", async () => {
|
|
97
|
+
const layer = {
|
|
98
|
+
type: "wfs",
|
|
99
|
+
featureType: "mock",
|
|
100
|
+
url: "http://mock/wfs",
|
|
101
|
+
} as MapContextLayerWfs;
|
|
102
|
+
const view = await createViewFromLayer(layer);
|
|
103
|
+
expect(view).toEqual({
|
|
104
|
+
extent: [1.33, 48.81, 4.3, 51.1],
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("should return view for GeoJSON layer", async () => {
|
|
109
|
+
const layer = {
|
|
110
|
+
type: "geojson",
|
|
111
|
+
data: {
|
|
112
|
+
type: "FeatureCollection",
|
|
113
|
+
features: [
|
|
114
|
+
{
|
|
115
|
+
type: "Feature",
|
|
116
|
+
properties: {},
|
|
117
|
+
geometry: {
|
|
118
|
+
type: "Point",
|
|
119
|
+
coordinates: [125.6, 10.1],
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: "Feature",
|
|
124
|
+
properties: {},
|
|
125
|
+
geometry: {
|
|
126
|
+
type: "Point",
|
|
127
|
+
coordinates: [100, 200],
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
} as MapContextLayerGeojson;
|
|
133
|
+
const view = await createViewFromLayer(layer);
|
|
134
|
+
expect(view).toEqual({
|
|
135
|
+
extent: [100, 10.1, 125.6, 200],
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("should throw error for unsupported layer type", async () => {
|
|
140
|
+
const layer = { type: "unsupported" } as any;
|
|
141
|
+
await expect(createViewFromLayer(layer)).rejects.toThrow(
|
|
142
|
+
"Unsupported layer type: unsupported",
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { transformExtent } from "ol/proj.js";
|
|
2
|
+
import { WfsEndpoint, WmsEndpoint, WmtsEndpoint } from "@camptocamp/ogc-client";
|
|
3
|
+
import { LONLAT_CRS_CODES } from "../constant/projections.js";
|
|
4
|
+
import { fromEPSGCode } from "ol/proj/proj4.js";
|
|
5
|
+
import GeoJSON from "ol/format/GeoJSON.js";
|
|
6
|
+
import { extend } from "ol/extent.js";
|
|
7
|
+
import type Feature from "ol/Feature.js";
|
|
8
|
+
import GeoTIFF from "ol/source/GeoTIFF.js";
|
|
9
|
+
import {
|
|
10
|
+
Extent,
|
|
11
|
+
MapContextLayer,
|
|
12
|
+
MapContextLayerGeotiff,
|
|
13
|
+
MapContextLayerWfs,
|
|
14
|
+
MapContextLayerWms,
|
|
15
|
+
MapContextLayerWmts,
|
|
16
|
+
MapContextView,
|
|
17
|
+
ViewByExtent,
|
|
18
|
+
} from "../model/index.js";
|
|
19
|
+
import { FeatureCollection, Geometry } from "geojson";
|
|
20
|
+
|
|
21
|
+
const GEOJSON = new GeoJSON();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a view from a layer by extracting its geographic extent. The returned extent is
|
|
25
|
+
* always expressed in EPSG:4326 (longitude/latitude) coordinates.
|
|
26
|
+
*
|
|
27
|
+
* @param layer - The map context layer to extract the extent from
|
|
28
|
+
* @returns A Promise resolving to a `ViewByExtent` object, or `null` if the extent cannot be determined
|
|
29
|
+
* @throws {Error} If the layer type is not supported
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```typescript
|
|
33
|
+
* const wmsLayer: MapContextLayerWms = {
|
|
34
|
+
* type: 'wms',
|
|
35
|
+
* url: 'https://example.com/wms',
|
|
36
|
+
* name: 'myLayer'
|
|
37
|
+
* };
|
|
38
|
+
*
|
|
39
|
+
* const view = await createViewFromLayer(wmsLayer);
|
|
40
|
+
* if (view) {
|
|
41
|
+
* console.log('Extent:', view.extent); // [minX, minY, maxX, maxY] in EPSG:4326
|
|
42
|
+
* }
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export async function createViewFromLayer(
|
|
46
|
+
layer: MapContextLayer,
|
|
47
|
+
): Promise<MapContextView | null> {
|
|
48
|
+
if (layer.type === "wms") {
|
|
49
|
+
return await getWmsLayerExtent(layer);
|
|
50
|
+
} else if (layer.type === "wmts") {
|
|
51
|
+
return await getWmtsLayerExtent(layer);
|
|
52
|
+
} else if (layer.type === "geojson" && layer.data) {
|
|
53
|
+
return computeExtentFromGeojson(layer.data);
|
|
54
|
+
} else if (layer.type === "wfs") {
|
|
55
|
+
return await getWfsLayerExtent(layer);
|
|
56
|
+
} else if (layer.type === "geotiff") {
|
|
57
|
+
return await getGeotiffLayerExtent(layer);
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`Unsupported layer type: ${layer.type}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function computeExtentFromGeojson(
|
|
64
|
+
data: FeatureCollection<Geometry | null> | string,
|
|
65
|
+
): ViewByExtent {
|
|
66
|
+
const geojson = typeof data === "string" ? JSON.parse(data) : data;
|
|
67
|
+
const features = GEOJSON.readFeatures(geojson) as Feature[];
|
|
68
|
+
const extent = features.reduce(
|
|
69
|
+
(prev, curr) => {
|
|
70
|
+
const geom = curr.getGeometry();
|
|
71
|
+
if (!geom) return prev;
|
|
72
|
+
return extend(prev, geom.getExtent()) as Extent;
|
|
73
|
+
},
|
|
74
|
+
[Infinity, Infinity, -Infinity, -Infinity] as Extent,
|
|
75
|
+
) as Extent;
|
|
76
|
+
return {
|
|
77
|
+
extent,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function getWmsLayerExtent(
|
|
82
|
+
layer: MapContextLayerWms,
|
|
83
|
+
): Promise<ViewByExtent | null> {
|
|
84
|
+
const endpoint = await new WmsEndpoint(layer.url).isReady();
|
|
85
|
+
const { boundingBoxes } = endpoint.getLayerByName(layer.name);
|
|
86
|
+
if (!Object.keys(boundingBoxes).length) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const lonLatCRS = Object.keys(boundingBoxes).find((crs) =>
|
|
90
|
+
LONLAT_CRS_CODES.includes(crs),
|
|
91
|
+
);
|
|
92
|
+
if (lonLatCRS) {
|
|
93
|
+
return {
|
|
94
|
+
extent: boundingBoxes[lonLatCRS] as Extent,
|
|
95
|
+
};
|
|
96
|
+
} else {
|
|
97
|
+
const availableEPSGCode = Object.keys(boundingBoxes)[0];
|
|
98
|
+
const proj = await fromEPSGCode(availableEPSGCode);
|
|
99
|
+
return {
|
|
100
|
+
extent: transformExtent(
|
|
101
|
+
boundingBoxes[availableEPSGCode],
|
|
102
|
+
proj,
|
|
103
|
+
"EPSG:4326",
|
|
104
|
+
) as Extent,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function getWmtsLayerExtent(
|
|
110
|
+
layer: MapContextLayerWmts,
|
|
111
|
+
): Promise<ViewByExtent | null> {
|
|
112
|
+
const endpoint = await new WmtsEndpoint(layer.url).isReady();
|
|
113
|
+
const layerName = endpoint.getSingleLayerName() ?? layer.name;
|
|
114
|
+
const wmtsLayer = endpoint.getLayerByName(layerName);
|
|
115
|
+
return wmtsLayer.latLonBoundingBox
|
|
116
|
+
? {
|
|
117
|
+
extent: wmtsLayer.latLonBoundingBox as Extent,
|
|
118
|
+
}
|
|
119
|
+
: null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function getWfsLayerExtent(
|
|
123
|
+
layer: MapContextLayerWfs,
|
|
124
|
+
): Promise<ViewByExtent | null> {
|
|
125
|
+
const endpoint = await new WfsEndpoint(layer.url).isReady();
|
|
126
|
+
const featureTypeSummary = endpoint.getFeatureTypeSummary(layer.featureType);
|
|
127
|
+
const boundingBox = featureTypeSummary?.boundingBox;
|
|
128
|
+
if (!boundingBox) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
extent: boundingBox,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function getGeotiffLayerExtent(
|
|
137
|
+
layer: MapContextLayerGeotiff,
|
|
138
|
+
): Promise<ViewByExtent | null> {
|
|
139
|
+
const source = new GeoTIFF({
|
|
140
|
+
sources: [{ url: layer.url }],
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return new Promise((resolve) => {
|
|
144
|
+
source.on("change", () => {
|
|
145
|
+
if (source.getState() === "ready") {
|
|
146
|
+
const tileGrid = source.getTileGrid();
|
|
147
|
+
const projection = source.getProjection();
|
|
148
|
+
if (!tileGrid || !projection) {
|
|
149
|
+
resolve(null);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const extent = transformExtent(
|
|
153
|
+
tileGrid.getExtent(),
|
|
154
|
+
projection,
|
|
155
|
+
"EPSG:4326",
|
|
156
|
+
) as Extent;
|
|
157
|
+
resolve({ extent });
|
|
158
|
+
} else if (source.getState() === "error") {
|
|
159
|
+
resolve(null);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@geospatial-sdk/core",
|
|
3
|
-
"version": "0.0.5-dev.0
|
|
3
|
+
"version": "0.0.5-dev.0.sha-5830a6a",
|
|
4
4
|
"description": "Core functions and models for the SDK",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"geospatial",
|
|
7
|
+
"sdk"
|
|
8
|
+
],
|
|
5
9
|
"author": "Olivia <olivia.guyot@camptocamp.com>",
|
|
6
|
-
"homepage": "",
|
|
10
|
+
"homepage": "https://github.com/camptocamp/geospatial-sdk#readme",
|
|
7
11
|
"license": "BSD-3-Clause",
|
|
8
12
|
"main": "dist/index.js",
|
|
9
13
|
"typings": "dist/index.d.ts",
|
|
@@ -18,9 +22,22 @@
|
|
|
18
22
|
"lib",
|
|
19
23
|
"dist"
|
|
20
24
|
],
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/camptocamp/geospatial-sdk.git"
|
|
28
|
+
},
|
|
21
29
|
"scripts": {
|
|
22
30
|
"test": "vitest",
|
|
23
31
|
"build": "tsc"
|
|
24
32
|
},
|
|
25
|
-
"
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"proj4": "^2.9.2"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"ol": "^10.7.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"ol": ">9.x"
|
|
41
|
+
},
|
|
42
|
+
"gitHead": "5830a6a9360dc444c59307aa7304214a2082381d"
|
|
26
43
|
}
|