Skip to content

Commit

Permalink
Data sources update
Browse files Browse the repository at this point in the history
- Switched from epsg.io and epsg-index to using epsg-database + additional sources.
- Added support for more conversion parameters and units.
- Fixed incorrect units mapping.
- Fixed +towgs84 mapping for datum 6135.
- Updated data.
  • Loading branch information
matafokka committed Jan 14, 2024
1 parent 3a37eed commit 20e1b7a
Show file tree
Hide file tree
Showing 23 changed files with 3,852 additions and 2,707 deletions.
294 changes: 250 additions & 44 deletions EPSG/CRSWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,72 +13,278 @@ GeoTIFF also contains ModelTiepointTag which can be used to specify XYZ coordina
So, for now, there's no standard use for this tag.
*/

const epsgIndex = require("epsg-index/all.json");
const got = require("got");
const datums = require("./data/GeogGeodeticDatumGeoKey.js");
const EllipsoidalCS = require("./data/EllipsoidalCS.js");
const conversions = require("./data/ProjectionGeoKey.js");
const Units = require("./data/Units.js");
const additionalCrs = require("./data/AdditionalCRS.js");
const forEach = require("./forEachEntryInEPSG.js");

function getObjectFromProjString(proj) {
if (!proj)
return {}

const pairs = proj.split(" ");
const obj = {};

for (const pair of pairs) {
const [key, value] = pair.split("=");
obj[key] = value ?? null;
}

return obj;
}

// These CS have messed up description, but they're ok without transformation
const CS_IGNORE_ORIENTATION = {
1035: true,
1036: true,
1037: true,
1038: true,
4499: true,
4463: true,
4464: true,
4465: true,
4466: true,
4467: true,
4468: true,
4469: true,
4470: true,
6500: true,
}

const orientationLettersStr = "nsewud";
const ORIENTATION_LETTERS = {};

for (const letter of orientationLettersStr)
ORIENTATION_LETTERS[letter] = true;

