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
12 changes: 6 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: 'Setup Node.js'
uses: 'actions/setup-node@v3'
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 20
- name: Install dependencies
run: |
npm i
Expand All @@ -30,12 +30,12 @@ jobs:
if: github.event_name == 'push' && contains(github.ref, 'release')
needs: build-and-test
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: 'Setup Node.js'
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
registry-url: https://registry.npmjs.org/
node-version: 18
node-version: 20
- name: Install dependencies
run: |
npm i
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "shpts",
"private": false,
"version": "1.0.9",
"version": "1.1.0",
"type": "module",
"repository": {
"type": "git",
Expand Down
16 changes: 13 additions & 3 deletions shpts/geometry/base.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { Coord, CoordType } from '@shpts/types/coordinate';
import { GeoJsonGeom } from '@shpts/types/geojson';
import { BoundingBox } from '@shpts/types/data';
import { BoundingBox, GeomHeader } from '@shpts/types/data';
import { MemoryStream } from '@shpts/utils/stream';

export abstract class BaseRecord {
constructor(readonly coordType: CoordType) {}
constructor(readonly coordType: CoordType, private hasMValuesPresent: boolean) {}
abstract toGeoJson(): GeoJsonGeom;

get hasZ(): boolean {
return this.coordType === CoordType.XYZM;
}

get hasM(): boolean {
get hasOptionalM(): boolean {
return this.coordType === CoordType.XYM || this.coordType === CoordType.XYZM;
}

get hasM(): boolean {
return this.hasMValuesPresent;
}

get coordLength(): number {
if (this.coordType === CoordType.XY) return 2;
if (this.coordType === CoordType.XYM) return 3;
Expand All @@ -35,6 +39,12 @@ export abstract class BaseRecord {
yMax: yMax,
};
}

static recordReadingFinalized(shpStream: MemoryStream, header: GeomHeader) {
//per spec, each record is (4 + header.length) * 2 bytes long
//if the stream is at the end of the record, then we are good
return shpStream.tell === header.offset + (4 + header.length) * 2;
}
}

export abstract class BaseRingedRecord extends BaseRecord {
Expand Down
12 changes: 8 additions & 4 deletions shpts/geometry/multipatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { assemblePolygonsWithHoles } from '@shpts/utils/orientation';
import { GeomUtil } from '@shpts/utils/geometry';

export class MultiPatchRecord extends BaseRingedRecord {
constructor(public coords: MultiPatchCoord, coordType: CoordType) {
super(coordType);
constructor(public coords: MultiPatchCoord, coordType: CoordType, hasMValuesPresent: boolean) {
super(coordType, hasMValuesPresent);
}

get type() {
Expand All @@ -23,7 +23,7 @@ export class MultiPatchRecord extends BaseRingedRecord {

static fromPresetReader(reader: ShapeReader, header: GeomHeader) {
const hasZ = reader.hasZ;
const hasM = reader.hasM;
const hasOptionalM = reader.hasOptionalM;
const shpStream = reader.shpStream;
let z, m;

Expand All @@ -34,12 +34,16 @@ export class MultiPatchRecord extends BaseRingedRecord {
const partTypes = shpStream.readInt32Array(numParts, true);
const xy = shpStream.readDoubleArray(numPoints * 2, true);
if (hasZ) z = MultiPatchRecord.getZValues(shpStream, numPoints);

const hasM = !this.recordReadingFinalized(shpStream, header) && hasOptionalM;
if (hasM) m = MultiPatchRecord.getMValues(shpStream, numPoints);

const coords = MultiPatchRecord.getCoords(parts, xy, z, m);
const assembledCoords = MultiPatchRecord.assemblePolygonsWithHoles(coords, partTypes);
return new MultiPatchRecord(
assembledCoords as MultiPatchCoord,
GeomUtil.coordType(header.type)
GeomUtil.coordType(header.type),
hasM
);
}

Expand Down
15 changes: 11 additions & 4 deletions shpts/geometry/multipoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { GeomHeader } from '@shpts/types/data';
import { MemoryStream } from '@shpts/utils/stream';

export class MultiPointRecord extends BaseRecord {
constructor(public coords: MultiPointCoord, coordType: CoordType) {
super(coordType);
constructor(public coords: MultiPointCoord, coordType: CoordType, hasMValuesPresent: boolean) {
super(coordType, hasMValuesPresent);
}

get type() {
Expand All @@ -17,17 +17,24 @@ export class MultiPointRecord extends BaseRecord {

static fromPresetReader(reader: ShapeReader, header: GeomHeader) {
const hasZ = reader.hasZ;
const hasM = reader.hasM;
const hasOptionalM = reader.hasOptionalM;
const shpStream = reader.shpStream;
let z, m;

MultiPointRecord.readBbox(shpStream); //throw away the bbox
const numPoints = shpStream.readInt32(true);
const xy = shpStream.readDoubleArray(numPoints * 2, true);
if (hasZ) z = MultiPointRecord.getZValues(shpStream, numPoints);

const hasM = !this.recordReadingFinalized(shpStream, header) && hasOptionalM;
if (hasM) m = MultiPointRecord.getMValues(shpStream, numPoints);

const coords = MultiPointRecord.getCoords(numPoints, xy, z, m);
return new MultiPointRecord(coords as MultiPointCoord, GeomUtil.coordType(header.type));
return new MultiPointRecord(
coords as MultiPointCoord,
GeomUtil.coordType(header.type),
hasM
);
}

private static getZValues(shpStream: MemoryStream, numPoints: number) {
Expand Down
2 changes: 1 addition & 1 deletion shpts/geometry/null.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CoordType } from '@shpts/types/coordinate';

export class ShpNullGeom extends BaseRecord {
constructor() {
super(CoordType.NULL);
super(CoordType.NULL, false);
}

get type() {
Expand Down
17 changes: 7 additions & 10 deletions shpts/geometry/point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { GeomHeader } from '@shpts/types/data';
import { GeomUtil } from '@shpts/utils/geometry';

export class PointRecord extends BaseRecord {
constructor(public coords: PointCoord, coordType: CoordType) {
super(coordType);
constructor(public coords: PointCoord, coordType: CoordType, hasMValuesPresent: boolean) {
super(coordType, hasMValuesPresent);
}

get type() {
Expand All @@ -16,22 +16,19 @@ export class PointRecord extends BaseRecord {

static fromPresetReader(reader: ShapeReader, header: GeomHeader) {
const hasZ = reader.hasZ;
const hasM = reader.hasM;
const hasOptionalM = reader.hasOptionalM;

const shpStream = reader.shpStream;

const coord = [];
coord.push(shpStream.readDouble(true)); //x
coord.push(shpStream.readDouble(true)); //y
if (hasZ) coord.push(shpStream.readDouble(true)); //z

if (hasM) {
if (hasZ) {
coord.push(shpStream.readDouble(true)); //z
}
coord.push(shpStream.readDouble(true)); //m
}
const hasM = !this.recordReadingFinalized(shpStream, header) && hasOptionalM;
if (hasM) coord.push(shpStream.readDouble(true)); //m

return new PointRecord(coord as PointCoord, GeomUtil.coordType(header.type));
return new PointRecord(coord as PointCoord, GeomUtil.coordType(header.type), hasM);
}

toGeoJson() {
Expand Down
11 changes: 7 additions & 4 deletions shpts/geometry/polygon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import { GeomHeader } from '@shpts/types/data';
import { assemblePolygonsWithHoles } from '@shpts/utils/orientation';

export class PolygonRecord extends BaseRingedRecord {
constructor(public coords: PolygonCoord, coordType: CoordType) {
super(coordType);
constructor(public coords: PolygonCoord, coordType: CoordType, hasMValuesPresent: boolean) {
super(coordType, hasMValuesPresent);
}

get type() {
Expand All @@ -23,7 +23,7 @@ export class PolygonRecord extends BaseRingedRecord {

static fromPresetReader(reader: ShapeReader, header: GeomHeader) {
const hasZ = reader.hasZ;
const hasM = reader.hasM;
const hasOptionalM = reader.hasOptionalM;
const shpStream = reader.shpStream;
let z, m;

Expand All @@ -33,10 +33,13 @@ export class PolygonRecord extends BaseRingedRecord {
const parts = shpStream.readInt32Array(numParts, true);
const xy = shpStream.readDoubleArray(numPoints * 2, true);
if (hasZ) z = PolygonRecord.getZValues(shpStream, numPoints);

const hasM = !this.recordReadingFinalized(shpStream, header) && hasOptionalM;
if (hasM) m = PolygonRecord.getMValues(shpStream, numPoints);

const coords = PolygonRecord.getCoords(parts, xy, z, m);
const polygons = assemblePolygonsWithHoles(coords);
return new PolygonRecord(polygons as PolygonCoord, GeomUtil.coordType(header.type));
return new PolygonRecord(polygons as PolygonCoord, GeomUtil.coordType(header.type), hasM);
}

toGeoJson(): GeoJsonGeom {
Expand Down
11 changes: 7 additions & 4 deletions shpts/geometry/polyline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { GeomUtil } from '@shpts/utils/geometry';
import { GeomHeader } from '@shpts/types/data';

export class PolyLineRecord extends BaseRingedRecord {
constructor(public coords: PolyLineCoord, coordType: CoordType) {
super(coordType);
constructor(public coords: PolyLineCoord, coordType: CoordType, hasMValuesPresent: boolean) {
super(coordType, hasMValuesPresent);
}

get type() {
Expand All @@ -17,7 +17,7 @@ export class PolyLineRecord extends BaseRingedRecord {

static fromPresetReader(reader: ShapeReader, header: GeomHeader) {
const hasZ = reader.hasZ;
const hasM = reader.hasM;
const hasOptionalM = reader.hasOptionalM;
const shpStream = reader.shpStream;
let z, m;

Expand All @@ -27,9 +27,12 @@ export class PolyLineRecord extends BaseRingedRecord {
const parts = shpStream.readInt32Array(numParts, true);
const xy = shpStream.readDoubleArray(numPoints * 2, true);
if (hasZ) z = PolyLineRecord.getZValues(shpStream, numPoints);

const hasM = !this.recordReadingFinalized(shpStream, header) && hasOptionalM;
if (hasM) m = PolyLineRecord.getMValues(shpStream, numPoints);

const coords = PolyLineRecord.getCoords(parts, xy, z, m);
return new PolyLineRecord(coords as PolyLineCoord, GeomUtil.coordType(header.type));
return new PolyLineRecord(coords as PolyLineCoord, GeomUtil.coordType(header.type), hasM);
}

toGeoJson() {
Expand Down
9 changes: 7 additions & 2 deletions shpts/reader/shpReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export class ShapeReader {

readonly recordCount: number = 0;
readonly hasZ: boolean;
readonly hasM: boolean;
readonly hasOptionalM: boolean;

private constructor(shp: ArrayBuffer, shx: ArrayBuffer) {
this.shpStream = new MemoryStream(shp);
Expand All @@ -27,9 +27,12 @@ export class ShapeReader {
if (this.shpHeader.type !== this.shxHeader.type)
throw new Error('SHP / SHX shapetype mismatch');

//const zRangeDefined = !isMNaN(this.shpHeader.zRange.min) && !isMNaN(this.shpHeader.zRange.max);
//const mRangeDefined = !isMNaN(this.shpHeader.mRange.min) && !isMNaN(this.shpHeader.mRange.max);

this.recordCount = (this.shxHeader.fileLength - 100) / 8;
this.hasZ = GeomUtil.hasZ(this.shpHeader.type);
this.hasM = GeomUtil.hasM(this.shpHeader.type);
this.hasOptionalM = GeomUtil.hasOptionalM(this.shpHeader.type);
}

static async fromFile(shp: File, shx: File) {
Expand Down Expand Up @@ -62,10 +65,12 @@ export class ShapeReader {
}

private readGeomHeader(): GeomHeader {
const offset = this.shpStream.tell;
const recNum = this.shpStream.readInt32(false);
const len = this.shpStream.readInt32(false);
const type: ShapeType = this.shpStream.readInt32(true) as ShapeType;
return {
offset,
length: len,
recordNum: recNum,
type: type,
Expand Down
3 changes: 2 additions & 1 deletion shpts/shpts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DbfFieldDescr, DbfFieldType } from './types/dbfTypes';
import { Coord, CoordType } from './types/coordinate';
import { triangulate } from './utils/triangulation';
import { BaseRecord } from './geometry/base';
import { ShapeType } from './utils/geometry';

export {
DbfReader,
Expand All @@ -31,4 +32,4 @@ export {
triangulate,
};

export type { DbfFieldType, DbfFieldDescr, Coord };
export type { DbfFieldType, DbfFieldDescr, Coord, ShapeType };
1 change: 1 addition & 0 deletions shpts/types/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface ShxRecord {
}

export interface GeomHeader {
offset: number;
recordNum: number;
length: number;
type: ShapeType;
Expand Down
2 changes: 1 addition & 1 deletion shpts/utils/geometry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export class GeomUtil {
return type === CoordType.XYZM;
}

static hasM(shapeType: ShapeType): boolean {
static hasOptionalM(shapeType: ShapeType): boolean {
const type = GeomUtil.coordType(shapeType);
return type === CoordType.XYZM || type === CoordType.XYM;
}
Expand Down
20 changes: 19 additions & 1 deletion test/polygon.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';
import { expectGeometry, expectRing, openFileAsArray } from './utils';
import { ShapeReader, PolygonRecord, CoordType } from '@shpts/shpts';
import { ShapeReader, PolygonRecord, CoordType, FeatureReader } from '@shpts/shpts';

test('Reading PolygonRecord', async () => {
const shpBuffer = openFileAsArray('testdata/polygon.shp');
Expand Down Expand Up @@ -39,6 +39,7 @@ test('Reading PolygonRecord', async () => {
]);

geom = expectGeometry(reader, 1, CoordType.XY, PolygonRecord);
expect(geom.hasM).toBeFalsy();
expect(geom.type).toEqual('Polygon');
expect(geom.coords.length).toBe(1);
polygon = geom.coords[0];
Expand Down Expand Up @@ -114,6 +115,7 @@ test('Reading PolygonRecord with M', async () => {
expect(reader.recordCount).toBe(2);

let geom = expectGeometry(reader, 0, CoordType.XYM, PolygonRecord);
expect(geom.hasM).toBeTruthy();
expect(geom.type).toEqual('Polygon');
expect(geom.coords.length).toBe(1);
let polygon = geom.coords[0];
Expand Down Expand Up @@ -256,3 +258,19 @@ test('Reading PolygonZ Terrain Example', async () => {
const reader = await ShapeReader.fromArrayBuffer(shpBuffer, shxBuffer);
expect(reader.recordCount).toBe(42798);
});

test('Reading PolygonZ without M values', async () => {
const shpBuffer = openFileAsArray('testdata/polygonZ.shp');
const shxBuffer = openFileAsArray('testdata/polygonZ.shx');
const dbfBuffer = openFileAsArray('testdata/polygonZ.dbf');

const reader = await FeatureReader.fromArrayBuffers(shpBuffer, shxBuffer, dbfBuffer);
const col = reader.readFeatureCollection();
const features = col.features;
expect(features.length).toBe(1);

const geom = features[0].geom;
expect(geom.type).toEqual('Polygon');
expect(geom.hasM).toBeFalsy();
expect(geom.hasZ).toBeTruthy();
});
Loading
Loading