mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-04-25 17:14:57 +02:00
fix(globe): render conflict zone polygons correctly on 3D globe (#994)
* fix(globe): add polygonGeoJsonGeometry accessor for polygon rendering globe.gl's default polygonGeoJsonGeometry accessor looks for d.geometry, but our GlobePolygon objects store coordinates in d.coords. Without this explicit accessor, all polygon data (conflict zones, boundaries, CII choropleth) was silently ignored — polygonsData was set but nothing rendered. * fix(globe): render conflict zone polygons correctly on 3D globe Root cause: globe.gl's ConicPolygonGeometry + earcut renders CCW exterior rings as their complement on the sphere (filling everything EXCEPT the polygon). The simplified CONFLICT_ZONES coords also lacked enough vertices for proper spherical tessellation. Fix: - Use real GeoJSON country geometries from countriesGeoData instead of simplified CONFLICT_ZONES.coords (maps zone IDs to ISO-2 codes) - Reverse winding order (CCW → CW) on all polygon rings so earcut fills the polygon interior, not the complement - Apply same winding fix to GEOPOLITICAL_BOUNDARIES polygons - Zones without country mapping (Strait of Hormuz, South Lebanon, Red Sea) are represented by center markers only Tested: Iran, Ukraine, Gaza/Israel, Sudan, Myanmar all render as localized red polygon overlays on the globe without envelope artifacts. * fix(globe): guard against null geometry in GeoJSON features Some GeoJSON features have null geometry which caused TypeError in flushPolygons. Add null guards for both conflict zone and CII choropleth polygon rendering paths.
This commit is contained in:
@@ -1217,24 +1217,49 @@ export class GlobeMap {
|
||||
|
||||
if (this.layers.geopoliticalBoundaries) {
|
||||
for (const b of GEOPOLITICAL_BOUNDARIES) {
|
||||
polys.push({ coords: [b.coords], name: b.name, _kind: 'boundary', boundaryType: b.boundaryType });
|
||||
// Reverse winding for globe.gl: CCW → CW to render polygon interior, not complement
|
||||
const reversed = b.coords.slice().reverse();
|
||||
polys.push({ coords: [reversed], name: b.name, _kind: 'boundary', boundaryType: b.boundaryType });
|
||||
}
|
||||
}
|
||||
|
||||
if (this.layers.conflicts) {
|
||||
// Map conflict zone IDs to ISO-2 country codes for real GeoJSON geometry lookup.
|
||||
// Using actual country geometries from countriesGeoData ensures correct rendering
|
||||
// (same approach as CII choropleth which renders correctly).
|
||||
const CONFLICT_ISO: Record<string, string[]> = {
|
||||
iran: ['IR'],
|
||||
ukraine: ['UA'],
|
||||
gaza: ['PS', 'IL'],
|
||||
sudan: ['SD'],
|
||||
myanmar: ['MM'],
|
||||
};
|
||||
for (const z of CONFLICT_ZONES) {
|
||||
const ring: number[][] = z.coords.map(c => [c[0], c[1]]);
|
||||
if (ring.length < 3) continue;
|
||||
const first = ring[0]!, last = ring[ring.length - 1]!;
|
||||
if (first[0] !== last[0] || first[1] !== last[1]) ring.push(first.slice());
|
||||
polys.push({
|
||||
coords: [ring],
|
||||
name: z.name,
|
||||
_kind: 'conflict',
|
||||
intensity: z.intensity ?? 'low',
|
||||
parties: z.parties,
|
||||
casualties: z.casualties,
|
||||
});
|
||||
const isoCodes = CONFLICT_ISO[z.id];
|
||||
if (isoCodes && this.countriesGeoData) {
|
||||
for (const feat of this.countriesGeoData.features) {
|
||||
const code = feat.properties?.['ISO3166-1-Alpha-2'] as string | undefined;
|
||||
if (!code || !isoCodes.includes(code)) continue;
|
||||
const geom = feat.geometry;
|
||||
if (!geom) continue;
|
||||
const rings = geom.type === 'Polygon' ? [geom.coordinates] : geom.type === 'MultiPolygon' ? geom.coordinates : [];
|
||||
for (const ring of rings) {
|
||||
const reversed = ring.map((r: number[][]) => [...r].reverse());
|
||||
polys.push({
|
||||
coords: reversed,
|
||||
name: z.name,
|
||||
_kind: 'conflict',
|
||||
intensity: z.intensity ?? 'low',
|
||||
parties: z.parties,
|
||||
casualties: z.casualties,
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Zones without country mapping (Strait of Hormuz, South Lebanon, etc.)
|
||||
// are represented by center markers only — custom simplified polygons
|
||||
// don't render correctly on globe.gl's spherical tessellation.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,6 +1269,7 @@ export class GlobeMap {
|
||||
const entry = code ? this.ciiScoresMap.get(code) : undefined;
|
||||
if (!entry || !code) continue;
|
||||
const geom = feat.geometry;
|
||||
if (!geom) continue;
|
||||
const rings = geom.type === 'Polygon' ? [geom.coordinates] : geom.type === 'MultiPolygon' ? geom.coordinates : [];
|
||||
const name = (feat.properties?.name as string) ?? code;
|
||||
for (const ring of rings) {
|
||||
@@ -1259,6 +1285,7 @@ export class GlobeMap {
|
||||
const conflictAlt: Record<string, number> = { high: 0.006, medium: 0.004, low: 0.003 };
|
||||
(this.globe as any)
|
||||
.polygonsData(polys)
|
||||
.polygonGeoJsonGeometry((d: GlobePolygon) => ({ type: 'Polygon', coordinates: d.coords }))
|
||||
.polygonCapColor((d: GlobePolygon) => {
|
||||
if (d._kind === 'cii') return colors[d.level!] ?? 'rgba(0,0,0,0)';
|
||||
if (d._kind === 'conflict') return conflictCap[d.intensity!] ?? conflictCap.low;
|
||||
|
||||
Reference in New Issue
Block a user