From 2583c2980a9aedf038d461b86364a7c361dbf6da Mon Sep 17 00:00:00 2001 From: Knut Forkalsrud Date: Sat, 6 Feb 2021 12:08:36 -0800 Subject: [PATCH] Convert coordinates to WGS 84 base on the .prj file This uses proj4js to read the "well known" text of the coordinate reference system. Then as we read in coordinates from the `.shp` file we map each to WGS 84 using the same proj4js library. https://tools.ietf.org/html/rfc7946#section-4 suggests that's the only allowed coordinate reference system in GeoJSON. Unfortunately this is a bit of a hack, it breaks with streaming nature of the program. It also affects only the command line `shp2json`. Accommodations for library use are lacking. There should be a command line switch for deciding to use the `.prj` transform this or not. Arguably the mapping should not be conflated with reading, but it was the most convenient way for me to make it work. Tests are absent. --- bin/shp2json | 13 +++++++++++-- index.js | 2 +- package.json | 3 ++- shapefile/index.js | 4 ++-- shp/index.js | 11 +++++++---- shp/multipoint.js | 6 ++++-- shp/point.js | 4 ++-- shp/polygon.js | 7 ++++--- shp/polyline.js | 4 ++-- shp/read.js | 2 +- 10 files changed, 36 insertions(+), 20 deletions(-) diff --git a/bin/shp2json b/bin/shp2json index facc848..16756bc 100755 --- a/bin/shp2json +++ b/bin/shp2json @@ -2,7 +2,8 @@ var fs = require("fs"), commander = require("commander"), - shapefile = require("../"); + shapefile = require("../"), + proj4 = require('proj4'); commander .version(require("../package.json").version) @@ -26,10 +27,18 @@ else if (commander.args.length !== 1) { var out = (commander.out === "-" ? process.stdout : fs.createWriteStream(commander.out)).on("error", handleEpipe); +// My addition to read prj file, turn it into a function that I will apply to all coordinates +var shpFilename = commander.args[0], + prjFilename = shpFilename.substring(0, shpFilename.length - 4) + ".prj", + prjStr = fs.readFileSync(prjFilename).toString(), + prj = proj4(prjStr), + xform = prj.inverse.bind(prj); +// end for now, but `xform` will be passed around + shapefile.open( commander.args[0] === "-" ? process.stdin : commander.args[0], commander.geometry || commander.ignoreProperties ? null : undefined, - {encoding: commander.encoding}) + {encoding: commander.encoding, xform: xform}) .then(commander.newlineDelimited ? (commander.geometry ? writeNewlineDelimitedGeometries : writeNewlineDelimitedFeatures) : (commander.geometry ? writeGeometryCollection : writeFeatureCollection)) diff --git a/index.js b/index.js index c9739ec..15f06ef 100644 --- a/index.js +++ b/index.js @@ -26,7 +26,7 @@ export function open(shp, dbf, options) { return Promise.all([shp, dbf]).then(function(sources) { var shp = sources[0], dbf = sources[1], encoding = "windows-1252"; if (options && options.encoding != null) encoding = options.encoding; - return shapefile(shp, dbf, dbf && new TextDecoder(encoding)); + return shapefile(shp, dbf, dbf && new TextDecoder(encoding), options ? options.xform : function (x) {x}); }); } diff --git a/package.json b/package.json index 211697e..2689a11 100644 --- a/package.json +++ b/package.json @@ -34,13 +34,14 @@ "array-source": "0.0", "commander": "2", "path-source": "0.1", + "proj4": "2.7.0", "slice-source": "0.4", "stream-source": "0.3", "text-encoding": "^0.6.4" }, "devDependencies": { "package-preamble": "0.1", - "rollup": "0.49", + "rollup": "^0.49.3", "rollup-plugin-node-resolve": "3", "tape": "4", "uglify-js": "3" diff --git a/shapefile/index.js b/shapefile/index.js index b562e72..5436cd0 100644 --- a/shapefile/index.js +++ b/shapefile/index.js @@ -3,9 +3,9 @@ import shp from "../shp/index"; import shapefile_cancel from "./cancel"; import shapefile_read from "./read"; -export default function(shpSource, dbfSource, decoder) { +export default function(shpSource, dbfSource, decoder, xform) { return Promise.all([ - shp(shpSource), + shp(shpSource, xform), dbfSource && dbf(dbfSource, decoder) ]).then(function(sources) { return new Shapefile(sources[0], sources[1]); diff --git a/shp/index.js b/shp/index.js index 8c08f64..1c8be18 100644 --- a/shp/index.js +++ b/shp/index.js @@ -24,21 +24,24 @@ var parsers = { 28: parseMultiPoint // MultiPointM }; -export default function(source) { +export default function(source, xform) { source = slice(source); return source.slice(100).then(function(array) { - return new Shp(source, view(array)); + return new Shp(source, view(array), xform); }); }; -function Shp(source, header) { +function Shp(source, header, xform) { var type = header.getInt32(32, true); if (!(type in parsers)) throw new Error("unsupported shape type: " + type); this._source = source; + this._xform = xform || function (x) { x }; this._type = type; this._index = 0; this._parse = parsers[type]; - this.bbox = [header.getFloat64(36, true), header.getFloat64(44, true), header.getFloat64(52, true), header.getFloat64(60, true)]; + var topLeft = xform([header.getFloat64(36, true), header.getFloat64(44, true)]); + var botRight = xform([header.getFloat64(52, true), header.getFloat64(60, true)]); + this.bbox = [ topLeft[0], topLeft[1], botRight[0], botRight[1] ]; } var prototype = Shp.prototype; diff --git a/shp/multipoint.js b/shp/multipoint.js index 06b8f5c..0051030 100644 --- a/shp/multipoint.js +++ b/shp/multipoint.js @@ -1,5 +1,7 @@ -export default function(record) { +export default function(record, xform) { var i = 40, j, n = record.getInt32(36, true), coordinates = new Array(n); - for (j = 0; j < n; ++j, i += 16) coordinates[j] = [record.getFloat64(i, true), record.getFloat64(i + 8, true)]; + for (j = 0; j < n; ++j, i += 16) { + coordinates[j] = xform([ record.getFloat64(i, true), record.getFloat64(i + 8, true) ]); + } return {type: "MultiPoint", coordinates: coordinates}; }; diff --git a/shp/point.js b/shp/point.js index b11d569..783e652 100644 --- a/shp/point.js +++ b/shp/point.js @@ -1,3 +1,3 @@ -export default function(record) { - return {type: "Point", coordinates: [record.getFloat64(4, true), record.getFloat64(12, true)]}; +export default function(record, xform) { + return {type: "Point", coordinates: xform([ record.getFloat64(4, true), record.getFloat64(12, true) ])}; }; diff --git a/shp/polygon.js b/shp/polygon.js index 3c8e6fb..9dbe16e 100644 --- a/shp/polygon.js +++ b/shp/polygon.js @@ -1,7 +1,8 @@ -export default function(record) { - var i = 44, j, n = record.getInt32(36, true), m = record.getInt32(40, true), parts = new Array(n), points = new Array(m), polygons = [], holes = []; +export default function(record, xform) { + var i = 44, j, n = record.getInt32(36, true), m = record.getInt32(40, true), + parts = new Array(n), points = new Array(m), polygons = [], holes = []; for (j = 0; j < n; ++j, i += 4) parts[j] = record.getInt32(i, true); - for (j = 0; j < m; ++j, i += 16) points[j] = [record.getFloat64(i, true), record.getFloat64(i + 8, true)]; + for (j = 0; j < m; ++j, i += 16) points[j] = xform([ record.getFloat64(i, true), record.getFloat64(i + 8, true) ]); parts.forEach(function(i, j) { var ring = points.slice(i, parts[j + 1]); diff --git a/shp/polyline.js b/shp/polyline.js index f18fd3b..b39b8c3 100644 --- a/shp/polyline.js +++ b/shp/polyline.js @@ -1,7 +1,7 @@ -export default function(record) { +export default function(record, xform) { var i = 44, j, n = record.getInt32(36, true), m = record.getInt32(40, true), parts = new Array(n), points = new Array(m); for (j = 0; j < n; ++j, i += 4) parts[j] = record.getInt32(i, true); - for (j = 0; j < m; ++j, i += 16) points[j] = [record.getFloat64(i, true), record.getFloat64(i + 8, true)]; + for (j = 0; j < m; ++j, i += 16) points[j] = xform([ record.getFloat64(i, true), record.getFloat64(i + 8, true)]); return n === 1 ? {type: "LineString", coordinates: points} : {type: "MultiLineString", coordinates: parts.map(function(i, j) { return points.slice(i, parts[j + 1]); })}; diff --git a/shp/read.js b/shp/read.js index 9a695c2..2035060 100644 --- a/shp/read.js +++ b/shp/read.js @@ -24,7 +24,7 @@ export default function() { function read() { var length = header.getInt32(4, false) * 2 - 4, type = header.getInt32(8, true); return length < 0 || (type && type !== that._type) ? skip() : that._source.slice(length).then(function(chunk) { - return {done: false, value: type ? that._parse(view(concat(array.slice(8), chunk))) : null}; + return {done: false, value: type ? that._parse(view(concat(array.slice(8), chunk)), that._xform) : null}; }); }