Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions PROJJSONBuilder2015.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import PROJJSONBuilderBase from './PROJJSONBuilderBase.js';

class PROJJSONBuilder2015 extends PROJJSONBuilderBase {
static convert(node, result = {}) {
super.convert(node, result);

// Skip `CS` and `USAGE` nodes for WKT2-2015
if (result.coordinate_system?.subtype === "Cartesian") {
delete result.coordinate_system;
}
if (result.usage) {
delete result.usage;
}

return result;
}
}

export default PROJJSONBuilder2015;
30 changes: 30 additions & 0 deletions PROJJSONBuilder2019.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import PROJJSONBuilderBase from './PROJJSONBuilderBase.js';

class PROJJSONBuilder2019 extends PROJJSONBuilderBase {
static convert(node, result = {}) {
super.convert(node, result);

// Handle `CS` node for WKT2-2019
const csNode = node.find((child) => Array.isArray(child) && child[0] === "CS");
if (csNode) {
result.coordinate_system = {
subtype: csNode[1],
axis: this.extractAxes(node),
};
}

// Handle `USAGE` node for WKT2-2019
const usageNode = node.find((child) => Array.isArray(child) && child[0] === "USAGE");
if (usageNode) {
result.usage = {
scope: usageNode.find((child) => Array.isArray(child) && child[0] === "SCOPE")?.[1],
area: usageNode.find((child) => Array.isArray(child) && child[0] === "AREA")?.[1],
bbox: usageNode.find((child) => Array.isArray(child) && child[0] === "BBOX")?.slice(1),
};
}

return result;
}
}

export default PROJJSONBuilder2019;
317 changes: 317 additions & 0 deletions PROJJSONBuilderBase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,317 @@
class PROJJSONBuilderBase {
static getId(node) {
const idNode = node.find((child) => Array.isArray(child) && child[0] === "ID");
if (idNode && idNode.length >= 3) {
return {
authority: idNode[1],
code: parseInt(idNode[2], 10),
};
}
return null;
}

static convertUnit(node, type = "unit") {
if (!node || node.length < 3) {
return { type, name: "unknown", conversion_factor: null };
}

const name = node[1];
const conversionFactor = parseFloat(node[2]) || null;

const idNode = node.find((child) => Array.isArray(child) && child[0] === "ID");
const id = idNode
? {
authority: idNode[1],
code: parseInt(idNode[2], 10),
}
: null;

return {
type,
name,
conversion_factor: conversionFactor,
id,
};
}

static convertAxis(node) {
const name = node[1] || "Unknown";

// Determine the direction
let direction;
const abbreviationMatch = name.match(/^\((.)\)$/); // Match abbreviations like "(E)" or "(N)"
if (abbreviationMatch) {
// Use the abbreviation to determine the direction
const abbreviation = abbreviationMatch[1].toUpperCase();
if (abbreviation === 'E') direction = 'east';
else if (abbreviation === 'N') direction = 'north';
else if (abbreviation === 'U') direction = 'up';
else throw new Error(`Unknown axis abbreviation: ${abbreviation}`);
} else {
// Use the explicit direction provided in the AXIS node
direction = node[2]?.toLowerCase() || "unknown";
}

const orderNode = node.find((child) => Array.isArray(child) && child[0] === "ORDER");
const order = orderNode ? parseInt(orderNode[1], 10) : null;

const unitNode = node.find(
(child) =>
Array.isArray(child) &&
(child[0] === "LENGTHUNIT" || child[0] === "ANGLEUNIT" || child[0] === "SCALEUNIT")
);
const unit = this.convertUnit(unitNode);

return {
name,
direction, // Use the valid PROJJSON direction value
unit,
order,
};
}

static extractAxes(node) {
return node
.filter((child) => Array.isArray(child) && child[0] === "AXIS")
.map((axis) => this.convertAxis(axis))
.sort((a, b) => (a.order || 0) - (b.order || 0)); // Sort by the "order" property
}

static convert(node, result = {}) {

switch (node[0]) {
case "PROJCRS":
result.type = "ProjectedCRS";
result.name = node[1];
result.base_crs = node.find((child) => Array.isArray(child) && child[0] === "BASEGEOGCRS")
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "BASEGEOGCRS"))
: null;
result.conversion = node.find((child) => Array.isArray(child) && child[0] === "CONVERSION")
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "CONVERSION"))
: null;

const csNode = node.find((child) => Array.isArray(child) && child[0] === "CS");
if (csNode) {
result.coordinate_system = {
type: csNode[1],
axis: this.extractAxes(node),
};
}

const lengthUnitNode = node.find((child) => Array.isArray(child) && child[0] === "LENGTHUNIT");
if (lengthUnitNode) {
const unit = this.convertUnit(lengthUnitNode);
result.coordinate_system.unit = unit; // Add unit to coordinate_system
}

result.id = this.getId(node);
break;

case "BASEGEOGCRS":
case "GEOGCRS":
result.type = "GeographicCRS";
result.name = node[1];

// Handle DATUM or ENSEMBLE
const datumOrEnsembleNode = node.find(
(child) => Array.isArray(child) && (child[0] === "DATUM" || child[0] === "ENSEMBLE")
);
if (datumOrEnsembleNode) {
const datumOrEnsemble = this.convert(datumOrEnsembleNode);
if (datumOrEnsembleNode[0] === "ENSEMBLE") {
result.datum_ensemble = datumOrEnsemble;
} else {
result.datum = datumOrEnsemble;
}
const primem = node.find((child) => Array.isArray(child) && child[0] === "PRIMEM");
if (primem && primem[1] !== 'Greenwich') {
datumOrEnsemble.prime_meridian = {
name: primem[1],
longitude: parseFloat(primem[2]),
}
}
}

