Skip to content

Commit c2a82a3

Browse files
committed
fix(map): Normalize winding order to CCW & replace d3-geo with turf LINDASSUP-247
- Previously the received winding order of polygons has not been modified. This caused issues with helpers relying on a specific winding order. We now "rewind" all polygons to CCW ("right hand rule"), right after parsing, as is the standard with GeoJSON. - The d3-geo helpers rely on a CW winding order, which would require a rewinding before each call. Therefore d3-geo is now replaced with equivalent helpers from turf, that use the CCW winding order.
1 parent 09d3383 commit c2a82a3

6 files changed

Lines changed: 546 additions & 49 deletions

File tree

app/charts/map/helpers.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { WebMercatorViewport } from "@deck.gl/core";
22
import { MapboxOverlay, MapboxOverlayProps } from "@deck.gl/mapbox";
3+
import turfBbox from "@turf/bbox";
34
import { extent } from "d3-array";
4-
import { geoBounds } from "d3-geo";
55
import { useEffect, useMemo, useState } from "react";
66
import { useControl, ViewState } from "react-map-gl";
77
import { feature } from "topojson-client";
@@ -137,9 +137,12 @@ export const getBBox = (
137137
let symbolsBbox: BBox | undefined;
138138

139139
if (shapes) {
140-
const _shapesBbox = geoBounds(shapes);
141-
if (!_shapesBbox.flat().some(isNaN)) {
142-
shapesBbox = _shapesBbox;
140+
const [minLng, minLat, maxLng, maxLat] = turfBbox(shapes);
141+
if (![minLng, minLat, maxLng, maxLat].some(isNaN)) {
142+
shapesBbox = [
143+
[minLng, minLat],
144+
[maxLng, maxLat],
145+
] as BBox;
143146
}
144147
}
145148

app/charts/map/map-state-props.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { geoCentroid } from "d3-geo";
1+
import turfCentroid from "@turf/centroid";
22
import keyBy from "lodash/keyBy";
33
import { useMemo } from "react";
44

@@ -158,7 +158,7 @@ export const useMapStateData = (
158158

159159
const points = features.map((d) => ({
160160
...d,
161-
coordinates: geoCentroid(d),
161+
coordinates: turfCentroid(d).geometry.coordinates,
162162
}));
163163

164164
return {

app/charts/map/map.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
import { supported } from "@mapbox/mapbox-gl-supported";
77
import { Button, Theme } from "@mui/material";
88
import { makeStyles } from "@mui/styles";
9-
import { geoArea } from "d3-geo";
9+
import turfArea from "@turf/area";
1010
import debounce from "lodash/debounce";
1111
import orderBy from "lodash/orderBy";
1212
import uniq from "lodash/uniq";
@@ -302,7 +302,7 @@ export const MapComponent = ({
302302
// Sort for smaller shapes to be over larger ones, to be able to use tooltip
303303
const sortedFeatures = orderBy(
304304
features.areaLayer?.shapes?.features,
305-
geoArea,
305+
turfArea,
306306
"desc"
307307
) satisfies GeoFeature[];
308308

app/graphql/resolvers/rdf.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { rewindGeometry } from "@placemarkio/geojson-rewind";
12
import { ascending, descending } from "d3-array";
23
import DataLoader from "dataloader";
34
import groupBy from "lodash/groupBy";
45
import ParsingClient from "sparql-http-client/ParsingClient";
56
import { topology } from "topojson-server";
67
import { LRUCache } from "typescript-lru-cache";
7-
import { parse as parseWKT } from "wellknown";
8+
import { GeoJSONGeometry, parse as parseWKT } from "wellknown";
89

910
import { Filters } from "@/config-types";
1011
import {
@@ -127,6 +128,16 @@ export const searchCubes: NonNullable<QueryResolvers["searchCubes"]> = async (
127128
});
128129
};
129130

131+
/**
132+
* Normalize the geometry's winding order to follow GeoJSON standards. It is
133+
* converted to CCW (a.k.a. "right hand rule") if it has a non-standard CW
134+
* winding order.
135+
*/
136+
const rewind = (geometry: GeoJSONGeometry): GeoJSONGeometry => {
137+
if (!geometry) return null;
138+
return rewindGeometry(geometry) as GeoJSONGeometry;
139+
};
140+
130141
export const dataCubeDimensionGeoShapes: NonNullable<
131142
QueryResolvers["dataCubeDimensionGeoShapes"]
132143
> = async (_, { locale, cubeFilter }, { setup }, info) => {
@@ -178,7 +189,7 @@ export const dataCubeDimensionGeoShapes: NonNullable<
178189
iri: value,
179190
label: dimensionValuesByValue.get(value),
180191
},
181-
geometry: parseWKT(shape.wktString),
192+
geometry: rewind(parseWKT(shape.wktString)),
182193
};
183194
});
184195

app/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"@opentelemetry/sdk-node": "^0.211.0",
5454
"@opentelemetry/sdk-trace-node": "^2.5.0",
5555
"@opentelemetry/semantic-conventions": "^1.39.0",
56+
"@placemarkio/geojson-rewind": "^1.0.3",
5657
"@preconstruct/next": "^3.0.1",
5758
"@prisma/client": "^4.10.1",
5859
"@rdfjs/data-model": "^2.0.2",
@@ -62,6 +63,9 @@
6263
"@testing-library/react-hooks": "^8.0.1",
6364
"@tpluscode/rdf-ns-builders": "2.0.1",
6465
"@tpluscode/sparql-builder": "^0.3.31",
66+
"@turf/area": "7.3.4",
67+
"@turf/bbox": "7.3.4",
68+
"@turf/centroid": "7.3.4",
6569
"@types/react-grid-layout": "^1.3.5",
6670
"@uiw/react-color": "^2.3.2",
6771
"@urql/devtools": "^2.0.3",
@@ -83,7 +87,6 @@
8387
"d3-delaunay": "^6.0.4",
8488
"d3-dsv": "^3.0.1",
8589
"d3-format": "^3.1.0",
86-
"d3-geo": "^3.1.1",
8790
"d3-interpolate": "^3.0.1",
8891
"d3-interpolate-path": "^2.3.0",
8992
"d3-scale": "^4.0.2",
@@ -204,7 +207,6 @@
204207
"@types/d3-delaunay": "^6.0.4",
205208
"@types/d3-dsv": "^3.0.7",
206209
"@types/d3-format": "^3.0.4",
207-
"@types/d3-geo": "^3.1.0",
208210
"@types/d3-interpolate": "^3.0.4",
209211
"@types/d3-interpolate-path": "^2.0.0",
210212
"@types/d3-scale": "^4.0.8",

0 commit comments

Comments
 (0)