@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.
Files changed (67) hide show
  1. package/README.md +38 -5
  2. package/dist/constant/projections.d.ts +2 -0
  3. package/dist/constant/projections.d.ts.map +1 -0
  4. package/dist/constant/projections.js +1 -0
  5. package/dist/index.d.ts +7 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +7 -3
  8. package/dist/model/events.d.ts +86 -0
  9. package/dist/model/events.d.ts.map +1 -0
  10. package/dist/model/events.js +37 -0
  11. package/dist/model/index.d.ts +9 -2
  12. package/dist/model/index.d.ts.map +1 -1
  13. package/dist/model/index.js +9 -2
  14. package/dist/model/map-context-diff.d.ts +11 -3
  15. package/dist/model/map-context-diff.d.ts.map +1 -1
  16. package/dist/model/map-context.d.ts +174 -22
  17. package/dist/model/map-context.d.ts.map +1 -1
  18. package/dist/model/resolved-map-state.d.ts +49 -0
  19. package/dist/model/resolved-map-state.d.ts.map +1 -0
  20. package/dist/model/resolved-map-state.js +1 -0
  21. package/dist/model/style.d.ts +3 -0
  22. package/dist/model/style.d.ts.map +1 -0
  23. package/dist/model/style.js +1 -0
  24. package/dist/utils/hash.d.ts +2 -0
  25. package/dist/utils/hash.d.ts.map +1 -0
  26. package/dist/utils/hash.js +28 -0
  27. package/dist/utils/index.d.ts +12 -4
  28. package/dist/utils/index.d.ts.map +1 -1
  29. package/dist/utils/index.js +12 -4
  30. package/dist/utils/map-context-diff.d.ts +1 -1
  31. package/dist/utils/map-context-diff.d.ts.map +1 -1
  32. package/dist/utils/map-context-diff.js +19 -11
  33. package/dist/utils/map-context-layer.d.ts +13 -0
  34. package/dist/utils/map-context-layer.d.ts.map +1 -0
  35. package/dist/utils/map-context-layer.js +44 -0
  36. package/dist/utils/map-context.d.ts +48 -4
  37. package/dist/utils/map-context.d.ts.map +1 -1
  38. package/dist/utils/map-context.js +80 -29
  39. package/dist/utils/style.d.ts +4 -0
  40. package/dist/utils/style.d.ts.map +1 -0
  41. package/dist/utils/style.js +68 -0
  42. package/dist/utils/view.d.ts +25 -0
  43. package/dist/utils/view.d.ts.map +1 -0
  44. package/dist/utils/view.js +126 -0
  45. package/lib/constant/projections.ts +1 -0
  46. package/lib/index.ts +8 -3
  47. package/lib/model/events.ts +113 -0
  48. package/lib/model/index.ts +10 -2
  49. package/lib/model/map-context-diff.ts +12 -3
  50. package/lib/model/map-context.ts +219 -26
  51. package/lib/model/resolved-map-state.ts +67 -0
  52. package/lib/model/style.ts +3 -0
  53. package/lib/utils/freeze.test.ts +1 -1
  54. package/lib/utils/hash.test.ts +180 -0
  55. package/lib/utils/hash.ts +28 -0
  56. package/lib/utils/index.ts +20 -4
  57. package/lib/utils/map-context-diff.test.ts +240 -10
  58. package/lib/utils/map-context-diff.ts +23 -14
  59. package/lib/utils/map-context-layer.test.ts +357 -0
  60. package/lib/utils/map-context-layer.ts +66 -0
  61. package/lib/utils/map-context.test.ts +136 -272
  62. package/lib/utils/map-context.ts +108 -39
  63. package/lib/utils/style.ts +71 -0
  64. package/lib/utils/url.test.ts +1 -1
  65. package/lib/utils/view.test.ts +146 -0
  66. package/lib/utils/view.ts +163 -0
  67. 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+97d2685",
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
- "gitHead": "97d2685498a9b6208d833a15480a5a9ca804e12f"
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
  }