s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
296 lines • 9.91 kB
JavaScript
// Implements https://www.esri.com/content/dam/esrisites/sitecore-archive/Files/Pdfs/library/whitepapers/pdfs/shapefile.pdf
import { extendBBox } from '../../geometry';
import { toReader } from '..';
/**
* # The Shapefile Reader
*
* ## Description
* Reads data from a shapefile implementing the {@link FeatureIterator} interface
*
* NOTE: It's recommended to not parse the shapefile directly but instead:
* - `import { shapefileFromURL } from 's2-tools';`
* - `import { shapefileFromPath } from 's2-tools/file';`
*
* This ensures the other files paired with the shapefile are loaded to properly handle the
* projection and properties data.
*
* ## Usage
* ```ts
* import { ShapeFileReader, DataBaseFile, Transformer } from 's2-tools';
* import { FileReader } from 's2-tools/file';
*
* const transform = new Transformer();
* const dbf = new DataBaseFile(new FileReader('./data.dbf'), 'utf-8');
* const reader = new ShapeFileReader(new FileReader('./data.shp'), dbf, transform);
*
* // read all the features
* for await (const feature of reader) {
* console.log(feature);
* }
* ```
*/
export class ShapeFileReader {
dbf;
transform;
reader;
#header;
rows = [];
/**
* @param input - the input data structure to parse
* @param dbf - the dbf file
* @param transform - transform mechanics if they exist
*/
constructor(input, dbf, transform) {
this.dbf = dbf;
this.transform = transform;
this.reader = toReader(input);
this.#parseHeader();
this.#getRows();
}
/**
* Return a shallow copy of the header data
* @returns - a shallow copy of the header data
*/
getHeader() {
return { ...this.#header };
}
/**
* Return all the features in the shapefile
* @returns - a collection of VectorFeatures
*/
async getFeatureCollection() {
const featureCollection = {
type: 'FeatureCollection',
features: [],
bbox: this.#header.bbox,
};
for await (const feature of this)
featureCollection.features.push(feature);
return featureCollection;
}
/**
* Iterate over all features in the shapefile
* @yields {VectorFeature}
*/
async *[Symbol.asyncIterator]() {
for (let i = 0; i < this.rows.length; i++) {
const feature = this.#parseRow(this.rows[i], i);
if (feature !== undefined)
yield feature;
}
}
/** Internal parse for the header */
#parseHeader() {
const { reader } = this;
this.#header = {
length: reader.getInt32(6 << 2) << 1,
version: reader.getInt32(7 << 2, true),
shpCode: reader.getInt32(8 << 2, true),
bbox: [
reader.getFloat64(9 << 2, true),
reader.getFloat64(11 << 2, true),
reader.getFloat64(13 << 2, true),
reader.getFloat64(15 << 2, true),
reader.getFloat64(17 << 2, true),
reader.getFloat64(19 << 2, true),
],
};
if (this.#header.shpCode > 20) {
this.#header.shpCode -= 20;
}
}
/** Internal parser to build all the row offsets */
#getRows() {
const { reader, rows } = this;
let offset = 100;
const len = reader.byteLength - 8;
while (offset <= len) {
const offsetLength = reader.getInt32(offset + 4) << 1;
const type = reader.getInt32(offset + 8, true);
if (offsetLength === 0)
break;
if (type !== 0)
rows.push(offset);
offset += 8 + offsetLength;
}
}
/**
* Get a row
* @param offset - offset of the row
* @returns - the row if it exists
*/
#getRow(offset) {
const { reader } = this;
const id = reader.getInt32(offset);
const len = reader.getInt32(offset + 4) << 1;
if (len === 0 || offset + len + 8 > reader.byteLength)
return;
return {
id,
len,
data: reader.slice(offset + 12, offset + 12 + len - 4),
type: reader.getInt32(offset + 8, true),
};
}
/**
* Parse a row
* @param rowOffset - the row to get and parse
* @param index - the index of the feature
* @returns - the parsed feature
*/
#parseRow(rowOffset, index) {
const row = this.#getRow(rowOffset);
if (row === undefined)
return;
const { id, type, data } = row;
const geometry = this.#parseGeometry(type, data);
if (geometry === undefined)
return;
return {
id,
type: 'VectorFeature',
properties: (this.dbf?.getProperties(index) ?? {}),
geometry,
};
}
/**
* Parse a shape geometry
* @param type - the shape type
* @param data - the shape data to parse
* @returns - the parsed geometry if its valid
*/
#parseGeometry(type, data) {
const is3D = type === 11 || type === 13 || type === 15 || type === 18;
if (type === 1 || type === 11) {
return {
type: 'Point',
is3D,
coordinates: this.#parsePoint(data, 0, is3D ? 16 : undefined),
};
}
else if (type === 8 || type === 18) {
return this.#parseMultiPoint(data, is3D);
}
else if (type === 3 || type === 5 || type === 13 || type === 15) {
const isPoly = type === 5 || type === 15;
return this.#parseMultiLine(data, isPoly, is3D);
}
else
throw new Error('invalid shape type');
}
/**
* Parse a point
* @param data - the raw data to decode
* @param offset - the offset of the point to decode
* @param offset3D - if provided, the offset of the Z value
* @returns - the decoded point
*/
#parsePoint(data, offset, offset3D) {
const point = {
x: data.getFloat64(offset, true),
y: data.getFloat64(offset + 8, true),
z: offset3D !== undefined ? data.getFloat64(offset3D, true) : undefined,
};
return this.transform?.forward(point) ?? point;
}
/**
* Parse a multi-point
* @param data - the raw data to decode
* @param is3D - is the shape a 3D shape
* @returns - the decoded point or multi-point
*/
#parseMultiPoint(data, is3D = false) {
const numPoints = data.getInt32(32, true);
if (numPoints === 0)
return;
let offset = 0;
let zOffset = 36 + 16 * numPoints;
// grab the min-max
const mins = this.#parsePoint(data, offset);
const maxs = this.#parsePoint(data, offset + 16);
offset += 36;
let bbox = [mins.x, mins.y, maxs.x, maxs.y, 0, 0];
if (is3D) {
bbox[4] = data.getFloat64(zOffset, true);
bbox[5] = data.getFloat64(zOffset + 8, true);
zOffset += 16;
}
const coordinates = [];
let index = 0;
while (index < numPoints) {
const point = this.#parsePoint(data, offset, is3D ? zOffset : undefined);
coordinates.push(point);
offset += 16;
if (is3D) {
zOffset += 8;
bbox = extendBBox(bbox, point);
}
index++;
}
if (numPoints === 1) {
return { type: 'Point', is3D, coordinates: coordinates[0] };
}
else {
return { type: 'MultiPoint', is3D, coordinates, bbox };
}
}
/**
* Parse a multi-line
* @param data - the raw data to decode
* @param isPoly - is the shape a polygon or line(s)
* @param is3D - is the shape a 3D shape
* @returns - the decoded point or multi-point
*/
#parseMultiLine(data, isPoly, is3D) {
const numParts = data.getInt32(32, true); // The number of rings in the polygon.
const numPoints = data.getInt32(36, true); // the total number of points in the polygon.
if (numPoints === 0 || numParts === 0)
return;
let offset = 0;
let zOffset = 40 + 4 * numParts + 16 * numPoints;
// grab the min-max
const mins = this.#parsePoint(data, offset);
const maxs = this.#parsePoint(data, offset + 16);
let bbox = [mins.x, mins.y, maxs.x, maxs.y, 0, 0];
offset += 40;
if (is3D) {
bbox[4] = data.getFloat64(zOffset, true);
bbox[5] = data.getFloat64(zOffset + 8, true);
zOffset += 16;
}
// build parts
const parts = [];
let done = 0;
while (done < numParts) {
parts.push(data.getInt32(offset, true));
offset += 4;
done++;
}
// build coordinates
let index = 0;
const coordinates = [];
for (let i = 0; i < numParts; i++) {
const partEnd = parts[i + 1] ?? numPoints;
// build a line for part
const line = [];
while (index < partEnd) {
const point = this.#parsePoint(data, offset, is3D ? zOffset : undefined);
line.push(point);
offset += 16;
if (is3D) {
zOffset += 8;
bbox = extendBBox(bbox, point);
}
index++;
}
coordinates.push(line);
}
if (!isPoly && numParts === 1) {
return { type: 'LineString', is3D, coordinates: coordinates[0], bbox };
}
else {
return { type: isPoly ? 'Polygon' : 'MultiLineString', is3D, coordinates, bbox };
}
}
}
//# sourceMappingURL=shp.js.map