mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
Merge branch 'main' into feat/tools-area-breakdowns
This commit is contained in:
8 files changed
+300
-18
No files matched your search
+21
-4
@@ -323,13 +323,30 @@ export function lineTouchesArea(coords, area) {
|
||||
|
||||
/** Whether a point lies inside a ring of `[lon, lat]` vertices (ray casting). */
|
||||
function ringContains(ring, point) {
|
||||
if (ring.length < 3) return false;
|
||||
// Keep adjacent vertices on the same longitude sheet, as the edge tests do.
|
||||
// Wrapping each vertex around the query point would split a dateline ring
|
||||
// when queried from Greenwich and mistake its complement for its interior.
|
||||
let previous = ring[0][0];
|
||||
let west = previous;
|
||||
let east = previous;
|
||||
const vertices = ring.map(([lon, lat]) => {
|
||||
if (Math.abs(lon - previous) > 180)
|
||||
lon += 360 * Math.round((previous - lon) / 360);
|
||||
previous = lon;
|
||||
west = Math.min(west, lon);
|
||||
east = Math.max(east, lon);
|
||||
return [lon, lat];
|
||||
});
|
||||
const middle = (west + east) / 2;
|
||||
const lon = point.lon + 360 * Math.round((middle - point.lon) / 360);
|
||||
let inside = false;
|
||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i, i += 1) {
|
||||
const [xi, yi] = ring[i];
|
||||
const [xj, yj] = ring[j];
|
||||
for (let i = 0, j = vertices.length - 1; i < vertices.length; j = i, i += 1) {
|
||||
const [xi, yi] = vertices[i];
|
||||
const [xj, yj] = vertices[j];
|
||||
if (
|
||||
yi > point.lat !== yj > point.lat &&
|
||||
point.lon < ((xj - xi) * (point.lat - yi)) / (yj - yi) + xi
|
||||
lon < ((xj - xi) * (point.lat - yi)) / (yj - yi) + xi
|
||||
)
|
||||
inside = !inside;
|
||||
}
|
||||
|
||||
@@ -262,3 +262,78 @@ test('a box radius reaches every point of the box, however large', async () => {
|
||||
const small = await resolveArea({ bbox: [-97.94, 30.1, -97.56, 30.52] });
|
||||
assert.ok(areaRadiusKm(small) < 30);
|
||||
});
|
||||
|
||||
test('polygon containment follows the short edges across the antimeridian', async () => {
|
||||
const ring = [
|
||||
[179, -2],
|
||||
[-179, -2],
|
||||
[-179, 2],
|
||||
[179, 2],
|
||||
];
|
||||
const areas = [
|
||||
[{ bbox: [179.4, -0.1, 179.6, 0.1] }, true],
|
||||
[{ bbox: [-179.6, -0.1, -179.4, 0.1] }, true],
|
||||
[{ bbox: [179.8, -0.1, -179.8, 0.1] }, true],
|
||||
[{ lat: 0, lon: 179.5, radius_km: 10 }, true],
|
||||
[{ lat: 0, lon: -179.5, radius_km: 10 }, true],
|
||||
[{ lat: 0, lon: 180, radius_km: 10 }, true],
|
||||
[{ lat: 0, lon: -180, radius_km: 10 }, true],
|
||||
[{ bbox: [-0.1, -0.1, 0.1, 0.1] }, false],
|
||||
[{ lat: 0, lon: 0, radius_km: 10 }, false],
|
||||
];
|
||||
for (const vertices of [
|
||||
ring,
|
||||
[...ring, [...ring[0]]],
|
||||
[...ring].reverse(),
|
||||
[...ring.slice(2), ...ring.slice(0, 2)],
|
||||
]) {
|
||||
for (const [input, expected] of areas) {
|
||||
assert.equal(
|
||||
polygonsTouchArea([[vertices]], await resolveArea(input)),
|
||||
expected,
|
||||
JSON.stringify({ vertices, input }),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('antimeridian polygons keep holes empty and include their boundaries', async () => {
|
||||
const outer = [
|
||||
[178, -3],
|
||||
[-178, -3],
|
||||
[-178, 3],
|
||||
[178, 3],
|
||||
];
|
||||
const hole = [
|
||||
[179, -1],
|
||||
[-179, -1],
|
||||
[-179, 1],
|
||||
[179, 1],
|
||||
];
|
||||
for (const input of [
|
||||
{ bbox: [179.4, -0.1, 179.6, 0.1] },
|
||||
{ bbox: [-179.6, -0.1, -179.4, 0.1] },
|
||||
{ lat: 0, lon: 180, radius_km: 10 },
|
||||
]) {
|
||||
const area = await resolveArea(input);
|
||||
assert.ok(polygonsTouchArea([[outer]], area));
|
||||
assert.ok(!polygonsTouchArea([[outer, hole]], area));
|
||||
assert.ok(!polygonsTouchArea([[outer, [...hole].reverse()]], area));
|
||||
}
|
||||
const solid = await resolveArea({ bbox: [178.4, -0.1, 178.6, 0.1] });
|
||||
assert.ok(polygonsTouchArea([[outer, hole]], solid));
|
||||
const boundary = await resolveArea({ bbox: [178.9, -0.1, 179.1, 0.1] });
|
||||
assert.ok(polygonsTouchArea([[outer, hole]], boundary));
|
||||
});
|
||||
|
||||
test('polygon containment retains rings spanning more than half the globe', async () => {
|
||||
const longitudes = [-170, -110, -50, 10, 70, 130];
|
||||
const ring = [
|
||||
...longitudes.map((lon) => [lon, -5]),
|
||||
...[...longitudes].reverse().map((lon) => [lon, 5]),
|
||||
];
|
||||
const inside = await resolveArea({ bbox: [99, -1, 101, 1] });
|
||||
const outside = await resolveArea({ bbox: [169, -1, 171, 1] });
|
||||
assert.ok(polygonsTouchArea([[ring]], inside));
|
||||
assert.ok(!polygonsTouchArea([[ring]], outside));
|
||||
});
|
||||
@@ -288,20 +288,25 @@ export const getFirePerimeters = defineTool({
|
||||
},
|
||||
});
|
||||
|
||||
/** The average vertex of a perimeter's outer rings, enough to place it in an area. */
|
||||
/** Average outer-ring vertices, using a circular longitude mean at the dateline. */
|
||||
function polygonCenter(polygons) {
|
||||
let lat = 0;
|
||||
let lon = 0;
|
||||
let sinLon = 0;
|
||||
let cosLon = 0;
|
||||
let count = 0;
|
||||
const toRad = Math.PI / 180;
|
||||
for (const polygon of polygons || []) {
|
||||
for (const [x, y] of polygon?.[0] || []) {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
||||
lon += x;
|
||||
sinLon += Math.sin(x * toRad);
|
||||
cosLon += Math.cos(x * toRad);
|
||||
lat += y;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
return count ? { lat: lat / count, lon: lon / count } : null;
|
||||
return count
|
||||
? { lat: lat / count, lon: Math.atan2(sinLon, cosLon) / toRad }
|
||||
: null;
|
||||
}
|
||||
|
||||
export const getTerrainHeight = defineTool({
|
||||
|
||||
@@ -227,6 +227,97 @@ test('fire perimeters are placed by their outline and sorted by size', async ()
|
||||
});
|
||||
});
|
||||
|
||||
test('fire perimeter queries include dateline interiors and exclude the opposite meridian', async () => {
|
||||
const perimeters = {
|
||||
getSnapshot: async () => [
|
||||
{
|
||||
stableId: 'dateline',
|
||||
name: 'Dateline Fire',
|
||||
acres: 100,
|
||||
polygons: [
|
||||
[
|
||||
[
|
||||
[179, -2],
|
||||
[-179, -2],
|
||||
[-179, 2],
|
||||
[179, 2],
|
||||
[179, -2],
|
||||
],
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const catalog = catalogWith({ perimeters });
|
||||
for (const area of [
|
||||
{ bbox: [179.4, -0.1, 179.6, 0.1] },
|
||||
{ bbox: [-179.6, -0.1, -179.4, 0.1] },
|
||||
{ lat: 0, lon: 180, radius_km: 10 },
|
||||
]) {
|
||||
const result = await catalog.call('get_fire_perimeters', { area });
|
||||
assert.deepEqual(
|
||||
result.data.rows.map((row) => row.id),
|
||||
['dateline'],
|
||||
);
|
||||
}
|
||||
const elsewhere = await catalog.call('get_fire_perimeters', {
|
||||
area: { bbox: [-0.1, -0.1, 0.1, 0.1] },
|
||||
});
|
||||
assert.equal(elsewhere.data.total, 0);
|
||||
});
|
||||
|
||||
test('fire perimeter coordinates stay at the dateline for closed and multipart outlines', async () => {
|
||||
const perimeters = {
|
||||
getSnapshot: async () => [
|
||||
{
|
||||
stableId: 'closed',
|
||||
acres: 100,
|
||||
polygons: [
|
||||
[
|
||||
[
|
||||
[179, -2],
|
||||
[-179, -2],
|
||||
[-179, 2],
|
||||
[179, 2],
|
||||
[179, -2],
|
||||
],
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
stableId: 'multipart',
|
||||
acres: 50,
|
||||
polygons: [
|
||||
[
|
||||
[
|
||||
[179, -1],
|
||||
[179.5, -1],
|
||||
[179.5, 1],
|
||||
[179, 1],
|
||||
],
|
||||
],
|
||||
[
|
||||
[
|
||||
[-179.5, -1],
|
||||
[-179, -1],
|
||||
[-179, 1],
|
||||
[-179.5, 1],
|
||||
],
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const result = await catalogWith({ perimeters }).call('get_fire_perimeters', {
|
||||
area: { bbox: [178, -3, -178, 3] },
|
||||
});
|
||||
assert.equal(result.data.total, 2);
|
||||
for (const row of result.data.rows) {
|
||||
assert.ok(Math.abs(row.lon) > 179, `${row.id}: ${row.lon}`);
|
||||
assert.ok(Math.abs(row.lon) <= 180);
|
||||
}
|
||||
});
|
||||
|
||||
test('terrain heights are returned in request order', async () => {
|
||||
const terrain = {
|
||||
async getHeights(points) {
|
||||
|
||||
Reference in new issue
Block a user