Skip to content

Commit 07a83f1

Browse files
authored
Merge pull request #34 from proj4js/wkt2-projjson
Add support for WKT2 and PROJJSON
2 parents 08d39de + 4519c11 commit 07a83f1

15 files changed

+2199
-36
lines changed

PROJJSONBuilder2015.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import PROJJSONBuilderBase from './PROJJSONBuilderBase.js';
2+
3+
class PROJJSONBuilder2015 extends PROJJSONBuilderBase {
4+
static convert(node, result = {}) {
5+
super.convert(node, result);
6+
7+
// Skip `CS` and `USAGE` nodes for WKT2-2015
8+
if (result.coordinate_system?.subtype === "Cartesian") {
9+
delete result.coordinate_system;
10+
}
11+
if (result.usage) {
12+
delete result.usage;
13+
}
14+
15+
return result;
16+
}
17+
}
18+
19+
export default PROJJSONBuilder2015;

PROJJSONBuilder2019.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import PROJJSONBuilderBase from './PROJJSONBuilderBase.js';
2+
3+
class PROJJSONBuilder2019 extends PROJJSONBuilderBase {
4+
static convert(node, result = {}) {
5+
super.convert(node, result);
6+
7+
// Handle `CS` node for WKT2-2019
8+
const csNode = node.find((child) => Array.isArray(child) && child[0] === "CS");
9+
if (csNode) {
10+
result.coordinate_system = {
11+
subtype: csNode[1],
12+
axis: this.extractAxes(node),
13+
};
14+
}
15+
16+
// Handle `USAGE` node for WKT2-2019
17+
const usageNode = node.find((child) => Array.isArray(child) && child[0] === "USAGE");
18+
if (usageNode) {
19+
result.usage = {
20+
scope: usageNode.find((child) => Array.isArray(child) && child[0] === "SCOPE")?.[1],
21+
area: usageNode.find((child) => Array.isArray(child) && child[0] === "AREA")?.[1],
22+
bbox: usageNode.find((child) => Array.isArray(child) && child[0] === "BBOX")?.slice(1),
23+
};
24+
}
25+
26+
return result;
27+
}
28+
}
29+
30+
export default PROJJSONBuilder2019;

