@geospatial-sdk/core 0.0.5-dev.0.sha-9f850cf → 0.0.5-dev.0.sha-ba0cd0b
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/dist/constant/projections.d.ts +8 -0
- package/dist/constant/projections.d.ts.map +1 -1
- package/dist/constant/projections.js +8 -0
- package/dist/model/events.d.ts +3 -3
- package/dist/model/events.d.ts.map +1 -1
- package/dist/model/events.js +2 -3
- package/dist/model/map-context-diff.d.ts +7 -1
- package/dist/model/map-context-diff.d.ts.map +1 -1
- package/dist/model/map-context.d.ts +15 -5
- package/dist/model/map-context.d.ts.map +1 -1
- package/dist/model/resolved-map-state.d.ts +1 -1
- package/dist/model/resolved-map-state.d.ts.map +1 -1
- package/dist/utils/coordinates.d.ts +52 -0
- package/dist/utils/coordinates.d.ts.map +1 -0
- package/dist/utils/coordinates.js +91 -0
- package/dist/utils/crs.d.ts +30 -0
- package/dist/utils/crs.d.ts.map +1 -0
- package/dist/utils/crs.js +83 -0
- package/dist/utils/geojson.d.ts +16 -0
- package/dist/utils/geojson.d.ts.map +1 -0
- package/dist/utils/geojson.js +118 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -0
- package/dist/utils/map-context-diff.d.ts.map +1 -1
- package/dist/utils/map-context-diff.js +10 -0
- package/dist/utils/view.d.ts +2 -2
- package/dist/utils/view.d.ts.map +1 -1
- package/dist/utils/view.js +16 -29
- package/lib/constant/projections.ts +10 -0
- package/lib/model/events.ts +4 -4
- package/lib/model/map-context-diff.ts +8 -1
- package/lib/model/map-context.ts +19 -5
- package/lib/model/resolved-map-state.ts +1 -1
- package/lib/utils/coordinates.test.ts +172 -0
- package/lib/utils/coordinates.ts +157 -0
- package/lib/utils/crs.test.ts +128 -0
- package/lib/utils/crs.ts +102 -0
- package/lib/utils/geojson.test.ts +208 -0
- package/lib/utils/geojson.ts +133 -0
- package/lib/utils/index.ts +14 -0
- package/lib/utils/map-context-diff.test.ts +68 -1
- package/lib/utils/map-context-diff.ts +11 -0
- package/lib/utils/view.test.ts +23 -0
- package/lib/utils/view.ts +19 -44
- package/package.json +2 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isLayerSame, isLayerSameAndUnchanged } from "./map-context-layer.js";
|
|
2
|
+
import { DEFAULT_MAP_VIEW_CRS } from "../constant/projections.js";
|
|
2
3
|
/**
|
|
3
4
|
* The following logic is produced by identifying layers in both context
|
|
4
5
|
* and determining whether they have been added, removed, changed or reordered.
|
|
@@ -76,6 +77,15 @@ export function computeMapContextDiff(nextContext, previousContext) {
|
|
|
76
77
|
let viewChanges = nextContext.view !== previousContext.view ? nextContext.view : undefined;
|
|
77
78
|
if (viewChanges !== null && viewChanges !== undefined) {
|
|
78
79
|
viewChanges = { ...viewChanges }; // copy the view to avoid unexpected mutations
|
|
80
|
+
// only keep the projection if it changed between the two contexts
|
|
81
|
+
const prevProjection = previousContext.view?.projection ?? DEFAULT_MAP_VIEW_CRS;
|
|
82
|
+
const nextProjection = nextContext.view?.projection ?? DEFAULT_MAP_VIEW_CRS;
|
|
83
|
+
if (nextProjection === prevProjection) {
|
|
84
|
+
delete viewChanges.projection;
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
viewChanges.projection = nextProjection;
|
|
88
|
+
}
|
|
79
89
|
}
|
|
80
90
|
return {
|
|
81
91
|
layersAdded,
|
package/dist/utils/view.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MapContextLayer, MapContextView } from "../model/index.js";
|
|
2
2
|
/**
|
|
3
|
-
* Creates a view from a layer by extracting its
|
|
4
|
-
*
|
|
3
|
+
* Creates a view from a layer by extracting its extent. The returned extent is preferably
|
|
4
|
+
* returned in EPSG:4326; if not available, it will be returned in a CRS available for this layer
|
|
5
5
|
*
|
|
6
6
|
* @param layer - The map context layer to extract the extent from
|
|
7
7
|
* @returns A Promise resolving to a `ViewByExtent` object, or `null` if the extent cannot be determined
|
package/dist/utils/view.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../lib/utils/view.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../lib/utils/view.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,eAAe,EAKf,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAI3B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAgBhC"}
|
package/dist/utils/view.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import { transformExtent } from "ol/proj.js";
|
|
2
1
|
import { WfsEndpoint, WmsEndpoint, WmtsEndpoint } from "@camptocamp/ogc-client";
|
|
3
2
|
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
3
|
import GeoTIFF from "ol/source/GeoTIFF.js";
|
|
8
|
-
|
|
4
|
+
import { ensureGeojson, getExtentFromGeojson } from "./geojson.js";
|
|
5
|
+
import { setCrs, transform } from "./coordinates.js";
|
|
9
6
|
/**
|
|
10
|
-
* Creates a view from a layer by extracting its
|
|
11
|
-
*
|
|
7
|
+
* Creates a view from a layer by extracting its extent. The returned extent is preferably
|
|
8
|
+
* returned in EPSG:4326; if not available, it will be returned in a CRS available for this layer
|
|
12
9
|
*
|
|
13
10
|
* @param layer - The map context layer to extract the extent from
|
|
14
11
|
* @returns A Promise resolving to a `ViewByExtent` object, or `null` if the extent cannot be determined
|
|
@@ -36,7 +33,9 @@ export async function createViewFromLayer(layer) {
|
|
|
36
33
|
return await getWmtsLayerExtent(layer);
|
|
37
34
|
}
|
|
38
35
|
else if (layer.type === "geojson" && layer.data) {
|
|
39
|
-
return
|
|
36
|
+
return {
|
|
37
|
+
extent: getExtentFromGeojson(ensureGeojson(layer.data)),
|
|
38
|
+
};
|
|
40
39
|
}
|
|
41
40
|
else if (layer.type === "wfs") {
|
|
42
41
|
return await getWfsLayerExtent(layer);
|
|
@@ -48,19 +47,6 @@ export async function createViewFromLayer(layer) {
|
|
|
48
47
|
throw new Error(`Unsupported layer type: ${layer.type}`);
|
|
49
48
|
}
|
|
50
49
|
}
|
|
51
|
-
function computeExtentFromGeojson(data) {
|
|
52
|
-
const geojson = typeof data === "string" ? JSON.parse(data) : data;
|
|
53
|
-
const features = GEOJSON.readFeatures(geojson);
|
|
54
|
-
const extent = features.reduce((prev, curr) => {
|
|
55
|
-
const geom = curr.getGeometry();
|
|
56
|
-
if (!geom)
|
|
57
|
-
return prev;
|
|
58
|
-
return extend(prev, geom.getExtent());
|
|
59
|
-
}, [Infinity, Infinity, -Infinity, -Infinity]);
|
|
60
|
-
return {
|
|
61
|
-
extent,
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
50
|
async function getWmsLayerExtent(layer) {
|
|
65
51
|
const endpoint = await new WmsEndpoint(layer.url).isReady();
|
|
66
52
|
const { boundingBoxes } = endpoint.getLayerByName(layer.name);
|
|
@@ -68,18 +54,17 @@ async function getWmsLayerExtent(layer) {
|
|
|
68
54
|
return null;
|
|
69
55
|
}
|
|
70
56
|
const lonLatCRS = Object.keys(boundingBoxes).find((crs) => LONLAT_CRS_CODES.includes(crs));
|
|
57
|
+
// give priority to lon/lat bounding boxes
|
|
71
58
|
if (lonLatCRS) {
|
|
72
59
|
return {
|
|
73
60
|
extent: boundingBoxes[lonLatCRS],
|
|
74
61
|
};
|
|
75
62
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
};
|
|
82
|
-
}
|
|
63
|
+
// otherwise take the first extent and apply its CRS
|
|
64
|
+
const availableEPSGCode = Object.keys(boundingBoxes)[0];
|
|
65
|
+
return {
|
|
66
|
+
extent: [...boundingBoxes[availableEPSGCode], availableEPSGCode],
|
|
67
|
+
};
|
|
83
68
|
}
|
|
84
69
|
async function getWmtsLayerExtent(layer) {
|
|
85
70
|
const endpoint = await new WmtsEndpoint(layer.url).isReady();
|
|
@@ -102,6 +87,7 @@ async function getWfsLayerExtent(layer) {
|
|
|
102
87
|
extent: boundingBox,
|
|
103
88
|
};
|
|
104
89
|
}
|
|
90
|
+
// TODO: DO NOT RELY ON OL FOR THIS!
|
|
105
91
|
async function getGeotiffLayerExtent(layer) {
|
|
106
92
|
const source = new GeoTIFF({
|
|
107
93
|
sources: [{ url: layer.url }],
|
|
@@ -115,7 +101,8 @@ async function getGeotiffLayerExtent(layer) {
|
|
|
115
101
|
resolve(null);
|
|
116
102
|
return;
|
|
117
103
|
}
|
|
118
|
-
const
|
|
104
|
+
const projectionCode = projection.getCode();
|
|
105
|
+
const extent = transform(setCrs(tileGrid.getExtent(), projectionCode), "EPSG:4326");
|
|
119
106
|
resolve({ extent });
|
|
120
107
|
}
|
|
121
108
|
else if (source.getState() === "error") {
|
|
@@ -1 +1,11 @@
|
|
|
1
1
|
export const LONLAT_CRS_CODES = ["EPSG:4326", "CRS:84"];
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* This CRS is considered if no CRS is specified in a coordinate or extent
|
|
5
|
+
*/
|
|
6
|
+
export const DEFAULT_CRS = "EPSG:4326";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This CRS is considered if no CRS is specified in a map view context
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MAP_VIEW_CRS = "EPSG:3857";
|
package/lib/model/events.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { EndpointError } from "@camptocamp/ogc-client";
|
|
2
2
|
import { Feature } from "geojson";
|
|
3
|
-
import BaseEvent from "ol/events/Event.js";
|
|
4
|
-
import type { Extent } from "ol/extent.js";
|
|
5
3
|
import {
|
|
6
4
|
ResolvedMapLayerState,
|
|
7
5
|
ResolvedMapState,
|
|
8
6
|
ResolvedMapViewState,
|
|
9
7
|
} from "./resolved-map-state.js";
|
|
8
|
+
import { Extent } from "./map-context.js";
|
|
10
9
|
|
|
11
10
|
export type FeaturesByLayerIndex = Map<number, Feature[]>;
|
|
12
11
|
|
|
@@ -78,11 +77,12 @@ export interface MapExtentChangeEvent {
|
|
|
78
77
|
* Use the MapLayerStateChangeEvent and LayerLoadingErrorEvent instead
|
|
79
78
|
*/
|
|
80
79
|
export const SourceLoadErrorType = "source-load-error";
|
|
81
|
-
export class SourceLoadErrorEvent
|
|
80
|
+
export class SourceLoadErrorEvent {
|
|
81
|
+
type: typeof SourceLoadErrorType;
|
|
82
82
|
message: string;
|
|
83
83
|
httpStatus?: number;
|
|
84
84
|
constructor(error: EndpointError | Error | Response) {
|
|
85
|
-
|
|
85
|
+
this.type = SourceLoadErrorType;
|
|
86
86
|
if (error instanceof Response) {
|
|
87
87
|
this.message = error.statusText;
|
|
88
88
|
this.httpStatus = error.status;
|
|
@@ -27,6 +27,13 @@ export interface MapContextLayerChanged {
|
|
|
27
27
|
position: number;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* If the `projection` field is omitted, it means that the view projection should not be changed.
|
|
32
|
+
* A return to the default map view projection should be explicitly stated in the `projection` property.
|
|
33
|
+
* `null` means reset to the default view.
|
|
34
|
+
*/
|
|
35
|
+
export type MapContextViewDiff = MapContextView | null;
|
|
36
|
+
|
|
30
37
|
/**
|
|
31
38
|
* Describes a delta between two contexts, in order to be
|
|
32
39
|
* applied to an existing map.
|
|
@@ -42,5 +49,5 @@ export interface MapContextDiff {
|
|
|
42
49
|
layersReordered: MapContextLayerReordered[];
|
|
43
50
|
layersRemoved: MapContextLayerPositioned[];
|
|
44
51
|
layersAdded: MapContextLayerPositioned[];
|
|
45
|
-
viewChanges?:
|
|
52
|
+
viewChanges?: MapContextViewDiff; // if this property is omitted, the view hasn't changed
|
|
46
53
|
}
|
package/lib/model/map-context.ts
CHANGED
|
@@ -237,15 +237,24 @@ export type MapContextLayer =
|
|
|
237
237
|
| MapContextLayerMapLibreStyle
|
|
238
238
|
| MapContextLayerGeotiff;
|
|
239
239
|
|
|
240
|
-
export type
|
|
240
|
+
export type CrsIdentifier = string;
|
|
241
241
|
|
|
242
242
|
/**
|
|
243
|
-
* Array components are respectively:
|
|
243
|
+
* Array components are respectively: x, y and optionally a CRS identifier
|
|
244
|
+
* (e.g. `"EPSG:2154"`). Without a CRS, coordinates are expressed in longitude/latitude.
|
|
244
245
|
*/
|
|
245
|
-
export type
|
|
246
|
+
export type Coordinate = [number, number] | [number, number, CrsIdentifier];
|
|
246
247
|
|
|
247
248
|
/**
|
|
248
|
-
*
|
|
249
|
+
* Array components are respectively: minX, minY, maxX, maxY and optionally a CRS identifier
|
|
250
|
+
* (e.g. `"EPSG:2154"`). Without a CRS, the extent is expressed in longitude/latitude.
|
|
251
|
+
*/
|
|
252
|
+
export type Extent =
|
|
253
|
+
| [number, number, number, number]
|
|
254
|
+
| [number, number, number, number, CrsIdentifier];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* @property center Expressed in longitude/latitude unless a CRS identifier is given as third element
|
|
249
258
|
* @property zoom
|
|
250
259
|
*/
|
|
251
260
|
export interface ViewByZoomAndCenter {
|
|
@@ -254,7 +263,7 @@ export interface ViewByZoomAndCenter {
|
|
|
254
263
|
}
|
|
255
264
|
|
|
256
265
|
/**
|
|
257
|
-
* @property extent Expressed in longitude/latitude
|
|
266
|
+
* @property extent Expressed in longitude/latitude unless a CRS identifier is given as fifth element
|
|
258
267
|
*/
|
|
259
268
|
export interface ViewByExtent {
|
|
260
269
|
extent: Extent;
|
|
@@ -280,6 +289,11 @@ export type MapContextView = (
|
|
|
280
289
|
) & {
|
|
281
290
|
maxZoom?: number;
|
|
282
291
|
maxExtent?: Extent;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* CRS in which the map is displayed (e.g. `"EPSG:2154"`); defaults to `"EPSG:3857"`.
|
|
295
|
+
*/
|
|
296
|
+
projection?: CrsIdentifier;
|
|
283
297
|
};
|
|
284
298
|
|
|
285
299
|
/**
|
|
@@ -43,7 +43,7 @@ export interface ResolvedMapViewState {
|
|
|
43
43
|
/** View center in longitude/latitude */
|
|
44
44
|
center: Coordinate;
|
|
45
45
|
|
|
46
|
-
/** Actual view extent in
|
|
46
|
+
/** Actual view extent in the current view projection */
|
|
47
47
|
extent: Extent;
|
|
48
48
|
|
|
49
49
|
/** Map units per pixel */
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import {
|
|
2
|
+
equals,
|
|
3
|
+
extend,
|
|
4
|
+
getCrs,
|
|
5
|
+
setCrs,
|
|
6
|
+
toGeographic,
|
|
7
|
+
transform,
|
|
8
|
+
} from "./coordinates.js";
|
|
9
|
+
import { Coordinate, Extent } from "../model/index.js";
|
|
10
|
+
|
|
11
|
+
describe("crs utils", () => {
|
|
12
|
+
describe("getCrs", () => {
|
|
13
|
+
it("defaults to EPSG:4326 when no CRS is given", () => {
|
|
14
|
+
expect(getCrs([1, 2])).toBe("EPSG:4326");
|
|
15
|
+
expect(getCrs([1, 2, 3, 4])).toBe("EPSG:4326");
|
|
16
|
+
});
|
|
17
|
+
it("reads the CRS from the last element", () => {
|
|
18
|
+
expect(getCrs([1, 2, "EPSG:2154"])).toBe("EPSG:2154");
|
|
19
|
+
expect(getCrs([1, 2, 3, 4, "EPSG:2154"])).toBe("EPSG:2154");
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("setCrs", () => {
|
|
24
|
+
it("applies the CRS to a coordinate without transforming it", () => {
|
|
25
|
+
expect(setCrs([2.3, 48.8], "EPSG:3857")).toEqual([
|
|
26
|
+
2.3,
|
|
27
|
+
48.8,
|
|
28
|
+
"EPSG:3857",
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("applies the CRS to an extent without transforming it", () => {
|
|
33
|
+
expect(setCrs([1, 2, 3, 4], "EPSG:3857")).toEqual([
|
|
34
|
+
1,
|
|
35
|
+
2,
|
|
36
|
+
3,
|
|
37
|
+
4,
|
|
38
|
+
"EPSG:3857",
|
|
39
|
+
]);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("replaces the CRS when one is already present", () => {
|
|
43
|
+
expect(setCrs([2.3, 48.8, "EPSG:4326"], "EPSG:3857")).toEqual([
|
|
44
|
+
2.3,
|
|
45
|
+
48.8,
|
|
46
|
+
"EPSG:3857",
|
|
47
|
+
]);
|
|
48
|
+
expect(setCrs([1, 2, 3, 4, "EPSG:4326"], "EPSG:3857")).toEqual([
|
|
49
|
+
1,
|
|
50
|
+
2,
|
|
51
|
+
3,
|
|
52
|
+
4,
|
|
53
|
+
"EPSG:3857",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns the original value when its CRS is unchanged", () => {
|
|
58
|
+
const coordinate = [2.3, 48.8, "EPSG:4326"] as Coordinate;
|
|
59
|
+
const extent = [1, 2, 3, 4, "EPSG:4326"] as Extent;
|
|
60
|
+
|
|
61
|
+
expect(setCrs(coordinate, "EPSG:4326")).toBe(coordinate);
|
|
62
|
+
expect(setCrs(extent, "EPSG:4326")).toBe(extent);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("extend", () => {
|
|
67
|
+
it("expands the first extent to include the second", () => {
|
|
68
|
+
const extent = [1, 2, 3, 4] as [number, number, number, number];
|
|
69
|
+
|
|
70
|
+
expect(extend(extent, [-1, 3, 2, 8])).toBe(extent);
|
|
71
|
+
expect(extent).toEqual([-1, 2, 3, 8]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("throws when extents use different CRSs", () => {
|
|
75
|
+
expect(() => extend([1, 2, 3, 4], [5, 6, 7, 8, "EPSG:3857"])).toThrow(
|
|
76
|
+
"Cannot combine extents with different CRS",
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("transform", () => {
|
|
82
|
+
it("returns the input unchanged when it is already in the target CRS", () => {
|
|
83
|
+
const coordinate = [2.3, 48.8, "EPSG:4326"] as Coordinate;
|
|
84
|
+
expect(transform(coordinate, "EPSG:4326")).toBe(coordinate);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("transforms a coordinate to the target CRS", () => {
|
|
88
|
+
const result = transform([1, 1], "EPSG:3857");
|
|
89
|
+
|
|
90
|
+
expect(result[0]).toBeCloseTo(111319.4908);
|
|
91
|
+
expect(result[1]).toBeCloseTo(111325.1429);
|
|
92
|
+
expect(result[2]).toBe("EPSG:3857");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("transforms an extent to the target CRS", () => {
|
|
96
|
+
const result = transform([0, 0, 1, 1], "EPSG:3857");
|
|
97
|
+
|
|
98
|
+
expect(result[0]).toBeCloseTo(0);
|
|
99
|
+
expect(result[1]).toBeCloseTo(0);
|
|
100
|
+
expect(result[2]).toBeCloseTo(111319.4908);
|
|
101
|
+
expect(result[3]).toBeCloseTo(111325.1429);
|
|
102
|
+
expect(result[4]).toBe("EPSG:3857");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("throws an error when the CRS cannot be converted", () => {
|
|
106
|
+
expect(() => transform([1, 2, "EPSG:999999"], "EPSG:4326")).toThrow(
|
|
107
|
+
"Cannot convert from EPSG:999999 to EPSG:4326",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("toGeographic", () => {
|
|
113
|
+
it("returns coordinates and extents in EPSG:4326 and strips the CRS identifier", () => {
|
|
114
|
+
expect(toGeographic([2.3, 48.8])).toEqual([2.3, 48.8]);
|
|
115
|
+
expect(toGeographic([2.3, 48.8, "EPSG:4326"])).toEqual([2.3, 48.8]);
|
|
116
|
+
expect(toGeographic([2.3, 48.8, 3.3, 49.8])).toEqual([
|
|
117
|
+
2.3, 48.8, 3.3, 49.8,
|
|
118
|
+
]);
|
|
119
|
+
expect(toGeographic([2.3, 48.8, 3.3, 49.8, "EPSG:4326"])).toEqual([
|
|
120
|
+
2.3, 48.8, 3.3, 49.8,
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("converts a coordinate from its declared CRS to EPSG:4326", () => {
|
|
125
|
+
const result = toGeographic([
|
|
126
|
+
111319.49079327357,
|
|
127
|
+
111325.1428663851,
|
|
128
|
+
"EPSG:3857",
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
expect(result[0]).toBeCloseTo(1);
|
|
132
|
+
expect(result[1]).toBeCloseTo(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("converts an extent from its declared CRS to EPSG:4326", () => {
|
|
136
|
+
const result = toGeographic([
|
|
137
|
+
0,
|
|
138
|
+
0,
|
|
139
|
+
111319.49079327357,
|
|
140
|
+
111325.1428663851,
|
|
141
|
+
"EPSG:3857",
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
expect(result[0]).toBeCloseTo(0);
|
|
145
|
+
expect(result[1]).toBeCloseTo(0);
|
|
146
|
+
expect(result[2]).toBeCloseTo(1);
|
|
147
|
+
expect(result[3]).toBeCloseTo(1);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("throws an error when the CRS cannot be converted", () => {
|
|
151
|
+
expect(() => toGeographic([1, 2, "EPSG:999999"])).toThrow(
|
|
152
|
+
"Cannot convert from EPSG:999999 to EPSG:4326",
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("equals", () => {
|
|
158
|
+
it("compares coordinates and their CRS", () => {
|
|
159
|
+
expect(equals([2.3, 48.8], [2.3, 48.8])).toBe(true);
|
|
160
|
+
expect(equals([2.3, 48.8], [2.3, 48.8, "EPSG:4326"])).toBe(true);
|
|
161
|
+
expect(equals([2.3, 48.8], [2.4, 48.8])).toBe(false);
|
|
162
|
+
expect(equals([2.3, 48.8], [2.3, 48.8, "EPSG:3857"])).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("compares extents and their CRS", () => {
|
|
166
|
+
expect(equals([1, 2, 3, 4], [1, 2, 3, 4])).toBe(true);
|
|
167
|
+
expect(equals([1, 2, 3, 4], [1, 2, 3, 4, "EPSG:4326"])).toBe(true);
|
|
168
|
+
expect(equals([1, 2, 3, 4], [1, 2, 3, 5])).toBe(false);
|
|
169
|
+
expect(equals([1, 2, 3, 4], [1, 2, 3, 4, "EPSG:3857"])).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { DEFAULT_CRS } from "../constant/projections.js";
|
|
2
|
+
import { Coordinate, CrsIdentifier, Extent } from "../model/map-context.js";
|
|
3
|
+
import proj4 from "proj4";
|
|
4
|
+
|
|
5
|
+
function isCoordinate(value: Coordinate | Extent): value is Coordinate {
|
|
6
|
+
return value.length < 4;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns the CRS of a coordinate or extent; defaults to EPSG:4326 (longitude/latitude)
|
|
11
|
+
*/
|
|
12
|
+
export function getCrs(value: Coordinate | Extent): string {
|
|
13
|
+
const last = value[value.length - 1];
|
|
14
|
+
return typeof last === "string" ? last : DEFAULT_CRS;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Apply a CRS identifier to a coordinate; THIS DOES NOT TRANSFORM COORDINATES
|
|
19
|
+
* @param value
|
|
20
|
+
* @param crs
|
|
21
|
+
*/
|
|
22
|
+
export function setCrs(value: Coordinate, crs: CrsIdentifier): Coordinate;
|
|
23
|
+
/**
|
|
24
|
+
* Apply a CRS identifier to an extent; THIS DOES NOT TRANSFORM COORDINATES
|
|
25
|
+
* @param value
|
|
26
|
+
* @param crs
|
|
27
|
+
*/
|
|
28
|
+
export function setCrs(value: Extent, crs: CrsIdentifier): Extent;
|
|
29
|
+
export function setCrs(
|
|
30
|
+
value: Coordinate | Extent,
|
|
31
|
+
crs: CrsIdentifier,
|
|
32
|
+
): Coordinate | Extent {
|
|
33
|
+
const currentCrs = getCrs(value);
|
|
34
|
+
if (currentCrs === crs) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (isCoordinate(value)) {
|
|
38
|
+
return [...value.slice(0, 2), crs] as Coordinate;
|
|
39
|
+
}
|
|
40
|
+
return [...value.slice(0, 4), crs] as Extent;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function equals(value1: Coordinate, value2: Coordinate): boolean;
|
|
44
|
+
export function equals(value1: Extent, value2: Extent): boolean;
|
|
45
|
+
export function equals(
|
|
46
|
+
value1: Coordinate | Extent,
|
|
47
|
+
value2: Coordinate | Extent,
|
|
48
|
+
): boolean {
|
|
49
|
+
const crs1 = getCrs(value1);
|
|
50
|
+
const crs2 = getCrs(value2);
|
|
51
|
+
const isCoordinate = value1.length < 4;
|
|
52
|
+
if (isCoordinate) {
|
|
53
|
+
return crs1 === crs2 && value1[0] === value2[0] && value1[1] === value2[1];
|
|
54
|
+
}
|
|
55
|
+
return (
|
|
56
|
+
crs1 === crs2 &&
|
|
57
|
+
value1[0] === value2[0] &&
|
|
58
|
+
value1[2] === value2[2] &&
|
|
59
|
+
value1[1] === value2[1] &&
|
|
60
|
+
value1[3] === value2[3]
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* first extent will be modified
|
|
66
|
+
* @param extent1
|
|
67
|
+
* @param extent2
|
|
68
|
+
*/
|
|
69
|
+
export function extend(extent1: Extent, extent2: Extent): Extent {
|
|
70
|
+
const crs1 = getCrs(extent1);
|
|
71
|
+
const crs2 = getCrs(extent2);
|
|
72
|
+
if (crs1 !== crs2) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`Cannot combine extents with different CRS: ${crs1} and ${crs2}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (extent2[0] < extent1[0]) {
|
|
78
|
+
extent1[0] = extent2[0];
|
|
79
|
+
}
|
|
80
|
+
if (extent2[2] > extent1[2]) {
|
|
81
|
+
extent1[2] = extent2[2];
|
|
82
|
+
}
|
|
83
|
+
if (extent2[1] < extent1[1]) {
|
|
84
|
+
extent1[1] = extent2[1];
|
|
85
|
+
}
|
|
86
|
+
if (extent2[3] > extent1[3]) {
|
|
87
|
+
extent1[3] = extent2[3];
|
|
88
|
+
}
|
|
89
|
+
return extent1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Project an extent into another CRS
|
|
94
|
+
* @param value
|
|
95
|
+
* @param toCrs
|
|
96
|
+
*/
|
|
97
|
+
export function transform(value: Extent, toCrs: CrsIdentifier): Extent;
|
|
98
|
+
/**
|
|
99
|
+
* Project a coordinate into another CRS
|
|
100
|
+
* @param value
|
|
101
|
+
* @param toCrs
|
|
102
|
+
*/
|
|
103
|
+
export function transform(value: Coordinate, toCrs: CrsIdentifier): Coordinate;
|
|
104
|
+
export function transform(
|
|
105
|
+
value: Coordinate | Extent,
|
|
106
|
+
toCrs: CrsIdentifier,
|
|
107
|
+
): Coordinate | Extent {
|
|
108
|
+
const crs = getCrs(value);
|
|
109
|
+
if (crs === toCrs) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
// convert using proj4
|
|
113
|
+
try {
|
|
114
|
+
if (isCoordinate(value)) {
|
|
115
|
+
const coordinate = value.slice(0, 2) as [number, number];
|
|
116
|
+
return setCrs(proj4(crs, toCrs, coordinate), toCrs);
|
|
117
|
+
} else {
|
|
118
|
+
const [minX, minY, maxX, maxY] = value.slice(0, 4) as [
|
|
119
|
+
number,
|
|
120
|
+
number,
|
|
121
|
+
number,
|
|
122
|
+
number,
|
|
123
|
+
];
|
|
124
|
+
// TODO: use finer logic for transforming extents, currently this can lead very often to wrong results
|
|
125
|
+
const [lonMin, latMin] = proj4(crs, toCrs, [minX, minY]);
|
|
126
|
+
const [lonMax, latMax] = proj4(crs, toCrs, [maxX, maxY]);
|
|
127
|
+
return setCrs([lonMin, latMin, lonMax, latMax], toCrs);
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Cannot convert from ${crs} to ${toCrs}: ${(e as Error)?.message ?? e}`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns another extent which is in geographic coordinates (EPSG:4326)
|
|
138
|
+
* @param value
|
|
139
|
+
*/
|
|
140
|
+
export function toGeographic(value: Extent): Extract<Extent, { length: 4 }>;
|
|
141
|
+
/**
|
|
142
|
+
* Returns another coordinate which is in geographic coordinates (EPSG:4326)
|
|
143
|
+
* @param value
|
|
144
|
+
*/
|
|
145
|
+
export function toGeographic(
|
|
146
|
+
value: Coordinate,
|
|
147
|
+
): Extract<Coordinate, { length: 2 }>;
|
|
148
|
+
export function toGeographic(
|
|
149
|
+
value: Coordinate | Extent,
|
|
150
|
+
): Extract<Coordinate, { length: 2 }> | Extract<Extent, { length: 4 }> {
|
|
151
|
+
if (isCoordinate(value)) {
|
|
152
|
+
const transformed = transform(value, "EPSG:4326") as Coordinate;
|
|
153
|
+
return transformed.slice(0, 2) as [number, number];
|
|
154
|
+
}
|
|
155
|
+
const transformed = transform(value as Extent, "EPSG:4326") as Extent;
|
|
156
|
+
return transformed.slice(0, 4) as [number, number, number, number];
|
|
157
|
+
}
|