mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
948c9ef57b
geoBoundaries has no county layer for Ireland: its ADM1 is the four provinces and its ADM2 the 166 local electoral areas, so the Atlas showed Ireland as four provinces next to a UK of counties. The Atlas builder can now take a country's regions from another source. Ireland's come from Tailte Éireann's 26 counties (CC BY 4.0), with Dublin, Cork, Galway and Limerick each one county, under their ISO 3166-2:IE codes. Those outlines come at full detail, so the builder simplifies them itself and keeps the borders two counties share identical on both sides, so no point between them falls into neither. ATLAS_ONLY rebuilds just the named countries without downloading the whole world. A migration clears Ireland's cached place regions so they are worked out again as counties, moves visited rows that name a county onto its code, and hides each county of a province a user had hidden. Provinces a user marked as visited are kept as they are. Fixes #2922
162 lines
5.6 KiB
TypeScript
162 lines
5.6 KiB
TypeScript
/**
|
|
* scripts/lib/shared-border-simplify.mjs simplifies a country's region outlines for the
|
|
* Atlas builder when the source has no simplified version of its own (Tailte Éireann's
|
|
* counties, #2922). What matters is that two regions sharing a border keep the same
|
|
* points on it, so no point between them falls into a sliver that belongs to neither.
|
|
*/
|
|
import { douglasPeucker, simplifySharedBorders } from '../../../scripts/lib/shared-border-simplify.mjs';
|
|
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
type Ring = number[][];
|
|
type Geometry = { type: 'Polygon'; coordinates: Ring[] } | { type: 'MultiPolygon'; coordinates: Ring[][] };
|
|
type Feature = { type: 'Feature'; properties: Record<string, unknown>; geometry: Geometry | null };
|
|
|
|
const polygon = (name: string, ...rings: Ring[]): Feature => ({
|
|
type: 'Feature',
|
|
properties: { name },
|
|
geometry: { type: 'Polygon', coordinates: rings },
|
|
});
|
|
|
|
// A border from about (1,0) up to about (1,1) that swings out and back, quantized like the builder's input.
|
|
const border: Ring = Array.from({ length: 41 }, (_, i) => [
|
|
Math.round((1 + 0.05 * Math.sin(i * 0.7)) * 1000) / 1000,
|
|
i / 40,
|
|
]);
|
|
|
|
/**
|
|
* Two neighbours: west runs up the shared border, east runs down it from a different
|
|
* start. Simplified each on its own, these two keep different points on the border
|
|
* (SIMPLIFY-003 checks that the fixture really does that).
|
|
*/
|
|
function neighbours(): Feature[] {
|
|
const west = [[0, 0], ...border, [0, 1], [0, 0]];
|
|
const east = [[2, 0], [2, 1], ...border.slice().reverse(), [2, 0]];
|
|
return [polygon('west', west), polygon('east', east)];
|
|
}
|
|
|
|
const outer = (f: Feature) => (f.geometry as { coordinates: Ring[] }).coordinates[0];
|
|
// The points of a ring on the shared border, its repeated closing point left out.
|
|
const onBorder = (ring: Ring) =>
|
|
ring
|
|
.slice(0, -1)
|
|
.filter(([x]) => x > 0.5 && x < 1.5)
|
|
.map((p) => p.join(','))
|
|
.sort();
|
|
|
|
describe('douglasPeucker', () => {
|
|
it('SIMPLIFY-001: keeps both ends and drops points within the tolerance', () => {
|
|
expect(
|
|
douglasPeucker(
|
|
[
|
|
[0, 0],
|
|
[1, 0.001],
|
|
[2, 0],
|
|
[3, 1],
|
|
],
|
|
0.01,
|
|
),
|
|
).toEqual([
|
|
[0, 0],
|
|
[2, 0],
|
|
[3, 1],
|
|
]);
|
|
});
|
|
|
|
it('SIMPLIFY-002: leaves a line of two points or fewer as it is', () => {
|
|
expect(
|
|
douglasPeucker(
|
|
[
|
|
[0, 0],
|
|
[1, 1],
|
|
],
|
|
5,
|
|
),
|
|
).toEqual([
|
|
[0, 0],
|
|
[1, 1],
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('simplifySharedBorders', () => {
|
|
it('SIMPLIFY-003: two neighbours keep exactly the same points on the border they share', () => {
|
|
const [west, east] = neighbours();
|
|
// Each polygon simplified on its own drifts apart on the border: the bug this avoids.
|
|
expect(onBorder(douglasPeucker(outer(west), 0.01))).not.toEqual(onBorder(douglasPeucker(outer(east), 0.01)));
|
|
|
|
const [westOut, eastOut] = simplifySharedBorders([west, east], 0.01) as Feature[];
|
|
expect(onBorder(outer(westOut))).toEqual(onBorder(outer(eastOut)));
|
|
expect(onBorder(outer(westOut)).length).toBeLessThan(border.length);
|
|
// Its two ends are where three outlines meet, and stay put.
|
|
expect(onBorder(outer(westOut))).toEqual(expect.arrayContaining([border[0].join(','), border[40].join(',')]));
|
|
});
|
|
|
|
it('SIMPLIFY-004: the result does not depend on which neighbour comes first', () => {
|
|
const [east, west] = simplifySharedBorders(neighbours().reverse(), 0.01) as Feature[];
|
|
const [west2, east2] = simplifySharedBorders(neighbours(), 0.01) as Feature[];
|
|
expect(outer(west)).toEqual(outer(west2));
|
|
expect(outer(east)).toEqual(outer(east2));
|
|
});
|
|
|
|
it('SIMPLIFY-005: an enclave shared whole with the hole around it simplifies the same on both sides', () => {
|
|
const circle = (reverse: boolean): Ring => {
|
|
const pts = Array.from({ length: 40 }, (_, i) => {
|
|
const a = (i / 40) * 2 * Math.PI;
|
|
return [Math.round((5 + Math.cos(a)) * 1000) / 1000, Math.round((5 + Math.sin(a)) * 1000) / 1000];
|
|
});
|
|
const ring = reverse ? pts.reverse() : pts;
|
|
return [...ring, ring[0]];
|
|
};
|
|
const square: Ring = [
|
|
[0, 0],
|
|
[10, 0],
|
|
[10, 10],
|
|
[0, 10],
|
|
[0, 0],
|
|
];
|
|
const [around, enclave] = simplifySharedBorders(
|
|
[polygon('around', square, circle(true)), polygon('enclave', circle(false))],
|
|
0.05,
|
|
) as Feature[];
|
|
const hole = (around.geometry as { coordinates: Ring[] }).coordinates[1];
|
|
const key = (ring: Ring) => ring.map((p) => p.join(',')).sort();
|
|
expect(key(hole)).toEqual(key(outer(enclave)));
|
|
expect(outer(enclave).length).toBeLessThan(41);
|
|
});
|
|
|
|
it('SIMPLIFY-006: a polygon too small to keep is dropped, and a feature with nothing left gets no geometry', () => {
|
|
const speck: Ring = [
|
|
[0, 0],
|
|
[0.001, 0],
|
|
[0.001, 0.001],
|
|
[0, 0],
|
|
];
|
|
const island = polygon('island', speck);
|
|
const [out] = simplifySharedBorders([island], 0.01) as Feature[];
|
|
expect(out.geometry).toBeNull();
|
|
expect(out.properties).toEqual({ name: 'island' });
|
|
});
|
|
|
|
it('SIMPLIFY-007: a MultiPolygon keeps the parts that survive and becomes a Polygon when one is left', () => {
|
|
const big: Ring = [
|
|
[0, 0],
|
|
[4, 0],
|
|
[4, 4],
|
|
[0, 4],
|
|
[0, 0],
|
|
];
|
|
const speck: Ring = [
|
|
[9, 9],
|
|
[9.001, 9],
|
|
[9.001, 9.001],
|
|
[9, 9],
|
|
];
|
|
const [out] = simplifySharedBorders(
|
|
[{ type: 'Feature', properties: {}, geometry: { type: 'MultiPolygon', coordinates: [[big], [speck]] } }],
|
|
0.01,
|
|
) as Feature[];
|
|
expect(out.geometry).toEqual({ type: 'Polygon', coordinates: [big] });
|
|
});
|
|
});
|