result.coordinate_system = {
type: "ellipsoidal",
axis: this.extractAxes(node),
};

result.id = this.getId(node);
break;

case "DATUM":
result.type = "GeodeticReferenceFrame";
result.name = node[1];
result.ellipsoid = node.find((child) => Array.isArray(child) && child[0] === "ELLIPSOID")
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "ELLIPSOID"))
: null;
break;

case "ENSEMBLE":
result.type = "DatumEnsemble";
result.name = node[1];

// Extract ensemble members
result.members = node
.filter((child) => Array.isArray(child) && child[0] === "MEMBER")
.map((member) => ({
type: "DatumEnsembleMember",
name: member[1],
id: this.getId(member), // Extract ID as { authority, code }
}));

// Extract accuracy
const accuracyNode = node.find((child) => Array.isArray(child) && child[0] === "ENSEMBLEACCURACY");
if (accuracyNode) {
result.accuracy = parseFloat(accuracyNode[1]);
}

// Extract ellipsoid
const ellipsoidNode = node.find((child) => Array.isArray(child) && child[0] === "ELLIPSOID");
if (ellipsoidNode) {
result.ellipsoid = this.convert(ellipsoidNode); // Convert the ellipsoid node
}

// Extract identifier for the ensemble
result.id = this.getId(node);
break;

case "ELLIPSOID":
result.type = "Ellipsoid";
result.name = node[1];
result.semi_major_axis = parseFloat(node[2]);
result.inverse_flattening = parseFloat(node[3]);
const units = node.find((child) => Array.isArray(child) && child[0] === "LENGTHUNIT")
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "LENGTHUNIT"), result)
: null;
break;

case "CONVERSION":
result.type = "Conversion";
result.name = node[1];
result.method = node.find((child) => Array.isArray(child) && child[0] === "METHOD")
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "METHOD"))
: null;
result.parameters = node
.filter((child) => Array.isArray(child) && child[0] === "PARAMETER")
.map((param) => this.convert(param));
break;

case "METHOD":
result.type = "Method";
result.name = node[1];
result.id = this.getId(node);
break;

case "PARAMETER":
result.type = "Parameter";
result.name = node[1];
result.value = parseFloat(node[2]);
result.unit = this.convertUnit(
node.find(
(child) =>
Array.isArray(child) &&
(child[0] === "LENGTHUNIT" || child[0] === "ANGLEUNIT" || child[0] === "SCALEUNIT")
)
);
result.id = this.getId(node);
break;

case "BOUNDCRS":
result.type = "BoundCRS";

// Process SOURCECRS
const sourceCrsNode = node.find((child) => Array.isArray(child) && child[0] === "SOURCECRS");
if (sourceCrsNode) {
const sourceCrsContent = sourceCrsNode.find((child) => Array.isArray(child));
result.source_crs = sourceCrsContent ? this.convert(sourceCrsContent) : null;
}

// Process TARGETCRS
const targetCrsNode = node.find((child) => Array.isArray(child) && child[0] === "TARGETCRS");
if (targetCrsNode) {
const targetCrsContent = targetCrsNode.find((child) => Array.isArray(child));
result.target_crs = targetCrsContent ? this.convert(targetCrsContent) : null;
}

// Process ABRIDGEDTRANSFORMATION
const transformationNode = node.find((child) => Array.isArray(child) && child[0] === "ABRIDGEDTRANSFORMATION");
if (transformationNode) {
result.transformation = this.convert(transformationNode);
} else {
result.transformation = null;
}
break;

case "ABRIDGEDTRANSFORMATION":
result.type = "Transformation";
result.name = node[1];
result.method = node.find((child) => Array.isArray(child) && child[0] === "METHOD")
? this.convert(node.find((child) => Array.isArray(child) && child[0] === "METHOD"))
: null;

result.parameters = node
.filter((child) => Array.isArray(child) && (child[0] === "PARAMETER" || child[0] === "PARAMETERFILE"))
.map((param) => {
if (param[0] === "PARAMETER") {
return this.convert(param);
} else if (param[0] === "PARAMETERFILE") {
return {
name: param[1],
value: param[2],
id: {
"authority": "EPSG",
"code": 8656
}
};
}
});

// Adjust the Scale difference parameter if present
if (result.parameters.length === 7) {
const scaleDifference = result.parameters[6];
if (scaleDifference.name === "Scale difference") {
scaleDifference.value = Math.round((scaleDifference.value - 1) * 1e12) / 1e6;
}
}

result.id = this.getId(node);
break;

case "AXIS":
if (!result.coordinate_system) {
result.coordinate_system = { type: "unspecified", axis: [] };
}
result.coordinate_system.axis.push(this.convertAxis(node));
break;

case "LENGTHUNIT":
const unit = this.convertUnit(node, 'LinearUnit');
if (result.coordinate_system && result.coordinate_system.axis) {
result.coordinate_system.axis.forEach((axis) => {
if (!axis.unit) {
axis.unit = unit;
}
});
}
if (unit.conversion_factor && unit.conversion_factor !== 1) {
if (result.semi_major_axis) {
result.semi_major_axis = {
value: result.semi_major_axis,
unit,
}
}
}
break;

default:
result.keyword = node[0];
break;
}

return result;
}
}

export default PROJJSONBuilderBase;
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# wkt-parser

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

The parser currently only supports wkt strings of [version 1](https://docs.ogc.org/is/18-010r7/18-010r7.html#196) (earlier than 2015).

It does not support geocentric currently (`GEOCCS`).
It does not support geocentric currently (`GEOCCS`/`). `COMPOUNDCS` is only supported for WKT1.
Loading