Skip to content

Commit 1c80a33

Browse files
committed
Better Spherical Voronoi Diagram
TODO: Typings for geoSpatialVoronoi. This seemingly simple commit took at least four hours of experimenting to complete. That's why it uses three additional modules. Trust me, this is the easiest way.
1 parent cc5e407 commit 1c80a33

6 files changed

Lines changed: 186 additions & 63 deletions

File tree

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@
2727
"astro": "^5.1.10",
2828
"class-variance-authority": "^0.7.1",
2929
"clsx": "^2.1.1",
30+
"d3-geo": "^3.1.1",
31+
"d3-geo-projection": "^4.0.0",
32+
"d3-geo-voronoi": "^2.1.0",
3033
"leaflet": "^1.9.4",
3134
"leaflet-contextmenu": "^1.4.0",
3235
"leaflet-draw": "^1.0.4",
@@ -47,6 +50,7 @@
4750
},
4851
"devDependencies": {
4952
"@eslint/js": "^9.19.0",
53+
"@types/d3-geo": "^3.1.0",
5054
"@types/geojson": "^7946.0.16",
5155
"@types/leaflet": "^1.9.16",
5256
"@types/lodash": "^4.17.15",

pnpm-lock.yaml

Lines changed: 141 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/maps/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ nw["${tentacleFirstTag[question.locationType]}"="${
103103
question.locationType
104104
}"](around:${turf.convertLength(
105105
question.radius,
106-
question.unit,
106+
question.unit ?? "miles",
107107
"meters",
108108
)}, ${question.lat}, ${question.lng});
109109
out center;

src/maps/tentacles.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { LatLng } from "leaflet";
22
import { findTentacleLocations, iconColors } from "./api";
33
import * as turf from "@turf/turf";
44
import { defaultUnit, questions } from "@/utils/context";
5+
import { geoSpatialVoronoi } from "./voronoi";
56

67
export type TentacleLocations =
78
| "aquarium"
@@ -29,24 +30,6 @@ export interface TentacleQuestion {
2930
locationType: TentacleLocations;
3031
}
3132

32-
const createGeodesicPolygon = (polygon: any, steps = 20) => {
33-
const coordinates = polygon.geometry.coordinates[0];
34-
const geodesicLines = [];
35-
36-
for (let i = 0; i < coordinates.length - 1; i++) {
37-
const start = coordinates[i];
38-
const end = coordinates[i + 1];
39-
40-
const geodesicLine = turf.greatCircle(start, end, { npoints: steps });
41-
geodesicLines.push(...geodesicLine.geometry.coordinates);
42-
}
43-
44-
geodesicLines.push(geodesicLines[0]);
45-
46-
// @ts-expect-error Typing is wrong
47-
return turf.polygon([geodesicLines]);
48-
};
49-
5033
export const adjustPerTentacle = async (
5134
question: TentacleQuestion,
5235
mapData: any,
@@ -61,16 +44,19 @@ export const adjustPerTentacle = async (
6144
}
6245

6346
const points = await findTentacleLocations(question);
64-
const voronoi = turf.voronoi(points);
6547

66-
const correctPolygonPre = voronoi.features.find((feature: any) => {
48+
const voronoi = geoSpatialVoronoi(points);
49+
50+
const correctPolygon = voronoi.features.find((feature: any) => {
6751
if (!question.location) return false;
68-
return feature.properties.name === question.location.properties.name;
52+
return (
53+
feature.properties.site.properties.name ===
54+
question.location.properties.name
55+
);
6956
});
70-
if (!correctPolygonPre) {
57+
if (!correctPolygon) {
7158
return mapData;
7259
}
73-
const correctPolygon = createGeodesicPolygon(correctPolygonPre);
7460

7561
const circle = turf.circle(
7662
turf.point([question.lng, question.lat]),

src/maps/thermometer.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { questions } from "@/utils/context";
22
import { iconColors } from "./api";
33
import * as turf from "@turf/turf";
44
import type { LatLng } from "leaflet";
5+
import { geoSpatialVoronoi } from "./voronoi";
56

67
export interface ThermometerQuestion {
78
distance?: number;
@@ -25,57 +26,25 @@ export const adjustPerThermometer = (
2526
throw new Error("Cannot be masked");
2627
}
2728

28-
// This code is messy but functional
2929
const pointA = turf.point([question.lngA, question.latA]);
3030
const pointB = turf.point([question.lngB, question.latB]);
3131

32-
const bearing = turf.bearing(pointA, pointB);
33-
const midpoint = turf.midpoint(pointA, pointB);
32+
const voronoi = geoSpatialVoronoi(turf.featureCollection([pointA, pointB]));
3433

35-
const coordinates = [];
36-
37-
for (let i = -5000; i <= 5000; i += 10) {
38-
// 5,000 is arbitrary
39-
const destination = turf.destination(midpoint, i, bearing + 90, {
40-
units: "kilometers",
41-
});
42-
coordinates.push(destination.geometry.coordinates);
43-
}
44-
45-
const perpendicular = turf.lineString(coordinates);
46-
47-
const polygon = turf.polygon([
48-
[
49-
...perpendicular.geometry.coordinates,
50-
[180, 90],
51-
[-180, -90],
52-
perpendicular.geometry.coordinates[0],
53-
],
54-
]);
55-
56-
const wouldRemoveA = turf.booleanPointInPolygon(
57-
pointB, // Should be A, but there must be an error in my logic somewhere else so this is fine
58-
polygon,
59-
);
60-
61-
if (
62-
(question.warmer && wouldRemoveA) ||
63-
(!question.warmer && !wouldRemoveA)
64-
) {
65-
polygon.geometry.coordinates[0].pop();
34+
if (question.warmer) {
6635
return turf.intersect(
6736
turf.featureCollection(
6837
mapData.features.length > 1
69-
? [turf.union(mapData)!, polygon]
70-
: [...mapData.features, polygon],
38+
? [turf.union(mapData)!, voronoi.features[1]]
39+
: [...mapData.features, voronoi.features[1]],
7140
),
7241
);
7342
} else {
74-
return turf.difference(
43+
return turf.intersect(
7544
turf.featureCollection(
7645
mapData.features.length > 1
77-
? [turf.union(mapData)!, polygon]
78-
: [...mapData.features, polygon],
46+
? [turf.union(mapData)!, voronoi.features[0]]
47+
: [...mapData.features, voronoi.features[0]],
7948
),
8049
);
8150
}

0 commit comments

Comments
 (0)