PROJJSONBuilderBase.js

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
class PROJJSONBuilderBase {
2+
static getId(node) {
3+
const idNode = node.find((child) => Array.isArray(child) && child[0] === "ID");
4+
if (idNode && idNode.length >= 3) {
5+
return {
6+
authority: idNode[1],
7+
code: parseInt(idNode[2], 10),
8+
};
9+
}
10+
return null;
11+
}
12+
13+
static convertUnit(node, type = "unit") {
14+
if (!node || node.length < 3) {
15+
return { type, name: "unknown", conversion_factor: null };
16+
}
17+
18+
const name = node[1];
19+
const conversionFactor = parseFloat(node[2]) || null;
20+
21+
const idNode = node.find((child) => Array.isArray(child) && child[0] === "ID");
22+
const id = idNode
23+
? {
24+
authority: idNode[1],
25+
code: parseInt(idNode[2], 10),
26+
}
27+
: null;
28+
29+
return {
30+
type,
31+
name,
32+
conversion_factor: conversionFactor,
33+
id,
34+
};
35+
}
36+
37+
static convertAxis(node) {
38+
const name = node[1] || "Unknown";
39+
40+
// Determine the direction
41+
let direction;
42+
const abbreviationMatch = name.match(/^\((.)\)$/); // Match abbreviations like "(E)" or "(N)"
43+
if (abbreviationMatch) {
44+
// Use the abbreviation to determine the direction
45+
const abbreviation = abbreviationMatch[1].toUpperCase();
46+
if (abbreviation === 'E') direction = 'east';
47+
else if (abbreviation === 'N') direction = 'north';
48+
else if (abbreviation === 'U') direction = 'up';
49+
else throw new Error(`Unknown axis abbreviation: ${abbreviation}`);
50+
} else {
51+
// Use the explicit direction provided in the AXIS node
52+
direction = node[2]?.toLowerCase() || "unknown";
53+
}
54+
55+
const orderNode = node.find((child) => Array.isArray(child) && child[0] === "ORDER");
56+
const order = orderNode ? parseInt(orderNode[1], 10) : null;
57+
58+
const unitNode = node.find(
59+
(child) =>
60+
Array.isArray(child) &&
61+
(child[0] === "LENGTHUNIT" || child[0] === "ANGLEUNIT" || child[0] === "SCALEUNIT")
62+
);
63+
const unit = this.convertUnit(unitNode);
64+
65+
return {
66+
name,
67+
direction, // Use the valid PROJJSON direction value
68+
unit,
69+
order,
70+
};
71+
}
72+
73+
static extractAxes(node) {
74+
return node
75+
.filter((child) => Array.isArray(child) && child[0] === "AXIS")
76+
.map((axis) => this.convertAxis(axis))
77+
.sort((a, b) => (a.order || 0) - (b.order || 0)); // Sort by the "order" property
78+
}
79+
80+
static convert(node, result = {}) {
81+
82+
switch (node[0]) {
83+
case "PROJCRS":
84+
result.type = "ProjectedCRS";
85+
result.name = node[1];
86+
result.base_crs = node.find((child) => Array.isArray(child) && child[0] === "BASEGEOGCRS")
87+
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "BASEGEOGCRS"))
88+
: null;
89+
result.conversion = node.find((child) => Array.isArray(child) && child[0] === "CONVERSION")
90+
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "CONVERSION"))
91+
: null;
92+
93+
const csNode = node.find((child) => Array.isArray(child) && child[0] === "CS");
94+
if (csNode) {
95+
result.coordinate_system = {
96+
type: csNode[1],
97+
axis: this.extractAxes(node),
98+
};
99+
}
100+
101+
const lengthUnitNode = node.find((child) => Array.isArray(child) && child[0] === "LENGTHUNIT");
102+
if (lengthUnitNode) {
103+
const unit = this.convertUnit(lengthUnitNode);
104+
result.coordinate_system.unit = unit; // Add unit to coordinate_system
105+
}
106+
107+
result.id = this.getId(node);
108+
break;
109+
110+
case "BASEGEOGCRS":
111+
case "GEOGCRS":
112+
result.type = "GeographicCRS";
113+
result.name = node[1];
114+
115+
// Handle DATUM or ENSEMBLE
116+
const datumOrEnsembleNode = node.find(
117+
(child) => Array.isArray(child) && (child[0] === "DATUM" || child[0] === "ENSEMBLE")
118+
);
119+
if (datumOrEnsembleNode) {
120+
const datumOrEnsemble = this.convert(datumOrEnsembleNode);
121+
if (datumOrEnsembleNode[0] === "ENSEMBLE") {
122+
result.datum_ensemble = datumOrEnsemble;
123+
} else {
124+
result.datum = datumOrEnsemble;
125+
}
126+
const primem = node.find((child) => Array.isArray(child) && child[0] === "PRIMEM");
127+
if (primem && primem[1] !== 'Greenwich') {
128+
datumOrEnsemble.prime_meridian = {
129+
name: primem[1],
130+
longitude: parseFloat(primem[2]),
131+
}
132+
}
133+
}
134+
135+
result.coordinate_system = {
136+
type: "ellipsoidal",
137+
axis: this.extractAxes(node),
138+
};
139+
140+
result.id = this.getId(node);
141+
break;
142+
143+
case "DATUM":
144+
result.type = "GeodeticReferenceFrame";
145+
result.name = node[1];
146+
result.ellipsoid = node.find((child) => Array.isArray(child) && child[0] === "ELLIPSOID")
147+
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "ELLIPSOID"))
148+
: null;
149+
break;
150+
151+
case "ENSEMBLE":
152+
result.type = "DatumEnsemble";
153+
result.name = node[1];
154+
155+
// Extract ensemble members
156+
result.members = node
157+
.filter((child) => Array.isArray(child) && child[0] === "MEMBER")
158+
.map((member) => ({
159+
type: "DatumEnsembleMember",
160+
name: member[1],
161+
id: this.getId(member), // Extract ID as { authority, code }
162+
}));
163+
164+
// Extract accuracy
165+
const accuracyNode = node.find((child) => Array.isArray(child) && child[0] === "ENSEMBLEACCURACY");
166+
if (accuracyNode) {
167+
result.accuracy = parseFloat(accuracyNode[1]);
168+
}
169+
170+
// Extract ellipsoid
171+
const ellipsoidNode = node.find((child) => Array.isArray(child) && child[0] === "ELLIPSOID");
172+
if (ellipsoidNode) {
173+
result.ellipsoid = this.convert(ellipsoidNode); // Convert the ellipsoid node
174+
}
175+
176+
// Extract identifier for the ensemble
177+
result.id = this.getId(node);
178+
break;
179+
180+
case "ELLIPSOID":
181+
result.type = "Ellipsoid";
182+
result.name = node[1];
183+
result.semi_major_axis = parseFloat(node[2]);
184+
result.inverse_flattening = parseFloat(node[3]);
185+
const units = node.find((child) => Array.isArray(child) && child[0] === "LENGTHUNIT")
186+
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "LENGTHUNIT"), result)
187+
: null;
188+
break;
189+
190+
case "CONVERSION":
191+
result.type = "Conversion";
192+
result.name = node[1];
193+
result.method = node.find((child) => Array.isArray(child) && child[0] === "METHOD")
194+
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "METHOD"))
195+
: null;
196+
result.parameters = node
197+
.filter((child) => Array.isArray(child) && child[0] === "PARAMETER")
198+
.map((param) => this.convert(param));
199+
break;
200+
201+
case "METHOD":
202+
result.type = "Method";
203+
result.name = node[1];
204+
result.id = this.getId(node);
205+
break;
206+
207+
case "PARAMETER":
208+
result.type = "Parameter";
209+
result.name = node[1];
210+
result.value = parseFloat(node[2]);
211+
result.unit = this.convertUnit(
212+
node.find(
213+
(child) =>
214+
Array.isArray(child) &&
215+
(child[0] === "LENGTHUNIT" || child[0] === "ANGLEUNIT" || child[0] === "SCALEUNIT")
216+
)
217+
);
218+
result.id = this.getId(node);
219+
break;
220+
221+
case "BOUNDCRS":
222+
result.type = "BoundCRS";
223+
224+
// Process SOURCECRS
225+
const sourceCrsNode = node.find((child) => Array.isArray(child) && child[0] === "SOURCECRS");
226+
if (sourceCrsNode) {
227+
const sourceCrsContent = sourceCrsNode.find((child) => Array.isArray(child));
228+
result.source_crs = sourceCrsContent ? this.convert(sourceCrsContent) : null;
229+
}
230+
231+
// Process TARGETCRS
232+
const targetCrsNode = node.find((child) => Array.isArray(child) && child[0] === "TARGETCRS");
233+
if (targetCrsNode) {
234+
const targetCrsContent = targetCrsNode.find((child) => Array.isArray(child));
235+
result.target_crs = targetCrsContent ? this.convert(targetCrsContent) : null;
236+
}
237+
238+
// Process ABRIDGEDTRANSFORMATION
239+
const transformationNode = node.find((child) => Array.isArray(child) && child[0] === "ABRIDGEDTRANSFORMATION");
240+
if (transformationNode) {
241+
result.transformation = this.convert(transformationNode);
242+
} else {
243+
result.transformation = null;
244+
}
245+
break;
246+
247+
case "ABRIDGEDTRANSFORMATION":
248+
result.type = "Transformation";
249+
result.name = node[1];
250+
result.method = node.find((child) => Array.isArray(child) && child[0] === "METHOD")
251+
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "METHOD"))
252+
: null;
253+
254+
result.parameters = node
255+
.filter((child) => Array.isArray(child) && (child[0] === "PARAMETER" || child[0] === "PARAMETERFILE"))
256+
.map((param) => {
257+
if (param[0] === "PARAMETER") {
258+
return this.convert(param);
259+
} else if (param[0] === "PARAMETERFILE") {
260+
return {
261+
name: param[1],
262+
value: param[2],
263+
id: {
264+
"authority": "EPSG",
265+
"code": 8656
266+
}
267+
};
268+
}
269+
});
270+
271+
// Adjust the Scale difference parameter if present
272+
if (result.parameters.length === 7) {
273+
const scaleDifference = result.parameters[6];
274+
if (scaleDifference.name === "Scale difference") {
275+
scaleDifference.value = Math.round((scaleDifference.value - 1) * 1e12) / 1e6;
276+
}
277+
}
278+
279+
result.id = this.getId(node);
280+
break;
281+
282+
case "AXIS":
283+
if (!result.coordinate_system) {
284+
result.coordinate_system = { type: "unspecified", axis: [] };
285+
}
286+
result.coordinate_system.axis.push(this.convertAxis(node));
287+
break;
288+
289+
case "LENGTHUNIT":
290+
const unit = this.convertUnit(node, 'LinearUnit');
291+
if (result.coordinate_system && result.coordinate_system.axis) {
292+
result.coordinate_system.axis.forEach((axis) => {
293+
if (!axis.unit) {
294+
axis.unit = unit;
295+
}
296+
});
297+
}
298+
if (unit.conversion_factor && unit.conversion_factor !== 1) {
299+
if (result.semi_major_axis) {
300+
result.semi_major_axis = {
301+
value: result.semi_major_axis,
302+
unit,
303+
}
304+
}
305+
}
306+
break;
307+
308+
default:
309+
result.keyword = node[0];
310+
break;
311+
}
312+
313+
return result;
314+
}
315+
}
316+
317+
export default PROJJSONBuilderBase;

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
# wkt-parser
22

3-
The wkt parser pulled out of proj4 so it can be hacked on.
3+
The wkt parser pulled out of proj4 so it can be hacked on. Supports WKT1, WKT2 and PROJJSON.
44

5-
The parser currently only supports wkt strings of [version 1](https://docs.ogc.org/is/18-010r7/18-010r7.html#196) (earlier than 2015).
6-
7-
It does not support geocentric currently (`GEOCCS`).
5+
It does not support geocentric currently (`GEOCCS`/`). `COMPOUNDCS` is only supported for WKT1.

0 commit comments

Comments
 (0)