Skip to content

Commit

Permalink
feat(linzjs-geojson): add iterate and truncate utilities for geojson (#…
Browse files Browse the repository at this point in the history
…3340)

### Motivation

We have had a need to reproject and truncate geojson objects and instead
of copying the logic into multiple locations add it to the geojson
helper we have here.

### Modifications

Add truncate to truncate lat lon pairs to by default 8 decimal places
Add iterate to iterate over all the points in a geojson feature

### Verification

unit tests and is currently in use in other code bases.
  • Loading branch information
blacha authored Sep 6, 2024
1 parent 74119c0 commit 406b3eb
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 0 deletions.
2 changes: 2 additions & 0 deletions packages/linzjs-geojson/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ export * from './multipolygon/area.js';
export * from './multipolygon/clipped.js';
export * from './multipolygon/convert.js';
export * from './types.js';
export * from './util/iterate.js';
export * from './util/truncate.js';
export * from './wgs84.js';
80 changes: 80 additions & 0 deletions packages/linzjs-geojson/src/util/__test__/iterate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';

import { iterate } from '../iterate.js';

export const TestGeometries = {
Point: { type: 'Point', coordinates: [0, 1] } as GeoJSON.Point,
MultiPoint: {
type: 'MultiPoint',
coordinates: [
[0, 1],
[1, 2],
],
} as GeoJSON.MultiPoint,
Polygon: {
type: 'Polygon',
coordinates: [
[
[0, 1],
[1, 2],
],
[
[3, 4],
[5, 6],
],
],
} as GeoJSON.Polygon,
MultiPolygon: {
type: 'MultiPolygon',
coordinates: [
[
[
[0, 1],
[1, 2],
],
[
[3, 4],
[5, 6],
],
],
],
} as GeoJSON.MultiPolygon,
LineString: {
type: 'LineString',
coordinates: [
[0, 1],
[1, 2],
],
} as GeoJSON.LineString,
MultiLineString: {
type: 'MultiLineString',
coordinates: [
[
[0, 1],
[1, 2],
],
[[1, 2]],
],
} as GeoJSON.MultiLineString,
};

describe('iterate', () => {
const fakeGeojson = { type: 'Feature', properties: {} } as const;

for (const [name, geometry] of Object.entries(TestGeometries)) {
describe(name, () => {
it('should iterate a ' + name, (t) => {
const cb = t.mock.fn();
iterate({ ...fakeGeojson, geometry }, cb);
const flatCoords = geometry.coordinates.flat(100);

assert.equal(cb.mock.callCount(), flatCoords.length / 2);
for (let i = 0; i < flatCoords.length; i += 2) {
const coord = [flatCoords[i], flatCoords[i + 1]];
assert.deepEqual(cb.mock.calls[i / 2].arguments[0], coord);
}
});
});
}
});
48 changes: 48 additions & 0 deletions packages/linzjs-geojson/src/util/__test__/truncate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import assert from 'node:assert';
import { describe, it } from 'node:test';

import { iterate } from '../iterate.js';
import { truncate } from '../truncate.js';
import { TestGeometries } from './iterate.test.js';

describe('truncate', () => {
const fakeGeojson = { type: 'Feature', properties: {} } as const;

for (const [name, geom] of Object.entries(TestGeometries)) {
describe(name, () => {
it(`should truncate ${name}`, () => {
const geometry = structuredClone(geom);
const json = { ...fakeGeojson, geometry } as GeoJSON.Feature;
iterate(json, (pt) => {
pt[0] = 1 + 1e-9;
pt[1] = 1 - 1e-9;
});

truncate(json);

// Validate every point has been truncated to 1 or -1
assert.equal(
geometry.coordinates.flat(100).every((f) => f === 1),
true,
);
});

it(`should not truncate less ${name} to than 8dp`, () => {
const geometry = structuredClone(geom);
const json = { ...fakeGeojson, geometry } as GeoJSON.Feature;
iterate(json, (pt) => {
pt[0] = 1.1;
pt[1] = -1.1;
});

truncate(json);

// Validate every point has been truncated to 1 or -1
assert.equal(
geometry.coordinates.flat(100).every((f) => f === 1.1 || f === -1.1),
true,
);
});
});
}
});
39 changes: 39 additions & 0 deletions packages/linzjs-geojson/src/util/iterate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Iterate all the positions inside the features positions
*
* @throws if geometry is of unknown type
*
* @param feature Feature to iterate
* @param cb call back to run on the position
* @returns
*/
export function iterate(feature: GeoJSON.Feature, cb: (pt: [number, number]) => void): void {
const geom = feature.geometry;
if (geom.type === 'Point') return cb(geom.coordinates as [number, number]);
if (geom.type === 'MultiPoint') return iteratePosition(geom.coordinates, cb);
if (geom.type === 'Polygon') return iteratePosition2(geom.coordinates, cb);
if (geom.type === 'MultiPolygon') return iteratePosition3(geom.coordinates, cb);
if (geom.type === 'LineString') return iteratePosition(geom.coordinates, cb);
if (geom.type === 'MultiLineString') return iteratePosition2(geom.coordinates, cb);

throw new Error('Unknown geometry type ');
}

// Iteration functions for three levels of nested positions commonly used in geojson
function iteratePosition3(coords: GeoJSON.Position[][][], cb: (pt: [number, number]) => void): void {
for (const outer of coords) {
for (const poly of outer) {
for (const pt of poly) cb(pt as [number, number]);
}
}
}

function iteratePosition2(coords: GeoJSON.Position[][], cb: (pt: [number, number]) => void): void {
for (const poly of coords) {
for (const pt of poly) cb(pt as [number, number]);
}
}

function iteratePosition(coords: GeoJSON.Position[], cb: (pt: [number, number]) => void): void {
for (const pt of coords) cb(pt as [number, number]);
}
27 changes: 27 additions & 0 deletions packages/linzjs-geojson/src/util/truncate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { iterate } from './iterate.js';

/**
* Number of decimal places to restrict capture areas to
* Rough numbers of decimal places to precision in meters
*
* 5DP - 1m
* 6DP - 0.1m
* 7DP - 0.01m (1cm)
* 8DP - 0.001m (1mm)
*/
const DefaultTruncationFactor = 8;

/**
* Truncate a multi polygon in lat,lng to {@link DefaultTruncationFactor} decimal places
*
* @warning This destroys the source geometry
* @param polygons
*/
export function truncate(feature: GeoJSON.Feature, truncateFactor = DefaultTruncationFactor): void {
const factor = Math.pow(10, truncateFactor);

iterate(feature, (pt) => {
pt[0] = Math.round(pt[0] * factor) / factor;
pt[1] = Math.round(pt[1] * factor) / factor;
});
}

0 comments on commit 406b3eb

Please sign in to comment.