Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions api/gmaps-kml.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/**
* CORS proxy for Google My Maps KML exports.
*
* Usage: GET /api/gmaps-kml?url=<encoded-kml-url>
*
* Google My Maps KML exports are blocked by CORS when fetched from a browser
* directly. This Edge Function proxies the request server-side and returns the
* KML with permissive CORS headers so the client can parse it.
*
* Only Google Maps KML export URLs are accepted (allowlisted hostname) to
* prevent this endpoint from being used as an open proxy.
*/

export const config = { runtime: 'edge' };

const ALLOWED_HOSTNAMES = new Set(['www.google.com', 'maps.google.com']);

export default async function handler(req) {
const { searchParams } = new URL(req.url);
const kmlUrl = searchParams.get('url');

if (!kmlUrl) {
return new Response('Missing ?url= parameter', { status: 400 });
}

let parsed;
try {
parsed = new URL(kmlUrl);
} catch {
return new Response('Invalid URL', { status: 400 });
}

if (!ALLOWED_HOSTNAMES.has(parsed.hostname)) {
return new Response('URL not allowed', { status: 403 });
}

const upstream = await fetch(kmlUrl, {
headers: { 'User-Agent': 'WorldMonitor/1.0' },
});

if (!upstream.ok) {
return new Response('Upstream fetch failed', { status: upstream.status });
}

const kmlText = await upstream.text();

return new Response(kmlText, {
status: 200,
headers: {
'Content-Type': 'application/vnd.google-earth.kml+xml; charset=utf-8',
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=300, s-maxage=600',
},
});
}
1 change: 1 addition & 0 deletions docs/data-sources.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ description: "Comprehensive documentation of all data sources, feed tiers, and c
- GPS/GNSS jamming zones from ADS-B transponder analysis (H3 hex grid, interference % classification)
- Geopolitical boundary overlays — Korean DMZ (43-point closed-ring polygon based on the Korean Armistice Agreement), with typed boundary categories (demilitarized, ceasefire, disputed, armistice) and info popups
- Iran conflict events — geocoded attacks, strikes, and military incidents sourced from LiveUAMap with severity classification
- **Conflict zone KML overlays** — on-demand overlay system sourced from Google My Maps KML exports. Selected via the ⚔ Conflicts dropdown in the header. KML is fetched, parsed client-side with `DOMParser` (no extra dependency), and rendered as native WebGL layers (lines for front lines, polygons for control zones) using the source map's own colors. Proxied through `/api/gmaps-kml` when direct fetch is CORS-blocked. To add a new conflict scene, append a `ConflictSceneConfig` entry to `src/config/conflicts.ts`.
- Weather alerts and severe conditions

</details>
Expand Down
6 changes: 6 additions & 0 deletions src/app/event-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,12 @@ export class EventHandlerManager implements AppModule {
trackMapViewChange(regionSelect.value);
});

// Fly to conflict area and load KML overlay when user picks from the conflicts dropdown.
const conflictSelect = document.getElementById('conflictSelect') as HTMLSelectElement | null;
conflictSelect?.addEventListener('change', () => {
this.ctx.map?.activateConflictScene(conflictSelect.value || null);
});

this.boundResizeHandler = debounce(() => {
this.ctx.map?.setIsResizing(false);
this.ctx.map?.render();
Expand Down
8 changes: 8 additions & 0 deletions src/app/panel-layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
VARIANT_DEFAULTS,
} from '@/config';
import { BETA_MODE } from '@/config/beta';
import { CONFLICT_SCENES } from '@/config/conflicts';
import { t } from '@/services/i18n';
import { getCurrentTheme } from '@/utils';
import { trackCriticalBannerAction } from '@/services/analytics';
Expand Down Expand Up @@ -308,6 +309,13 @@ export class PanelLayoutManager implements AppModule {
<option value="oceania">${t('components.deckgl.views.oceania')}</option>
</select>
</div>
${CONFLICT_SCENES.length > 0 ? `
<div class="conflict-selector">
<select id="conflictSelect" class="region-select conflict-select" title="Jump to conflict zone">
<option value="">⚔ Conflicts\u2026</option>
${CONFLICT_SCENES.map(scene => `<option value="${scene.id}">${scene.label}</option>`).join('')}
</select>
</div>` : ''}
<button class="mobile-search-btn" id="mobileSearchBtn" aria-label="${t('header.search')}">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
</button>
Expand Down
Loading
Loading