forEach(`
SELECT crs.coord_ref_sys_code AS id,
crs.coord_ref_sys_kind AS type,
crs.datum_code AS datum,
crs.cmpd_horizcrs_code AS basecrs,
coord_sys_code AS cs
FROM epsg.epsg_coordinatereferencesystem AS crs
WHERE crs.coord_ref_sys_kind NOT LIKE 'vertical'
crs.coord_ref_sys_kind as type,
cs.coord_sys_name as cs_name,
crs.cmpd_horizcrs_code AS compound_base_crs,
crs.datum_code AS datum,
crs.base_crs_code AS base_crs,
crs.projection_conv_code AS conversion,
cs.coord_sys_code AS cs_id,
base_crs_data.datum_code AS base_datum
FROM epsg.epsg_coordinatereferencesystem as crs
LEFT JOIN epsg.epsg_coordinatereferencesystem base_crs_data ON crs.base_crs_code = base_crs_data.coord_ref_sys_code
LEFT JOIN epsg.epsg_coordinatesystem cs ON cs.coord_sys_code = crs.coord_sys_code
WHERE crs.coord_ref_sys_kind NOT LIKE 'vertical' and crs.coord_ref_sys_kind NOT LIKE 'engineering'
ORDER BY crs.coord_ref_sys_kind DESC
`, async (result, fetchedCRS) => {

// Due to sorting, compound CRS comes last
if (result.type === "compound")
return fetchedCRS[result.basecrs.toString()];

// Check if CRS in epsg-index library
let idStr = result.id.toString();
if (idStr in epsgIndex && epsgIndex[idStr].proj4)
return epsgIndex[idStr].proj4;

// Try to fetch proj4 string from epsg.io
if (result.type === "projected") {
let response;
try {
response = (await got(`https://epsg.io/${idStr}.proj4`)).body; // Might respond with 520 error
} catch (e) {
return fetchedCRS[result.compound_base_crs.toString()];

let conversion = conversions[result.conversion + ""];

if (!conversion) {
if (result.type === "projected")
return;
}
if (response.startsWith("+p"))
return response;
// All PCS has been processed by epsg-index or epsg.io
// Any other PCS can't be represented as Proj4 string
return;
else if (result.type === "geocentric")
conversion = "geocent";
else
conversion = "longlat";
}

// Only GCS left
const datum = datums[result.datum] || datums[result.base_datum];

let str = "+proj=longlat ";
let csObj = EllipsoidalCS[result.cs.toString()];
if (!csObj)
// Datum is always present, but I like to check anyway
if (!datum)
return;
str += "+axis=" + csObj.axis + " ";

if (result.datum) {
let datumValue = datums[result.datum.toString()];
if (datumValue)
str += datumValue;
const conversionKeys = getObjectFromProjString("+proj=" + conversion);
const datumKeys = getObjectFromProjString(datum);

for (const key in datumKeys) {
conversionKeys[key] = datumKeys[key];
}

if (csObj.x || csObj.y)
return {
p: str,
x: csObj.x,
y: csObj.y,
// Exceptions

if (result.cs_id === 4468) {
const value = parseFloat(conversionKeys["+lat_0"]);
conversionKeys["+lat_0"] = (isNaN(value) ? 0 : value) + 90; // Not sure if it must be added or replaced
}

// Merge keys into a string

let projStr = "";
for (const key in conversionKeys) {
projStr += key;
const value = conversionKeys[key];

if (value !== null) {
projStr += "=" + value;
}

projStr += " ";
}

projStr = projStr.substring(0, projStr.length - 1);

if (!result.cs_name)
return projStr;

// Get orientation

// For some reason, CS parameters are merged into one string instead of being split to the columns
// Example:
// Ellipsoidal 3D CS. Axes: latitude, longitude, ellipsoidal height. Orientations: north, east, up. UoM: degree, degree, metre.
// Let's just hope this structure won't change later, ha-ha.
let sentencesStr = result.cs_name.toLowerCase(); // Normalize string, though, it already seems normalized
if (sentencesStr.endsWith("."))
sentencesStr = sentencesStr.substring(0, sentencesStr.length - 1);
let sentences = sentencesStr.split(". ");

// We don't need CS description and axes
sentences.shift();
sentences.shift();

// Split each sentence into parameter name (string before column) and values separated by a comma
// CRS should have two fields: orientations and uom
// uom can have one (for all axes), two (for lon and lat) or three (for lon, lat and height) units.
// The third one is always for height. We don't need it, so we'll ignore it later.
let crs = {};
for (let sentence of sentences) {
let paramName = "", columnIndex = 0;
for (let symbol of sentence) {
columnIndex++; // Accounting space
if (symbol === ":")
break;
paramName += symbol;
}
crs[paramName] = sentence.substring(columnIndex + 1).split(", ");
}

// Get orientation, i.e. +axis parameter

let orientation = "";
let isOrientationValid = !!crs.orientations;

if (crs.orientations) {
for (let direction of crs.orientations) {
const firstLetter = direction[0];

if (!ORIENTATION_LETTERS[firstLetter]) {
if (!CS_IGNORE_ORIENTATION[result.cs_id])
return;

isOrientationValid = false;
break;
}

orientation += firstLetter;
}
}

// Validate orientation further
if (isOrientationValid) {
const letters = {};

return str;
for (const letter of orientation) {
letters[letter] = (letters[letter] || -1) + 1;

if (letters[letter] > 1) {
isOrientationValid = false;

if (CS_IGNORE_ORIENTATION[result.cs_id])
break;
else
return;
}
}
}

let uomsNames = {
x: crs.uom[0],
y: crs.uom[1]
};

if (!uomsNames.y)
uomsNames.y = uomsNames.x;

let uoms = {};
let axes = ["x", "y"];
let isAngle = false;
for (let axis of axes) {
let uom = uomsNames[axis];
let m;
if (uom === "deg" || uom === "degree" || uom === "degrees") { // Can't just find deg because there're degree with hemisphere and dec degree
m = 1;
isAngle = true;
} else if (uom.includes("grad") || uom.includes("gon")) {
m = Units["9105"].m * 180 / Math.PI;
isAngle = true;
} else if (uom.includes("rad")) { // grad handled by previous case
m = Units["9101"].m * 180 / Math.PI;
isAngle = true;
} else if (uom === "m" || uom.includes("met"))
m = 1;
else if (uom === "ft")
m = 0.3048;
else if (uom === "ftus")
m = 0.3048006096;
else if (uom === "ydind")
m = 0.3047995;
else if (uom === "ftcla")
m = 0.3047972654;
else if (uom === "ydcl")
m = 3.3047972654;
else if (uom === "chbnb")
m = Units["9042"].m;
else if (uom === "chse")
m = 20.1167651215526;
else if (uom === "chse(t)")
m = 20.116756;
else if (uom === "ftgc")
m = 0.304799710181509;
else if (uom === "ftse")
m = 0.304799471538676;
else if (uom === "km")
m = 1000;
else if (uom === "lkcla")
m = 0.201166195164;
else if (uom === "ydse")
m = 0.914398414616029;
else if (uom === "glm")
m = 1.0000135965;
else if (uom === "lk")
m = 0.201168;
else
return;

// Uoms doesn't use other units than specified above for now, but let's kinda future-proof it
if (uom.includes("μ") || (isAngle && uom.includes("m")))
m *= 0.000001;
uoms[axis] = m;
}

if (orientation && isOrientationValid)
projStr += " +axis=" + orientation;

if (!isAngle && uoms.x == uoms.y && uoms.x) {
if (uoms.x !== 1)
projStr += " +to_meter=" + uoms.x;
} else if (uoms.x || uoms.y) {
return {
p: projStr,
x: uoms.x,
y: uoms.y,
}
}

return projStr;
}, "CRS", `/**
* Maps EPSG CRS to their proj4 definitions. Should be a base for Proj4 string.
* Corresponding geokeys are GeographicTypeGeoKey and ProjectedCSTypeGeoKey.
* @type {Object}
*/`);
*/`, (obj) => {
for (const key in additionalCrs) {
if (!obj[key])
obj[key] = additionalCrs[key]
}
});
4 changes: 2 additions & 2 deletions EPSG/ConversionsWorker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* This script generates conversions (used by GeoTIFF as ProjectionGeoKey) from epsg.io. Can be used standalone or in a worker. */
/* This script generates conversions (used by GeoTIFF as ProjectionGeoKey). Can be used standalone or in a worker. */

const methods = require("./data/Methods.js")
const parameters = require("./data/MethodParameters.js");
Expand Down Expand Up @@ -29,7 +29,7 @@ forEach(`

if (uomCode in Units) {
let {m} = Units[uomCode];
if (paramDef.includes("lat") || paramDef.includes("lon"))
if (paramDef.includes("lat") || paramDef.includes("lon") || paramDef.includes("alpha") || paramDef.includes("gamma"))
m *= 180 / Math.PI; // Radians are angular base units
value *= m;
} else
Expand Down
2 changes: 1 addition & 1 deletion EPSG/DatumsWorker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* This script generates datums from epsg.io. Can be used standalone or in a worker. */
/* This script generates datums. Can be used standalone or in a worker. */

const KnownDatums = require("./data/KnownDatums.js");
const meridians = require("./data/GeogPrimeMeridianGeoKey.js");
Expand Down
Loading

0 comments on commit 20e1b7a

Please sign in to comment.