-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
163 additions
and
3 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,159 @@ | ||
/** | ||
* The code in this file rewinds GeoJSON polygons and multipolygons to fit d3's expectations. | ||
* This is adapted by Matthieu Viry from a notebook of Philippe Rivière: https://observablehq.com/@fil/rewind | ||
* which is licensed under the ISC license. | ||
*/ | ||
|
||
import d3 from './d3-custom'; | ||
import { GeoJSONFeature, GeoJSONFeatureCollection } from '../global'; | ||
|
||
const { | ||
geoTransform, | ||
geoStream, | ||
geoContains, | ||
geoArea, | ||
} = d3; | ||
|
||
function projectPolygons(o, stream) { | ||
let coordinates = []; | ||
let polygon; let | ||
line; | ||
geoStream( | ||
o, | ||
stream({ | ||
polygonStart() { | ||
coordinates.push((polygon = [])); | ||
}, | ||
polygonEnd() {}, | ||
lineStart() { | ||
polygon.push((line = [])); | ||
}, | ||
lineEnd() { | ||
line.push(line[0].slice()); | ||
}, | ||
point(x, y) { | ||
line.push([x, y]); | ||
}, | ||
}), | ||
); | ||
if (o.type === 'Polygon') { | ||
// eslint-disable-next-line prefer-destructuring | ||
coordinates = coordinates[0]; | ||
} | ||
return { ...o, coordinates, rewind: true }; | ||
} | ||
|
||
function projectGeometry(o, stream) { | ||
// eslint-disable-next-line no-nested-ternary | ||
return !o | ||
? null | ||
: o.type === 'GeometryCollection' // eslint-disable-line no-nested-ternary | ||
// eslint-disable-next-line @typescript-eslint/no-use-before-define | ||
? projectGeometryCollection(o, stream) | ||
: o.type === 'Polygon' || o.type === 'MultiPolygon' | ||
? projectPolygons(o, stream) | ||
: o; | ||
} | ||
|
||
function projectFeature(o, stream) { | ||
return { ...o, geometry: projectGeometry(o.geometry, stream) }; | ||
} | ||
|
||
function projectFeatureCollection(o, stream) { | ||
return { ...o, features: o.features.map((f) => projectFeature(f, stream)) }; | ||
} | ||
|
||
function projectGeometryCollection(obj, stream) { | ||
return { | ||
...obj, | ||
geometries: obj.geometries.map((o) => projectGeometry(o, stream)), | ||
}; | ||
} | ||
|
||
const geoProjectSimple = function (object, projection) { | ||
const { stream } = projection; | ||
let project; | ||
if (!stream) throw new Error('invalid projection'); | ||
switch (object && object.type) { | ||
case 'Feature': | ||
project = projectFeature; | ||
break; | ||
case 'FeatureCollection': | ||
project = projectFeatureCollection; | ||
break; | ||
default: | ||
project = projectGeometry; | ||
break; | ||
} | ||
return project(object, stream); | ||
}; | ||
|
||
function geoRewindStream(simple = true) { | ||
let ring; | ||
let polygon; | ||
return geoTransform({ | ||
polygonStart() { | ||
this.stream.polygonStart(); | ||
polygon = []; | ||
}, | ||
lineStart() { | ||
if (polygon) polygon.push((ring = [])); | ||
else this.stream.lineStart(); | ||
}, | ||
lineEnd() { | ||
if (!polygon) this.stream.lineEnd(); | ||
}, | ||
point(x, y) { | ||
if (polygon) ring.push([x, y]); | ||
else this.stream.point(x, y); | ||
}, | ||
polygonEnd() { | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const [i, rring] of polygon.entries()) { | ||
rring.push(rring[0].slice()); | ||
if ( | ||
i // eslint-disable-line no-nested-ternary | ||
// a hole must contain the first point of the polygon | ||
? !geoContains( | ||
{ type: 'Polygon', coordinates: [rring] }, | ||
polygon[0][0], | ||
) | ||
: polygon[1] | ||
// the outer ring must contain the first point of its first hole (if any) | ||
? !geoContains( | ||
{ type: 'Polygon', coordinates: [rring] }, | ||
polygon[1][0], | ||
// eslint-disable-next-line @typescript-eslint/no-loop-func | ||
) && !rring.some((p) => p[0] === polygon[1][0][0] && p[1] === polygon[1][0][1]) | ||
// a single ring polygon must be smaller than a hemisphere (optional) | ||
: simple && geoArea({ type: 'Polygon', coordinates: [rring] }) > 2 * Math.PI | ||
) { | ||
rring.reverse(); | ||
} | ||
|
||
this.stream.lineStart(); | ||
rring.pop(); | ||
// eslint-disable-next-line no-restricted-syntax | ||
for (const [x, y] of rring) this.stream.point(x, y); | ||
this.stream.lineEnd(); | ||
} | ||
this.stream.polygonEnd(); | ||
polygon = null; | ||
}, | ||
}); | ||
} | ||
|
||
const rewindFeature = ( | ||
feature: GeoJSONFeature, | ||
simple: boolean, | ||
) => geoProjectSimple(feature, geoRewindStream(simple)); | ||
|
||
const rewindLayer = ( | ||
layer: GeoJSONFeatureCollection, | ||
simple: boolean = true, | ||
): GeoJSONFeatureCollection => { | ||
const features = layer.features.map((feature) => rewindFeature(feature, simple)); | ||
return { ...layer, features }; | ||
}; | ||
|
||
export default rewindLayer; |