diff --git a/dist/index.d.ts b/dist/index.d.ts index 8db94eb6..aae9fcf5 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,19 +1,19 @@ declare module "@mapbox/shp-write" { export type OGCGeometry = - 'NULL' | - 'POINT' | - 'POLYLINE' | - 'POLYGON' | - 'MULTIPOINT' | - 'POINTZ' | - 'POLYLINEZ' | - 'POLYGONZ' | - 'MULTIPOINTZ' | - 'POINTM' | - 'POLYLINEM' | - 'POLYGONM' | - 'MULTIPOINTM' | - 'MULTIPATCH'; + | "NULL" + | "POINT" + | "POLYLINE" + | "POLYGON" + | "MULTIPOINT" + | "POINTZ" + | "POLYLINEZ" + | "POLYGONZ" + | "MULTIPOINTZ" + | "POINTM" + | "POLYLINEM" + | "POLYGONM" + | "MULTIPOINTM" + | "MULTIPATCH"; export interface DownloadOptions { folder?: string; @@ -28,7 +28,7 @@ declare module "@mapbox/shp-write" { }; } - type Compression = 'STORE' | 'DEFLATE'; + type Compression = "STORE" | "DEFLATE"; interface OutputByType { base64: string; string: string; @@ -44,14 +44,25 @@ declare module "@mapbox/shp-write" { type OutputType = keyof OutputByType; export interface ZipOptions { - compression: Compression, - outputType: OutputType + compression: Compression; + outputType: OutputType; } - export function download( - geojson: GeoJSON.FeatureCollection, - options?: DownloadOptions & ZipOptions - ): void; + export function download(geojson: GeoJSON.FeatureCollection, options?: DownloadOptions & ZipOptions): void; + + type PreparedGeojsonForWriting = { + geometries: number[] | number[][] | number[][][] | number[][][][]; + properties: {}; + type: string; + }; + + export var geojson = { + point: (geojson: { features: Feature[] }) => PreparedGeojsonForWriting, + line: (geojson: { features: Feature[] }) => PreparedGeojsonForWriting, + multiline: (geojson: { features: Feature[] }) => PreparedGeojsonForWriting, + polygon: (geojson: { features: Feature[] }) => PreparedGeojsonForWriting, + multipolygon: (geojson: { features: Feature[] }) => PreparedGeojsonForWriting, + }; export function write( data: Array, @@ -70,5 +81,6 @@ declare module "@mapbox/shp-write" { export function zip( geojson: GeoJSON.FeatureCollection, options?: DownloadOptions & ZipOptions, - stream?: boolean): Promise; + stream?: boolean + ): Promise; } diff --git a/dist/index.js b/dist/index.js index ae607deb..61362730 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,3 +1,4 @@ module.exports.download = require("../src/download"); module.exports.write = require("../src/write"); module.exports.zip = require("../src/zip"); +module.exports.geojson = require("../src/geojson"); diff --git a/src/extent.js b/src/extent.js index 64f929f3..b815929b 100644 --- a/src/extent.js +++ b/src/extent.js @@ -1,24 +1,30 @@ module.exports.enlarge = function enlargeExtent(extent, pt) { - if (pt[0] < extent.xmin) extent.xmin = pt[0]; - if (pt[0] > extent.xmax) extent.xmax = pt[0]; - if (pt[1] < extent.ymin) extent.ymin = pt[1]; - if (pt[1] > extent.ymax) extent.ymax = pt[1]; - return extent; + if (pt[0] < extent.xmin) extent.xmin = pt[0]; + if (pt[0] > extent.xmax) extent.xmax = pt[0]; + if (pt[1] < extent.ymin) extent.ymin = pt[1]; + if (pt[1] > extent.ymax) extent.ymax = pt[1]; + if ((pt[2] || 0) < extent.zmin) extent.zmin = pt[2] || 0; + if ((pt[2] || 0) > extent.zmax) extent.zmax = pt[2] || 0; + return extent; }; module.exports.enlargeExtent = function enlargeExtent(extent, ext) { - if (ext.xmax > extent.xmax) extent.xmax = ext.xmax; - if (ext.xmin < extent.xmin) extent.xmin = ext.xmin; - if (ext.ymax > extent.ymax) extent.ymax = ext.ymax; - if (ext.ymin < extent.ymin) extent.ymin = ext.ymin; - return extent; + if (ext.xmax > extent.xmax) extent.xmax = ext.xmax; + if (ext.xmin < extent.xmin) extent.xmin = ext.xmin; + if (ext.ymax > extent.ymax) extent.ymax = ext.ymax; + if (ext.ymin < extent.ymin) extent.ymin = ext.ymin; + if (ext.zmax && ext.zmax > extent.zmax) extent.zmax = ext.zmax; + if (ext.zmin && ext.zmin < extent.zmin) extent.zmin = ext.zmin; + return extent; }; -module.exports.blank = function() { - return { - xmin: Number.MAX_VALUE, - ymin: Number.MAX_VALUE, - xmax: -Number.MAX_VALUE, - ymax: -Number.MAX_VALUE - }; +module.exports.blank = function () { + return { + xmin: Number.MAX_VALUE, + ymin: Number.MAX_VALUE, + zmin: Number.MAX_VALUE, + xmax: -Number.MAX_VALUE, + ymax: -Number.MAX_VALUE, + zmax: -Number.MAX_VALUE, + }; }; diff --git a/src/geojson.js b/src/geojson.js index be8f28c8..afd51958 100644 --- a/src/geojson.js +++ b/src/geojson.js @@ -14,7 +14,7 @@ function justType(gjType, shpType) { return function (gj) { var oftype = gj.features.filter(isType(gjType)); return { - geometries: shpType === 'POLYLINE' ? [oftype.map(justCoords)] : oftype.map(justCoords), + geometries: shpType === "POLYLINE" ? oftype.map((_) => [justCoords(_)]) : oftype.map(justCoords), properties: oftype.map(justProps), type: shpType, }; @@ -22,7 +22,7 @@ function justType(gjType, shpType) { } /** - * + * * @param {Feature} feature The feature to get the coordinates from * @returns {number[] | number[][] | number[][][] | number[][][][]} */ @@ -31,8 +31,8 @@ function justCoords(feature) { } /** - * - * @param {Feature} feature The feature to get the properties from + * + * @param {Feature} feature The feature to get the properties from * @returns {Object.} */ function justProps(feature) { diff --git a/src/polyZ.js b/src/polyZ.js new file mode 100644 index 00000000..8288afd9 --- /dev/null +++ b/src/polyZ.js @@ -0,0 +1,141 @@ +var ext = require("./extent"), + types = require("./types"); + +module.exports.write = function writePoints(geometries, extent, shpView, shxView, TYPE) { + var shpI = 0, + shxI = 0, + shxOffset = 100; + + geometries.forEach(writePolyLine); + + function writePolyLine(coordinates, i) { + var flattened = justCoords(coordinates), + noParts = parts([coordinates], TYPE), + contentLength = flattened.length * 16 + 48 + (noParts - 1) * 4 + flattened.length * 8 + 16; + + var featureExtent = flattened.reduce(function (extent, c) { + return ext.enlarge(extent, c); + }, ext.blank()); + + // INDEX + shxView.setInt32(shxI, shxOffset / 2); // offset + shxView.setInt32(shxI + 4, contentLength / 2); // offset length + + shxI += 8; + shxOffset += contentLength + 8; + + shpView.setInt32(shpI, i + 1); // record number + shpView.setInt32(shpI + 4, contentLength / 2); // length + shpView.setInt32(shpI + 8, TYPE, true); // POLYLINEZ=13 + shpView.setFloat64(shpI + 12, featureExtent.xmin, true); // EXTENT + shpView.setFloat64(shpI + 20, featureExtent.ymin, true); + shpView.setFloat64(shpI + 28, featureExtent.xmax, true); + shpView.setFloat64(shpI + 36, featureExtent.ymax, true); + shpView.setInt32(shpI + 44, noParts, true); + shpView.setInt32(shpI + 48, flattened.length, true); // POINTS + shpView.setInt32(shpI + 52, 0, true); // The first part - index zero + + var onlyParts = coordinates.reduce(function (arr, coords) { + if (Array.isArray(coords[0][0])) { + arr = arr.concat(coords); + } else { + arr.push(coords); + } + return arr; + }, []); + for (var p = 1; p < noParts; p++) { + shpView.setInt32( + // set part index + shpI + 52 + p * 4, + onlyParts.reduce(function (a, b, idx) { + return idx < p ? a + b.length : a; + }, 0), + true + ); + } + + var zMin = Number.MAX_VALUE; + var zMax = -Number.MAX_VALUE; + + shpI += 56 + (noParts - 1) * 4; + + flattened.forEach(function writeLine(coords, i) { + if ((coords[2] || 0) < zMin) zMin = coords[2] || 0; + if ((coords[2] || 0) > zMax) zMax = coords[2] || 0; + + shpView.setFloat64(shpI, coords[0], true); // X + shpView.setFloat64(shpI + 8, coords[1], true); // Y + shpI += 16; + }); + + // Write z value range + shpView.setFloat64(shpI, zMin, true); + shpView.setFloat64(shpI + 8, zMax, true); + shpI += 16; + + // Write z values. + flattened.forEach(function (p, i) { + shpView.setFloat64(shpI, p[2] || 0, true); + shpI += 8; + }); + } +}; + +module.exports.shpLength = function (geometries, TYPE) { + var flattened = justCoords(geometries); + var length = geometries.length * 56 + flattened.length * 16 + 32 + flattened.length * 16; + return length; +}; + +module.exports.shxLength = function (geometries) { + return geometries.length * 8; +}; + +module.exports.extent = function (coordinates) { + return justCoords(coordinates).reduce(function (extent, c) { + return ext.enlarge(extent, c); + }, ext.blank()); +}; + +function parts(geometries, TYPE) { + var no = 1; + if ( + TYPE === types.geometries.POLYGON || + TYPE === types.geometries.POLYLINE || + TYPE === types.geometries.POLYGONZ || + TYPE === types.geometries.POLYLINEZ + ) { + no = geometries.reduce(function (no, coords) { + no += coords.length; + if (Array.isArray(coords[0][0][0])) { + // multi + no += coords.reduce(function (no, rings) { + return no + rings.length - 1; // minus outer + }, 0); + } + return no; + }, 0); + } + return no; +} + +module.exports.parts = parts; + +function totalPoints(geometries) { + var sum = 0; + geometries.forEach(function (g) { + sum += g.length; + }); + return sum; +} + +function justCoords(coords, l) { + if (l === undefined) l = []; + if (typeof coords[0][0] == "object") { + return coords.reduce(function (memo, c) { + return memo.concat(justCoords(c)); + }, l); + } else { + return coords; + } +} diff --git a/src/write.js b/src/write.js index 42d9ff96..1c868910 100644 --- a/src/write.js +++ b/src/write.js @@ -1,63 +1,63 @@ -var types = require('./types'); -var dbf = require('dbf'); -var prj = require('./prj'); -var pointWriter = require('./points'); -var polyWriter = require('./poly'); +var types = require("./types"); +var dbf = require("dbf"); +var prj = require("./prj"); +var pointWriter = require("./points"); +var polyWriter = require("./poly"); +var polyZWriter = require("./polyZ"); var writers = { - 1: pointWriter, - 5: polyWriter, - 3: polyWriter + 1: pointWriter, + 5: polyWriter, + 3: polyWriter, + 13: polyZWriter, }; module.exports = write; // Low-level writing interface function write(rows, geometry_type, geometries, callback) { - - var TYPE = types.geometries[geometry_type]; - var writer = writers[TYPE]; - var parts = writer.parts(geometries, TYPE); - var shpLength = 100 + (parts - geometries.length) * 4 + writer.shpLength(geometries); - var shxLength = 100 + writer.shxLength(geometries); - var shpBuffer = new ArrayBuffer(shpLength); - var shpView = new DataView(shpBuffer); - var shxBuffer = new ArrayBuffer(shxLength); - var shxView = new DataView(shxBuffer); - var extent = writer.extent(geometries); - - writeHeader(shpView, TYPE); - writeHeader(shxView, TYPE); - writeExtent(extent, shpView); - writeExtent(extent, shxView); - - writer.write(geometries, extent, - new DataView(shpBuffer, 100), - new DataView(shxBuffer, 100), - TYPE); - - shpView.setInt32(24, shpLength / 2); - shxView.setInt32(24, (50 + geometries.length * 4)); - - var dbfBuf = dbf.structure(rows); - - callback(null, { - shp: shpView, - shx: shxView, - dbf: dbfBuf, - prj: prj - }); + var TYPE = types.geometries[geometry_type]; + var writer = writers[TYPE]; + var parts = writer.parts(geometries, TYPE); + var shpLength = 100 + (parts - geometries.length) * 4 + writer.shpLength(geometries); + var shxLength = 100 + writer.shxLength(geometries); + var shpBuffer = new ArrayBuffer(shpLength); + var shpView = new DataView(shpBuffer); + var shxBuffer = new ArrayBuffer(shxLength); + var shxView = new DataView(shxBuffer); + var extent = writer.extent(geometries); + + writeHeader(shpView, TYPE); + writeHeader(shxView, TYPE); + writeExtent(extent, shpView); + writeExtent(extent, shxView); + + writer.write(geometries, extent, new DataView(shpBuffer, 100), new DataView(shxBuffer, 100), TYPE); + + shpView.setInt32(24, shpLength / 2); + shxView.setInt32(24, 50 + geometries.length * 4); + + var dbfBuf = dbf.structure(rows); + + callback(null, { + shp: shpView, + shx: shxView, + dbf: dbfBuf, + prj: prj, + }); } function writeHeader(view, TYPE) { - view.setInt32(0, 9994); - view.setInt32(28, 1000, true); - view.setInt32(32, TYPE, true); + view.setInt32(0, 9994); + view.setInt32(28, 1000, true); + view.setInt32(32, TYPE, true); } function writeExtent(extent, view) { - view.setFloat64(36, extent.xmin, true); - view.setFloat64(44, extent.ymin, true); - view.setFloat64(52, extent.xmax, true); - view.setFloat64(60, extent.ymax, true); + view.setFloat64(36, extent.xmin, true); + view.setFloat64(44, extent.ymin, true); + view.setFloat64(52, extent.xmax, true); + view.setFloat64(60, extent.ymax, true); + view.setFloat64(68, extent.zmin, true); + view.setFloat64(76, extent.zmax, true); } diff --git a/test/comparison_files/zip_polyline_1.zip b/test/comparison_files/zip_polyline_1.zip new file mode 100644 index 00000000..7db60548 Binary files /dev/null and b/test/comparison_files/zip_polyline_1.zip differ diff --git a/test/zip.test.js b/test/zip.test.js new file mode 100644 index 00000000..1545d5a9 --- /dev/null +++ b/test/zip.test.js @@ -0,0 +1,62 @@ +var expect = require("expect.js"), + zip = require("../src/zip"), + path = require("path"), + fs = require("fs"); + +describe("zip", function () { + describe("#polyline", function () { + it("1. multi-feature polyline with associated feature data", async function () { + const options = { + outputType: "blob", + compression: "STORE", + }; + + var geojson = { + type: "FeatureCollection", + features: [ + { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [ + [0, 0], + [10, 0], + ], + }, + properties: { + name: "Foo", + }, + }, + { + type: "Feature", + geometry: { + type: "LineString", + coordinates: [ + [0, 10], + [10, 10], + ], + }, + properties: { + name: "Bar", + }, + }, + ], + }; + + const zippedShapefile = await zip(geojson, options); + + const testFile = path.join("test", "comparison_files", "zip_polyline_1_test.zip"); + const sourceFile = path.join("test", "comparison_files", "zip_polyline_1.zip"); + + await new Promise(async function (resolve, reject) { + fs.writeFile(testFile, Buffer.from(await zippedShapefile.arrayBuffer()), () => resolve("saved")); + }); + + const testFileData = fs.readFileSync(testFile).toString(); + const sourceFileData = fs.readFileSync(sourceFile).toString(); + expect(testFileData).to.equal(sourceFileData); + + fs.rmSync(testFile); + }); + }); +});