@geospatial-sdk/core 0.0.5-dev.0.sha-7ea9c6a → 0.0.5-dev.0.sha-9c03c7f
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
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import proj4 from "proj4";
|
|
2
|
+
import {
|
|
3
|
+
ensureContextCrsReady,
|
|
4
|
+
ensureCrsRegistered,
|
|
5
|
+
ensureDiffCrsReady,
|
|
6
|
+
getContextCrsCodes,
|
|
7
|
+
getDiffCrsCodes,
|
|
8
|
+
getViewCrsCodes,
|
|
9
|
+
} from "./crs.js";
|
|
10
|
+
import { MapContextDiff } from "../model/map-context-diff.js";
|
|
11
|
+
|
|
12
|
+
const emptyDiff: MapContextDiff = {
|
|
13
|
+
layersAdded: [],
|
|
14
|
+
layersRemoved: [],
|
|
15
|
+
layersChanged: [],
|
|
16
|
+
layersReordered: [],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe("crs utils", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
vi.unstubAllGlobals();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("scanning", () => {
|
|
25
|
+
it("scans a view", () => {
|
|
26
|
+
expect(
|
|
27
|
+
getViewCrsCodes({
|
|
28
|
+
center: [1, 2, "EPSG:2154"],
|
|
29
|
+
zoom: 3,
|
|
30
|
+
projection: "EPSG:3857",
|
|
31
|
+
maxExtent: [0, 0, 1, 1, "EPSG:32631"],
|
|
32
|
+
}),
|
|
33
|
+
).toEqual(new Set(["EPSG:2154", "EPSG:3857", "EPSG:32631"]));
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("scans a context", () => {
|
|
37
|
+
expect(
|
|
38
|
+
getContextCrsCodes({
|
|
39
|
+
layers: [],
|
|
40
|
+
view: { extent: [0, 0, 1, 1, "EPSG:2154"], projection: "EPSG:3857" },
|
|
41
|
+
}),
|
|
42
|
+
).toEqual(new Set(["EPSG:2154", "EPSG:3857"]));
|
|
43
|
+
expect(getContextCrsCodes({ layers: [], view: null })).toEqual(new Set());
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("scans a diff", () => {
|
|
47
|
+
expect(
|
|
48
|
+
getDiffCrsCodes({
|
|
49
|
+
...emptyDiff,
|
|
50
|
+
viewChanges: {
|
|
51
|
+
center: [0, 0, "EPSG:2154"],
|
|
52
|
+
zoom: 1,
|
|
53
|
+
projection: "EPSG:3857",
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
).toEqual(new Set(["EPSG:2154", "EPSG:3857"]));
|
|
57
|
+
expect(getDiffCrsCodes(emptyDiff)).toEqual(new Set());
|
|
58
|
+
expect(getDiffCrsCodes({ ...emptyDiff, viewChanges: null })).toEqual(
|
|
59
|
+
new Set(),
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("registration", () => {
|
|
65
|
+
it("does not fetch known CRS", async () => {
|
|
66
|
+
const fetchMock = vi.fn();
|
|
67
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
68
|
+
proj4.defs("EPSG:2154", "+proj=longlat +datum=WGS84 +no_defs");
|
|
69
|
+
await ensureCrsRegistered(
|
|
70
|
+
new Set(["EPSG:3857", "EPSG:4326", "EPSG:2154"]),
|
|
71
|
+
);
|
|
72
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("fetches and registers a missing CRS", async () => {
|
|
76
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
77
|
+
ok: true,
|
|
78
|
+
text: () => Promise.resolve("+proj=longlat +datum=WGS84 +no_defs"),
|
|
79
|
+
});
|
|
80
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
81
|
+
await ensureCrsRegistered(new Set(["EPSG:9999001"]));
|
|
82
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
83
|
+
"https://spatialreference.org/ref/epsg/9999001/ogcwkt/",
|
|
84
|
+
);
|
|
85
|
+
expect(proj4.defs("EPSG:9999001")).toBeTruthy();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("throws if the response is not ok", async () => {
|
|
89
|
+
vi.stubGlobal(
|
|
90
|
+
"fetch",
|
|
91
|
+
vi.fn().mockResolvedValue({ ok: false, status: 404 }),
|
|
92
|
+
);
|
|
93
|
+
await expect(
|
|
94
|
+
ensureCrsRegistered(new Set(["EPSG:9999002"])),
|
|
95
|
+
).rejects.toThrow(/Could not find a definition for CRS "EPSG:9999002"/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("throws if the request fails", async () => {
|
|
99
|
+
vi.stubGlobal("fetch", () => Promise.reject(new Error("offline")));
|
|
100
|
+
await expect(
|
|
101
|
+
ensureCrsRegistered(new Set(["EPSG:9999003"])),
|
|
102
|
+
).rejects.toThrow(/EPSG:9999003.*offline/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("throws on an invalid code", async () => {
|
|
106
|
+
await expect(ensureCrsRegistered(new Set(["foo"]))).rejects.toThrow(
|
|
107
|
+
/Could not find a definition for CRS "foo"/,
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("checks the CRS of a context and of a diff", async () => {
|
|
112
|
+
vi.stubGlobal("fetch", () => Promise.reject(new Error("offline")));
|
|
113
|
+
await expect(
|
|
114
|
+
ensureContextCrsReady({
|
|
115
|
+
layers: [],
|
|
116
|
+
view: { center: [0, 0], zoom: 1, projection: "EPSG:9999004" },
|
|
117
|
+
}),
|
|
118
|
+
).rejects.toThrow(/EPSG:9999004/);
|
|
119
|
+
await expect(
|
|
120
|
+
ensureDiffCrsReady({
|
|
121
|
+
...emptyDiff,
|
|
122
|
+
viewChanges: { center: [0, 0], zoom: 1, projection: "EPSG:9999005" },
|
|
123
|
+
}),
|
|
124
|
+
).rejects.toThrow(/EPSG:9999005/);
|
|
125
|
+
await expect(ensureDiffCrsReady(emptyDiff)).resolves.toBeUndefined();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
});
|
package/lib/utils/crs.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import proj4 from "proj4";
|
|
2
|
+
import { DEFAULT_MAP_VIEW_CRS } from "../constant/projections.js";
|
|
3
|
+
import {
|
|
4
|
+
CrsIdentifier,
|
|
5
|
+
MapContext,
|
|
6
|
+
MapContextView,
|
|
7
|
+
} from "../model/map-context.js";
|
|
8
|
+
import { MapContextDiff } from "../model/map-context-diff.js";
|
|
9
|
+
import { getCrs } from "./coordinates.js";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns the CRS codes referenced by a view (display CRS, center, extent, max extent)
|
|
13
|
+
*/
|
|
14
|
+
export function getViewCrsCodes(view: MapContextView): Set<CrsIdentifier> {
|
|
15
|
+
const codes = new Set<CrsIdentifier>([
|
|
16
|
+
view.projection ?? DEFAULT_MAP_VIEW_CRS,
|
|
17
|
+
]);
|
|
18
|
+
if ("center" in view && view.center) codes.add(getCrs(view.center));
|
|
19
|
+
if ("extent" in view && view.extent) codes.add(getCrs(view.extent));
|
|
20
|
+
if (view.maxExtent) codes.add(getCrs(view.maxExtent));
|
|
21
|
+
return codes;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns all the CRS codes used in a map context
|
|
26
|
+
*/
|
|
27
|
+
export function getContextCrsCodes(context: MapContext): Set<CrsIdentifier> {
|
|
28
|
+
// TODO: check for geometries (e.g. GeoJSON)
|
|
29
|
+
|
|
30
|
+
if (!context.view) return new Set();
|
|
31
|
+
return getViewCrsCodes(context.view);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns all the CRS codes used in a map context diff
|
|
36
|
+
*/
|
|
37
|
+
export function getDiffCrsCodes(diff: MapContextDiff): Set<CrsIdentifier> {
|
|
38
|
+
// TODO: check for geometries (e.g. GeoJSON)
|
|
39
|
+
|
|
40
|
+
if (!diff.viewChanges) return new Set();
|
|
41
|
+
return getViewCrsCodes(diff.viewChanges);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isRegistered(code: CrsIdentifier): boolean {
|
|
45
|
+
// consider Web Mercator and Geographic as known
|
|
46
|
+
if (code === "EPSG:3857" || code === "EPSG:4326") return true;
|
|
47
|
+
return !!proj4.defs(code);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function registerCrs(code: CrsIdentifier): Promise<void> {
|
|
51
|
+
if (typeof code !== "string" || !code.includes(":")) {
|
|
52
|
+
throw new Error(`Invalid code`);
|
|
53
|
+
}
|
|
54
|
+
const [authority, num] = code.toLowerCase().split(":", 2);
|
|
55
|
+
const response = await fetch(
|
|
56
|
+
`https://spatialreference.org/ref/${authority}/${num}/ogcwkt/`,
|
|
57
|
+
);
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Unexpected response from spatialreference.org: ${response.status}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
const def = await response.text();
|
|
64
|
+
proj4.defs(code, def);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Makes sure all given CRS codes are registered, looking up the missing ones.
|
|
69
|
+
* @throws Error if the definition of a CRS cannot be fetched
|
|
70
|
+
*/
|
|
71
|
+
export async function ensureCrsRegistered(
|
|
72
|
+
codes: Set<CrsIdentifier>,
|
|
73
|
+
): Promise<void> {
|
|
74
|
+
const missing = Array.from(codes).filter((code) => !isRegistered(code));
|
|
75
|
+
await Promise.all(
|
|
76
|
+
missing.map(async (code) => {
|
|
77
|
+
try {
|
|
78
|
+
await registerCrs(code);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Could not find a definition for CRS "${code}": ${(e as Error)?.message ?? e}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Makes sure all CRS used in a map context are registered
|
|
90
|
+
* @throws Error if the definition of a CRS cannot be fetched
|
|
91
|
+
*/
|
|
92
|
+
export function ensureContextCrsReady(context: MapContext): Promise<void> {
|
|
93
|
+
return ensureCrsRegistered(getContextCrsCodes(context));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Makes sure all CRS used in a map context diff are registered
|
|
98
|
+
* @throws Error if the definition of a CRS cannot be fetched
|
|
99
|
+
*/
|
|
100
|
+
export function ensureDiffCrsReady(diff: MapContextDiff): Promise<void> {
|
|
101
|
+
return ensureCrsRegistered(getDiffCrsCodes(diff));
|
|
102
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureGeojson,
|
|
3
|
+
getExtentFromGeojson,
|
|
4
|
+
getExtentFromGeometry,
|
|
5
|
+
} from "./geojson.js";
|
|
6
|
+
import type {
|
|
7
|
+
Feature,
|
|
8
|
+
FeatureCollection,
|
|
9
|
+
Geometry,
|
|
10
|
+
GeometryCollection,
|
|
11
|
+
LineString,
|
|
12
|
+
MultiLineString,
|
|
13
|
+
MultiPoint,
|
|
14
|
+
MultiPolygon,
|
|
15
|
+
Point,
|
|
16
|
+
Polygon,
|
|
17
|
+
} from "geojson";
|
|
18
|
+
|
|
19
|
+
describe("geojson utils", () => {
|
|
20
|
+
describe("ensureGeojson", () => {
|
|
21
|
+
it("should return the same object if it is a valid GeoJSON", () => {
|
|
22
|
+
const geojson = {
|
|
23
|
+
type: "FeatureCollection",
|
|
24
|
+
features: [],
|
|
25
|
+
} as FeatureCollection;
|
|
26
|
+
expect(ensureGeojson(geojson)).toBe(geojson);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("should parse a GeoJSON string into an object", () => {
|
|
30
|
+
const geojsonString = JSON.stringify({
|
|
31
|
+
type: "FeatureCollection",
|
|
32
|
+
features: [],
|
|
33
|
+
});
|
|
34
|
+
expect(ensureGeojson(geojsonString)).toEqual({
|
|
35
|
+
type: "FeatureCollection",
|
|
36
|
+
features: [],
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should throw if the geojson string is invalid", () => {
|
|
41
|
+
const invalidGeojson = "hello: world";
|
|
42
|
+
expect(() => ensureGeojson(invalidGeojson)).toThrow(
|
|
43
|
+
"Invalid GeoJSON string: SyntaxError: Unexpected token 'h', \"hello: world\" is not valid JSON",
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("getExtentFromGeometry", () => {
|
|
49
|
+
const polygon: Polygon = {
|
|
50
|
+
type: "Polygon",
|
|
51
|
+
coordinates: [
|
|
52
|
+
[
|
|
53
|
+
[1, 1],
|
|
54
|
+
[1, 4],
|
|
55
|
+
[4, 4],
|
|
56
|
+
[4, 1],
|
|
57
|
+
[1, 1],
|
|
58
|
+
],
|
|
59
|
+
[
|
|
60
|
+
[2, 2],
|
|
61
|
+
[2, 3],
|
|
62
|
+
[130, 3],
|
|
63
|
+
[130, 2],
|
|
64
|
+
[2, 2],
|
|
65
|
+
],
|
|
66
|
+
],
|
|
67
|
+
};
|
|
68
|
+
const lineString: LineString = {
|
|
69
|
+
type: "LineString",
|
|
70
|
+
coordinates: [
|
|
71
|
+
[10, 20],
|
|
72
|
+
[20, 20],
|
|
73
|
+
[50, 10],
|
|
74
|
+
[100, -5],
|
|
75
|
+
[-100, -100],
|
|
76
|
+
],
|
|
77
|
+
};
|
|
78
|
+
const point: Point = {
|
|
79
|
+
type: "Point",
|
|
80
|
+
coordinates: [50, -200],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
it("computes the bounding box correctly for a Polygon", () => {
|
|
84
|
+
const bbox = getExtentFromGeometry(polygon);
|
|
85
|
+
expect(bbox).toEqual([1, 1, 130, 4]);
|
|
86
|
+
});
|
|
87
|
+
it("computes the bounding box correctly for a LineString", () => {
|
|
88
|
+
const bbox = getExtentFromGeometry(lineString);
|
|
89
|
+
expect(bbox).toEqual([-100, -100, 100, 20]);
|
|
90
|
+
});
|
|
91
|
+
it("computes the bounding box correctly for a Point", () => {
|
|
92
|
+
const bbox = getExtentFromGeometry(point);
|
|
93
|
+
expect(bbox).toEqual([50, -200, 50, -200]);
|
|
94
|
+
});
|
|
95
|
+
it("computes the bounding box correctly for a MultiPolygon", () => {
|
|
96
|
+
const multiPolygon: MultiPolygon = {
|
|
97
|
+
type: "MultiPolygon",
|
|
98
|
+
coordinates: [
|
|
99
|
+
polygon.coordinates,
|
|
100
|
+
[
|
|
101
|
+
[
|
|
102
|
+
[-20, -20],
|
|
103
|
+
[-20, 30],
|
|
104
|
+
[20, 30],
|
|
105
|
+
[20, -20],
|
|
106
|
+
[-20, -20],
|
|
107
|
+
],
|
|
108
|
+
],
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
const bbox = getExtentFromGeometry(multiPolygon);
|
|
112
|
+
expect(bbox).toEqual([-20, -20, 130, 30]);
|
|
113
|
+
});
|
|
114
|
+
it("computes the bounding box correctly for a MultiLineString", () => {
|
|
115
|
+
const multiLineString: MultiLineString = {
|
|
116
|
+
type: "MultiLineString",
|
|
117
|
+
coordinates: [
|
|
118
|
+
lineString.coordinates,
|
|
119
|
+
[
|
|
120
|
+
[-2, -2],
|
|
121
|
+
[2, 3],
|
|
122
|
+
[150, 200],
|
|
123
|
+
[100, 10],
|
|
124
|
+
],
|
|
125
|
+
],
|
|
126
|
+
};
|
|
127
|
+
const bbox = getExtentFromGeometry(multiLineString);
|
|
128
|
+
expect(bbox).toEqual([-100, -100, 150, 200]);
|
|
129
|
+
});
|
|
130
|
+
it("computes the bounding box correctly for a MultiPoint", () => {
|
|
131
|
+
const multiPoint: MultiPoint = {
|
|
132
|
+
type: "MultiPoint",
|
|
133
|
+
coordinates: [point.coordinates, [200, 300]],
|
|
134
|
+
};
|
|
135
|
+
const bbox = getExtentFromGeometry(multiPoint);
|
|
136
|
+
expect(bbox).toEqual([50, -200, 200, 300]);
|
|
137
|
+
});
|
|
138
|
+
it("computes the bounding box correctly for a GeometryCollection", () => {
|
|
139
|
+
const geom: GeometryCollection = {
|
|
140
|
+
type: "GeometryCollection",
|
|
141
|
+
geometries: [lineString, polygon, point],
|
|
142
|
+
};
|
|
143
|
+
const bbox = getExtentFromGeometry(geom);
|
|
144
|
+
expect(bbox).toEqual([-100, -200, 130, 20]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("getExtentFromGeoJSON", () => {
|
|
149
|
+
it("computes the combined extent of a FeatureCollection", () => {
|
|
150
|
+
const geojson: FeatureCollection<Geometry | null> = {
|
|
151
|
+
type: "FeatureCollection",
|
|
152
|
+
features: [
|
|
153
|
+
{
|
|
154
|
+
type: "Feature",
|
|
155
|
+
properties: {},
|
|
156
|
+
geometry: { type: "Point", coordinates: [5, 10] },
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
type: "Feature",
|
|
160
|
+
properties: {},
|
|
161
|
+
geometry: { type: "Point", coordinates: [-2, 20] },
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: "Feature",
|
|
165
|
+
properties: {},
|
|
166
|
+
geometry: null,
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(getExtentFromGeojson(geojson)).toEqual([-2, 10, 5, 20]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("computes the extent of a Feature", () => {
|
|
175
|
+
const feature: Feature<Point> = {
|
|
176
|
+
type: "Feature",
|
|
177
|
+
properties: {},
|
|
178
|
+
geometry: { type: "Point", coordinates: [5, 10] },
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
expect(getExtentFromGeojson(feature)).toEqual([5, 10, 5, 10]);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("computes the extent of a Geometry", () => {
|
|
185
|
+
const geometry: Point = {
|
|
186
|
+
type: "Point",
|
|
187
|
+
coordinates: [-2, 20],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
expect(getExtentFromGeojson(geometry)).toEqual([-2, 20, -2, 20]);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("returns an empty extent for a Feature with no geometry", () => {
|
|
194
|
+
const feature: Feature<null> = {
|
|
195
|
+
type: "Feature",
|
|
196
|
+
properties: {},
|
|
197
|
+
geometry: null,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
expect(getExtentFromGeojson(feature)).toEqual([
|
|
201
|
+
Infinity,
|
|
202
|
+
Infinity,
|
|
203
|
+
-Infinity,
|
|
204
|
+
-Infinity,
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { Feature, FeatureCollection, Geometry, Position } from "geojson";
|
|
2
|
+
import { Extent } from "../model/index.js";
|
|
3
|
+
import { extend } from "./coordinates.js";
|
|
4
|
+
|
|
5
|
+
export function ensureGeojson(
|
|
6
|
+
value: FeatureCollection<Geometry | null> | string,
|
|
7
|
+
): FeatureCollection<Geometry | null> {
|
|
8
|
+
if (typeof value === "string") {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(value) as FeatureCollection;
|
|
11
|
+
} catch (e) {
|
|
12
|
+
throw new Error(`Invalid GeoJSON string: ${e}`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* This assumes the geometry is in EPSG:4326
|
|
20
|
+
* TODO: support other CRSs
|
|
21
|
+
* @param geometry
|
|
22
|
+
*/
|
|
23
|
+
export function getExtentFromGeometry(geometry: Geometry): Extent {
|
|
24
|
+
// use the bounding box if specified in the GeoJSON object
|
|
25
|
+
if (geometry.bbox) {
|
|
26
|
+
return geometry.bbox.length > 5
|
|
27
|
+
? [
|
|
28
|
+
geometry.bbox[0],
|
|
29
|
+
geometry.bbox[1],
|
|
30
|
+
geometry.bbox[3],
|
|
31
|
+
geometry.bbox[4] as number,
|
|
32
|
+
]
|
|
33
|
+
: (geometry.bbox as Extent);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const coordinatesReducer = (prev: Extent, coords: Position) => {
|
|
37
|
+
if (coords[0] < prev[0]) {
|
|
38
|
+
prev[0] = coords[0];
|
|
39
|
+
}
|
|
40
|
+
if (coords[1] < prev[1]) {
|
|
41
|
+
prev[1] = coords[1];
|
|
42
|
+
}
|
|
43
|
+
if (coords[0] > prev[2]) {
|
|
44
|
+
prev[2] = coords[0];
|
|
45
|
+
}
|
|
46
|
+
if (coords[1] > prev[3]) {
|
|
47
|
+
prev[3] = coords[1];
|
|
48
|
+
}
|
|
49
|
+
return prev;
|
|
50
|
+
};
|
|
51
|
+
const coordinatesArrayReducer = (prev: Extent, coordsArray: Position[]) => {
|
|
52
|
+
const bbox = coordsArray.reduce(coordinatesReducer, [
|
|
53
|
+
Infinity,
|
|
54
|
+
Infinity,
|
|
55
|
+
-Infinity,
|
|
56
|
+
-Infinity,
|
|
57
|
+
]);
|
|
58
|
+
return extend(prev, bbox);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const initialExtent = [Infinity, Infinity, -Infinity, -Infinity] as Extent;
|
|
62
|
+
|
|
63
|
+
switch (geometry.type) {
|
|
64
|
+
case "MultiPolygon":
|
|
65
|
+
return geometry.coordinates.reduce((prev, polygonCoords) => {
|
|
66
|
+
const bbox = polygonCoords.reduce(coordinatesArrayReducer, [
|
|
67
|
+
Infinity,
|
|
68
|
+
Infinity,
|
|
69
|
+
-Infinity,
|
|
70
|
+
-Infinity,
|
|
71
|
+
]);
|
|
72
|
+
return extend(prev, bbox);
|
|
73
|
+
}, initialExtent);
|
|
74
|
+
case "GeometryCollection":
|
|
75
|
+
return geometry.geometries.reduce((prev, geom) => {
|
|
76
|
+
const bbox = getExtentFromGeometry(geom);
|
|
77
|
+
return extend(prev, bbox);
|
|
78
|
+
}, initialExtent);
|
|
79
|
+
case "Polygon":
|
|
80
|
+
case "MultiLineString":
|
|
81
|
+
return geometry.coordinates.reduce(coordinatesArrayReducer, [
|
|
82
|
+
Infinity,
|
|
83
|
+
Infinity,
|
|
84
|
+
-Infinity,
|
|
85
|
+
-Infinity,
|
|
86
|
+
]);
|
|
87
|
+
case "LineString":
|
|
88
|
+
case "MultiPoint":
|
|
89
|
+
return geometry.coordinates.reduce<Extent>(coordinatesReducer, [
|
|
90
|
+
Infinity,
|
|
91
|
+
Infinity,
|
|
92
|
+
-Infinity,
|
|
93
|
+
-Infinity,
|
|
94
|
+
]);
|
|
95
|
+
case "Point":
|
|
96
|
+
return coordinatesReducer(
|
|
97
|
+
[Infinity, Infinity, -Infinity, -Infinity],
|
|
98
|
+
geometry.coordinates,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* This assumes the geometry is in EPSG:4326
|
|
105
|
+
* TODO: support other CRSs
|
|
106
|
+
* @param geojson
|
|
107
|
+
*/
|
|
108
|
+
export function getExtentFromGeojson(
|
|
109
|
+
geojson:
|
|
110
|
+
FeatureCollection<Geometry | null> | Feature<Geometry | null> | Geometry,
|
|
111
|
+
): Extent {
|
|
112
|
+
const empty = [Infinity, Infinity, -Infinity, -Infinity] as Extent;
|
|
113
|
+
if ("type" in geojson) {
|
|
114
|
+
switch (geojson.type) {
|
|
115
|
+
case "FeatureCollection":
|
|
116
|
+
return geojson.features.reduce((prev, feature) => {
|
|
117
|
+
if (feature.geometry) {
|
|
118
|
+
const bbox = getExtentFromGeometry(feature.geometry);
|
|
119
|
+
return extend(prev, bbox);
|
|
120
|
+
}
|
|
121
|
+
return prev;
|
|
122
|
+
}, empty);
|
|
123
|
+
case "Feature":
|
|
124
|
+
if (geojson.geometry) {
|
|
125
|
+
return getExtentFromGeometry(geojson.geometry);
|
|
126
|
+
}
|
|
127
|
+
return empty;
|
|
128
|
+
default:
|
|
129
|
+
return getExtentFromGeometry(geojson);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return empty;
|
|
133
|
+
}
|
package/lib/utils/index.ts
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
export * from "./url.js";
|
|
7
7
|
export * from "./freeze.js";
|
|
8
8
|
export * from "./hash.js";
|
|
9
|
+
export {
|
|
10
|
+
getCrs,
|
|
11
|
+
setCrs,
|
|
12
|
+
transform,
|
|
13
|
+
toGeographic,
|
|
14
|
+
equals,
|
|
15
|
+
extend,
|
|
16
|
+
} from "./coordinates.js";
|
|
9
17
|
export { computeMapContextDiff } from "./map-context-diff.js";
|
|
10
18
|
export {
|
|
11
19
|
getLayerPosition,
|
|
@@ -18,3 +26,9 @@ export {
|
|
|
18
26
|
export { updateLayer } from "./map-context-layer.js";
|
|
19
27
|
export { createViewFromLayer } from "./view.js";
|
|
20
28
|
export * from "./style.js";
|
|
29
|
+
export {
|
|
30
|
+
ensureGeojson,
|
|
31
|
+
getExtentFromGeojson,
|
|
32
|
+
getExtentFromGeometry,
|
|
33
|
+
} from "./geojson.js";
|
|
34
|
+
export { ensureContextCrsReady, ensureDiffCrsReady } from "./crs.js";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
SAMPLE_LAYER6,
|
|
11
11
|
SAMPLE_LAYER7,
|
|
12
12
|
} from "../../fixtures/map-context.fixtures.js";
|
|
13
|
-
import {
|
|
13
|
+
import { DEFAULT_MAP_VIEW_CRS } from "../constant/projections.js";
|
|
14
14
|
|
|
15
15
|
describe("Context diff utils", () => {
|
|
16
16
|
describe("computeMapContextDiff", () => {
|
|
@@ -330,6 +330,73 @@ describe("Context diff utils", () => {
|
|
|
330
330
|
});
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
|
+
|
|
334
|
+
describe("changing projection from default to custom", () => {
|
|
335
|
+
beforeEach(() => {
|
|
336
|
+
contextOld = {
|
|
337
|
+
...SAMPLE_CONTEXT,
|
|
338
|
+
view: {
|
|
339
|
+
center: [5, 10],
|
|
340
|
+
zoom: 3,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
contextNew = {
|
|
344
|
+
...SAMPLE_CONTEXT,
|
|
345
|
+
view: {
|
|
346
|
+
center: [5, 10],
|
|
347
|
+
zoom: 3,
|
|
348
|
+
projection: "EPSG:1234",
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
});
|
|
352
|
+
it("output the correct diff", () => {
|
|
353
|
+
diff = computeMapContextDiff(contextNew, contextOld);
|
|
354
|
+
expect(diff).toEqual({
|
|
355
|
+
layersAdded: [],
|
|
356
|
+
layersChanged: [],
|
|
357
|
+
layersRemoved: [],
|
|
358
|
+
layersReordered: [],
|
|
359
|
+
viewChanges: {
|
|
360
|
+
center: [5, 10],
|
|
361
|
+
zoom: 3,
|
|
362
|
+
projection: "EPSG:1234",
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
describe("changing projection from custom to default", () => {
|
|
368
|
+
beforeEach(() => {
|
|
369
|
+
contextOld = {
|
|
370
|
+
...SAMPLE_CONTEXT,
|
|
371
|
+
view: {
|
|
372
|
+
center: [5, 10],
|
|
373
|
+
zoom: 3,
|
|
374
|
+
projection: "EPSG:1234",
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
contextNew = {
|
|
378
|
+
...SAMPLE_CONTEXT,
|
|
379
|
+
view: {
|
|
380
|
+
center: [5, 10],
|
|
381
|
+
zoom: 3,
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
});
|
|
385
|
+
it("output the correct diff", () => {
|
|
386
|
+
diff = computeMapContextDiff(contextNew, contextOld);
|
|
387
|
+
expect(diff).toEqual({
|
|
388
|
+
layersAdded: [],
|
|
389
|
+
layersChanged: [],
|
|
390
|
+
layersRemoved: [],
|
|
391
|
+
layersReordered: [],
|
|
392
|
+
viewChanges: {
|
|
393
|
+
center: [5, 10],
|
|
394
|
+
zoom: 3,
|
|
395
|
+
projection: DEFAULT_MAP_VIEW_CRS,
|
|
396
|
+
},
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
333
400
|
});
|
|
334
401
|
|
|
335
402
|
describe("combined changes", () => {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
MapContextLayerReordered,
|
|
8
8
|
} from "../model/index.js";
|
|
9
9
|
import { isLayerSame, isLayerSameAndUnchanged } from "./map-context-layer.js";
|
|
10
|
+
import { DEFAULT_MAP_VIEW_CRS } from "../constant/projections.js";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* The following logic is produced by identifying layers in both context
|
|
@@ -99,6 +100,16 @@ export function computeMapContextDiff(
|
|
|
99
100
|
nextContext.view !== previousContext.view ? nextContext.view : undefined;
|
|
100
101
|
if (viewChanges !== null && viewChanges !== undefined) {
|
|
101
102
|
viewChanges = { ...viewChanges }; // copy the view to avoid unexpected mutations
|
|
103
|
+
|
|
104
|
+
// only keep the projection if it changed between the two contexts
|
|
105
|
+
const prevProjection =
|
|
106
|
+
previousContext.view?.projection ?? DEFAULT_MAP_VIEW_CRS;
|
|
107
|
+
const nextProjection = nextContext.view?.projection ?? DEFAULT_MAP_VIEW_CRS;
|
|
108
|
+
if (nextProjection === prevProjection) {
|
|
109
|
+
delete viewChanges.projection;
|
|
110
|
+
} else {
|
|
111
|
+
viewChanges.projection = nextProjection;
|
|
112
|
+
}
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
return {
|