Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: adding a mechanism to check for presence of m-values during parsing #15

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
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
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();
});
1 change: 1 addition & 0 deletions testdata/polygonZ.cpg
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
UTF-8
Binary file added testdata/polygonZ.dbf
Binary file not shown.
1 change: 1 addition & 0 deletions testdata/polygonZ.prj
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROJCS["WGS_1984_Web_Mercator_Auxiliary_Sphere",GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137.0,298.257223563]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Mercator_Auxiliary_Sphere"],PARAMETER["False_Easting",0.0],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",0.0],PARAMETER["Standard_Parallel_1",0.0],PARAMETER["Auxiliary_Sphere_Type",0.0],UNIT["Meter",1.0]]
Binary file added testdata/polygonZ.shp
Binary file not shown.
Binary file added testdata/polygonZ.shx
Binary file not shown.
Loading