@geospatial-sdk/core 0.0.5-dev.0.sha-7ea9c6a → 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.
Files changed (46) hide show
  1. package/dist/constant/projections.d.ts +8 -0
  2. package/dist/constant/projections.d.ts.map +1 -1
  3. package/dist/constant/projections.js +8 -0
  4. package/dist/model/events.d.ts +3 -3
  5. package/dist/model/events.d.ts.map +1 -1
  6. package/dist/model/events.js +2 -3
  7. package/dist/model/map-context-diff.d.ts +7 -1
  8. package/dist/model/map-context-diff.d.ts.map +1 -1
  9. package/dist/model/map-context.d.ts +15 -5
  10. package/dist/model/map-context.d.ts.map +1 -1
  11. package/dist/model/resolved-map-state.d.ts +1 -1
  12. package/dist/model/resolved-map-state.d.ts.map +1 -1
  13. package/dist/utils/coordinates.d.ts +52 -0
  14. package/dist/utils/coordinates.d.ts.map +1 -0
  15. package/dist/utils/coordinates.js +91 -0
  16. package/dist/utils/crs.d.ts +30 -0
  17. package/dist/utils/crs.d.ts.map +1 -0
  18. package/dist/utils/crs.js +83 -0
  19. package/dist/utils/geojson.d.ts +16 -0
  20. package/dist/utils/geojson.d.ts.map +1 -0
  21. package/dist/utils/geojson.js +118 -0
  22. package/dist/utils/index.d.ts +3 -0
  23. package/dist/utils/index.d.ts.map +1 -1
  24. package/dist/utils/index.js +3 -0
  25. package/dist/utils/map-context-diff.d.ts.map +1 -1
  26. package/dist/utils/map-context-diff.js +10 -0
  27. package/dist/utils/view.d.ts +2 -2
  28. package/dist/utils/view.d.ts.map +1 -1
  29. package/dist/utils/view.js +16 -29
  30. package/lib/constant/projections.ts +10 -0
  31. package/lib/model/events.ts +4 -4
  32. package/lib/model/map-context-diff.ts +8 -1
  33. package/lib/model/map-context.ts +19 -5
  34. package/lib/model/resolved-map-state.ts +1 -1
  35. package/lib/utils/coordinates.test.ts +172 -0
  36. package/lib/utils/coordinates.ts +157 -0
  37. package/lib/utils/crs.test.ts +128 -0
  38. package/lib/utils/crs.ts +102 -0
  39. package/lib/utils/geojson.test.ts +208 -0
  40. package/lib/utils/geojson.ts +133 -0
  41. package/lib/utils/index.ts +14 -0
  42. package/lib/utils/map-context-diff.test.ts +68 -1
  43. package/lib/utils/map-context-diff.ts +11 -0
  44. package/lib/utils/view.test.ts +23 -0
  45. package/lib/utils/view.ts +19 -44
  46. 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,
@@ -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 geographic extent. The returned extent is
4
- * always expressed in EPSG:4326 (longitude/latitude) coordinates.
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
@@ -1 +1 @@
1
- {"version":3,"file":"view.d.ts","sourceRoot":"","sources":["../../lib/utils/view.ts"],"names":[],"mappings":"AAQA,OAAO,EAEL,eAAe,EAKf,cAAc,EAEf,MAAM,mBAAmB,CAAC;AAK3B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAchC"}
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"}
@@ -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
- const GEOJSON = new GeoJSON();
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 geographic extent. The returned extent is
11
- * always expressed in EPSG:4326 (longitude/latitude) coordinates.
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 computeExtentFromGeojson(layer.data);
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
- else {
77
- const availableEPSGCode = Object.keys(boundingBoxes)[0];
78
- const proj = await fromEPSGCode(availableEPSGCode);
79
- return {
80
- extent: transformExtent(boundingBoxes[availableEPSGCode], proj, "EPSG:4326"),
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 extent = transformExtent(tileGrid.getExtent(), projection, "EPSG:4326");
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";
@@ -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 extends BaseEvent {
80
+ export class SourceLoadErrorEvent {
81
+ type: typeof SourceLoadErrorType;
82
82
  message: string;
83
83
  httpStatus?: number;
84
84
  constructor(error: EndpointError | Error | Response) {
85
- super(SourceLoadErrorType);
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?: MapContextView | null; // this is both optional (meaning no view change) and nullable (meaning reset to default view)
52
+ viewChanges?: MapContextViewDiff; // if this property is omitted, the view hasn't changed
46
53
  }
@@ -237,15 +237,24 @@ export type MapContextLayer =
237
237
  | MapContextLayerMapLibreStyle
238
238
  | MapContextLayerGeotiff;
239
239
 
240
- export type Coordinate = [number, number];
240
+ export type CrsIdentifier = string;
241
241
 
242
242
  /**
243
- * Array components are respectively: minX, minY, maxX, maxY
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 Extent = [number, number, number, number];
246
+ export type Coordinate = [number, number] | [number, number, CrsIdentifier];
246
247
 
247
248
  /**
248
- * @property center Expressed in longitude/latitude
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 longitude/latitude */
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
+ }