-
Notifications
You must be signed in to change notification settings - Fork 17
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(linzjs-geojson): add iterate and truncate utilities for geojson (#…
…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
Showing
5 changed files
with
196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
48
packages/linzjs-geojson/src/util/__test__/truncate.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
); | ||
}); | ||
}); | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}); | ||
} |