mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix: find fire perimeters that overlap an area
Fire perimeters were placed at their average vertex, so a fire enclosing the whole requested area was missed. Polygons now count when a vertex is inside the area, an edge crosses it, or the polygon contains it outside any hole.
This commit is contained in:
1 parent
4af15aa6a9
commit
dd8ba562d2
4 files changed
+88
-1
No files matched your search
@@ -296,3 +296,38 @@ export function lineTouchesArea(coords, area) {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether a point lies inside a ring of `[lon, lat]` vertices (ray casting). */
|
||||||
|
function ringContains(ring, point) {
|
||||||
|
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];
|
||||||
|
if (
|
||||||
|
yi > point.lat !== yj > point.lat &&
|
||||||
|
point.lon < ((xj - xi) * (point.lat - yi)) / (yj - yi) + xi
|
||||||
|
)
|
||||||
|
inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether polygons (each a list of `[lon, lat]` rings, outer ring first)
|
||||||
|
* overlap an area: a vertex inside it, an edge crossing it, or the area's
|
||||||
|
* center inside a polygon and outside its holes.
|
||||||
|
*/
|
||||||
|
export function polygonsTouchArea(polygons, area) {
|
||||||
|
const center = areaCenter(area);
|
||||||
|
return (polygons || []).some((rings) => {
|
||||||
|
if (!Array.isArray(rings) || !rings.length) return false;
|
||||||
|
const closed = rings.map((ring) =>
|
||||||
|
ring.length && ring[0] !== ring.at(-1) ? [...ring, ring[0]] : ring,
|
||||||
|
);
|
||||||
|
if (closed.some((ring) => lineTouchesArea(ring, area))) return true;
|
||||||
|
return (
|
||||||
|
ringContains(rings[0], center) &&
|
||||||
|
!rings.slice(1).some((hole) => ringContains(hole, center))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
areaContains,
|
areaContains,
|
||||||
distanceKm,
|
distanceKm,
|
||||||
lineTouchesArea,
|
lineTouchesArea,
|
||||||
|
polygonsTouchArea,
|
||||||
resolveArea,
|
resolveArea,
|
||||||
} from './area.js';
|
} from './area.js';
|
||||||
import { placeFromGeocodeResult } from './places.js';
|
import { placeFromGeocodeResult } from './places.js';
|
||||||
@@ -224,3 +225,23 @@ test('lines touch an area when a segment crosses it with no vertex inside', asyn
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('polygons touch an area by vertex, crossing edge or enclosing it', async () => {
|
||||||
|
const box = await resolveArea({ bbox: [-1, -1, 1, 1] });
|
||||||
|
const square = (half, x = 0) => [
|
||||||
|
[
|
||||||
|
[x - half, -half],
|
||||||
|
[x + half, -half],
|
||||||
|
[x + half, half],
|
||||||
|
[x - half, half],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
assert.ok(polygonsTouchArea([square(5)], box), 'encloses the area');
|
||||||
|
assert.ok(polygonsTouchArea([square(0.5)], box), 'inside the area');
|
||||||
|
assert.ok(polygonsTouchArea([square(1, 1.5)], box), 'overlaps an edge');
|
||||||
|
assert.ok(!polygonsTouchArea([square(0.5, 5)], box), 'elsewhere');
|
||||||
|
const ring = [...square(5), ...square(3)];
|
||||||
|
assert.ok(!polygonsTouchArea([ring], box), 'the area sits in a hole');
|
||||||
|
const circle = await resolveArea({ lat: 0, lon: 0, radius_km: 20 });
|
||||||
|
assert.ok(polygonsTouchArea([square(5)], circle));
|
||||||
|
});
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
areaCenter,
|
areaCenter,
|
||||||
areaContains,
|
areaContains,
|
||||||
distanceKm,
|
distanceKm,
|
||||||
|
polygonsTouchArea,
|
||||||
resolveArea,
|
resolveArea,
|
||||||
resolvePoint,
|
resolvePoint,
|
||||||
} from '../area.js';
|
} from '../area.js';
|
||||||
@@ -237,8 +238,9 @@ export const getFirePerimeters = defineTool({
|
|||||||
async run(args, { services, signal }) {
|
async run(args, { services, signal }) {
|
||||||
const area = await resolveArea(args.area, { services, signal });
|
const area = await resolveArea(args.area, { services, signal });
|
||||||
const rows = (await services.perimeters.getSnapshot({ signal }))
|
const rows = (await services.perimeters.getSnapshot({ signal }))
|
||||||
|
.filter((fire) => polygonsTouchArea(fire.polygons, area))
|
||||||
.map((fire) => ({ fire, point: polygonCenter(fire.polygons) }))
|
.map((fire) => ({ fire, point: polygonCenter(fire.polygons) }))
|
||||||
.filter(({ point }) => point && areaContains(area, point))
|
.filter(({ point }) => point)
|
||||||
.map(({ fire, point }) => ({
|
.map(({ fire, point }) => ({
|
||||||
id: fire.stableId,
|
id: fire.stableId,
|
||||||
name: fire.name,
|
name: fire.name,
|
||||||
|
|||||||
@@ -665,3 +665,32 @@ test('brief sections use a named place at its own point', async () => {
|
|||||||
assert.deepEqual(queries[0], { latitude: 35.6769, longitude: 139.7639 });
|
assert.deepEqual(queries[0], { latitude: 35.6769, longitude: 139.7639 });
|
||||||
assert.match(result.data.sections.aircraft.summary, /in Tokyo, Japan\.$/);
|
assert.match(result.data.sections.aircraft.summary, /in Tokyo, Japan\.$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a fire perimeter enclosing the whole area is found', async () => {
|
||||||
|
const perimeters = {
|
||||||
|
getSnapshot: async () => [
|
||||||
|
{
|
||||||
|
stableId: 'big',
|
||||||
|
name: 'Big Fire',
|
||||||
|
acres: 250000,
|
||||||
|
polygons: [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[-122, 38],
|
||||||
|
[-120, 38],
|
||||||
|
[-120, 40],
|
||||||
|
[-122, 40],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = await catalogWith({ perimeters }).call('get_fire_perimeters', {
|
||||||
|
area: { bbox: [-121.2, 38.9, -120.8, 39.1] },
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
result.data.rows.map((row) => row.id),
|
||||||
|
['big'],
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user