import { IconLayer, TextLayer, PathLayer } from '@deck.gl/layers';
import type { PickingInfo, Layer } from '@deck.gl/core';
import { svgToDataUri } from '../../utils/svgToDataUri';
import { MILITARY_BASES } from '../../data/militaryBases';
import type { MilitaryBase } from '../../data/militaryBases';
import { GOV_BUILDINGS } from '../../data/govBuildings';
import type { GovBuilding } from '../../data/govBuildings';
import { NK_LAUNCH_SITES, NK_LAUNCH_TYPE_META } from '../../data/nkLaunchSites';
import type { NKLaunchSite } from '../../data/nkLaunchSites';
import { NK_MISSILE_EVENTS } from '../../data/nkMissileEvents';
import type { NKMissileEvent } from '../../data/nkMissileEvents';
import { hexToRgb } from './types';
import type { LayerFactoryConfig } from './types';
// ─── NKMissile SVG ────────────────────────────────────────────────────────────
function getMissileColor(type: string): string {
if (type.includes('ICBM')) return '#dc2626';
if (type.includes('IRBM')) return '#ef4444';
if (type.includes('SLBM')) return '#3b82f6';
return '#f97316';
}
function missileLaunchSvg(color: string): string {
return ``;
}
function missileImpactSvg(color: string): string {
return ``;
}
// ─── MilitaryBase SVG ─────────────────────────────────────────────────────────
function navalBaseSvg(color: string, size: number): string {
return ``;
}
function airforceBaseSvg(color: string, size: number): string {
return ``;
}
function armyBaseSvg(color: string, size: number): string {
return ``;
}
function missileBaseSvg(color: string, size: number): string {
return ``;
}
function jointBaseSvg(color: string, size: number): string {
return ``;
}
// ─── GovBuilding SVG ──────────────────────────────────────────────────────────
function governmentBuildingSvg(color: string, size: number): string {
return ``;
}
function militaryHqSvg(color: string, size: number): string {
return ``;
}
function intelligenceSvg(color: string, size: number): string {
return ``;
}
function foreignSvg(color: string, size: number): string {
return ``;
}
function maritimeSvg(color: string, size: number): string {
return ``;
}
function defenseSvg(color: string, size: number): string {
return ``;
}
// ─── NK Launch Site SVG ───────────────────────────────────────────────────────
function icbmSiteSvg(color: string, size: number): string {
return ``;
}
function srbmSiteSvg(color: string, size: number): string {
return ``;
}
function slbmSiteSvg(color: string, size: number): string {
return ``;
}
function cruiseSiteSvg(color: string, size: number): string {
return ``;
}
function artillerySiteSvg(color: string, size: number): string {
return ``;
}
// ─── Module-level icon caches ─────────────────────────────────────────────────
const launchIconCache = new Map();
const impactIconCache = new Map();
const milBaseIconCache = new Map();
const govBuildingIconCache = new Map();
const nkLaunchIconCache = new Map();
// ─── MilitaryBase icon helpers ────────────────────────────────────────────────
const MIL_BASE_TYPE_COLOR: Record = {
naval: '#3b82f6', airforce: '#f59e0b', army: '#22c55e',
missile: '#ef4444', joint: '#a78bfa',
};
type MilBaseSvgFn = (color: string, size: number) => string;
const MIL_BASE_SVG_FN: Record = {
naval: navalBaseSvg,
airforce: airforceBaseSvg,
army: armyBaseSvg,
missile: missileBaseSvg,
joint: jointBaseSvg,
};
function getMilBaseIconUrl(type: string): string {
if (!milBaseIconCache.has(type)) {
const color = MIL_BASE_TYPE_COLOR[type] ?? '#a78bfa';
const fn = MIL_BASE_SVG_FN[type] ?? jointBaseSvg;
milBaseIconCache.set(type, svgToDataUri(fn(color, 64)));
}
return milBaseIconCache.get(type)!;
}
// ─── GovBuilding icon helpers ─────────────────────────────────────────────────
const GOV_TYPE_COLOR: Record = {
executive: '#f59e0b', legislature: '#a78bfa', military_hq: '#ef4444',
intelligence: '#6366f1', foreign: '#3b82f6', maritime: '#06b6d4', defense: '#dc2626',
};
type GovSvgFn = (color: string, size: number) => string;
const GOV_SVG_FN: Record = {
executive: governmentBuildingSvg,
legislature: governmentBuildingSvg,
military_hq: militaryHqSvg,
intelligence: intelligenceSvg,
foreign: foreignSvg,
maritime: maritimeSvg,
defense: defenseSvg,
};
function getGovBuildingIconUrl(type: string): string {
if (!govBuildingIconCache.has(type)) {
const color = GOV_TYPE_COLOR[type] ?? '#f59e0b';
const fn = GOV_SVG_FN[type] ?? governmentBuildingSvg;
govBuildingIconCache.set(type, svgToDataUri(fn(color, 64)));
}
return govBuildingIconCache.get(type)!;
}
// ─── NKLaunchSite icon helpers ────────────────────────────────────────────────
type NkLaunchSvgFn = (color: string, size: number) => string;
const NK_LAUNCH_SVG_FN: Record = {
icbm: icbmSiteSvg,
irbm: icbmSiteSvg,
srbm: srbmSiteSvg,
slbm: slbmSiteSvg,
cruise: cruiseSiteSvg,
artillery: artillerySiteSvg,
mlrs: artillerySiteSvg,
};
function getNkLaunchIconUrl(type: string): string {
if (!nkLaunchIconCache.has(type)) {
const color = NK_LAUNCH_TYPE_META[type]?.color ?? '#f97316';
const fn = NK_LAUNCH_SVG_FN[type] ?? srbmSiteSvg;
nkLaunchIconCache.set(type, svgToDataUri(fn(color, 64)));
}
return nkLaunchIconCache.get(type)!;
}
export function createMilitaryLayers(
config: { militaryBases: boolean; govBuildings: boolean; nkLaunch: boolean; nkMissile: boolean },
fc: LayerFactoryConfig,
): Layer[] {
const layers: Layer[] = [];
const sc = fc.sc;
const onPick = fc.onPick;
// ── Military Bases — IconLayer ────────────────────────────────────────
if (config.militaryBases) {
layers.push(
new IconLayer({
id: 'static-militarybase-icon',
data: MILITARY_BASES,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getMilBaseIconUrl(d.type), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
getSize: 16 * sc,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo) => {
if (info.object) onPick({ kind: 'militaryBase', object: info.object });
return true;
},
}),
new TextLayer({
id: 'static-militarybase-label',
data: MILITARY_BASES,
getPosition: (d) => [d.lng, d.lat],
getText: (d) => (d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo),
getSize: 11 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => [...hexToRgb(MIL_BASE_TYPE_COLOR[d.type] ?? '#a78bfa'), 255] as [number, number, number, number],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 9],
fontFamily: 'monospace',
fontWeight: 700,
fontSettings: { sdf: true },
outlineWidth: 8,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
// ── Gov Buildings — IconLayer ─────────────────────────────────────────
if (config.govBuildings) {
layers.push(
new IconLayer({
id: 'static-govbuilding-icon',
data: GOV_BUILDINGS,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getGovBuildingIconUrl(d.type), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
getSize: 12 * sc,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo) => {
if (info.object) onPick({ kind: 'govBuilding', object: info.object });
return true;
},
}),
new TextLayer({
id: 'static-govbuilding-label',
data: GOV_BUILDINGS,
getPosition: (d) => [d.lng, d.lat],
getText: (d) => (d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo),
getSize: 11 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => [...hexToRgb(GOV_TYPE_COLOR[d.type] ?? '#f59e0b'), 255] as [number, number, number, number],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 8],
fontFamily: 'monospace',
fontWeight: 700,
fontSettings: { sdf: true },
outlineWidth: 8,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
// ── NK Launch Sites — IconLayer ───────────────────────────────────────
if (config.nkLaunch) {
layers.push(
new IconLayer({
id: 'static-nklaunch-icon',
data: NK_LAUNCH_SITES,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getNkLaunchIconUrl(d.type), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
getSize: (d) => (d.type === 'artillery' || d.type === 'mlrs' ? 12 : 15) * sc,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo) => {
if (info.object) onPick({ kind: 'nkLaunch', object: info.object });
return true;
},
}),
new TextLayer({
id: 'static-nklaunch-label',
data: NK_LAUNCH_SITES,
getPosition: (d) => [d.lng, d.lat],
getText: (d) => (d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo),
getSize: 11 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => [...hexToRgb(NK_LAUNCH_TYPE_META[d.type]?.color ?? '#f97316'), 255] as [number, number, number, number],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 8],
fontFamily: 'monospace',
fontWeight: 700,
fontSettings: { sdf: true },
outlineWidth: 8,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
// ── NK Missile Events — IconLayer ─────────────────────────────────────
if (config.nkMissile) {
function getLaunchIconUrl(type: string): string {
if (!launchIconCache.has(type)) {
launchIconCache.set(type, svgToDataUri(missileLaunchSvg(getMissileColor(type))));
}
return launchIconCache.get(type)!;
}
function getImpactIconUrl(type: string): string {
if (!impactIconCache.has(type)) {
impactIconCache.set(type, svgToDataUri(missileImpactSvg(getMissileColor(type))));
}
return impactIconCache.get(type)!;
}
interface LaunchPoint { ev: NKMissileEvent; lat: number; lng: number }
interface ImpactPoint { ev: NKMissileEvent; lat: number; lng: number }
const launchData: LaunchPoint[] = NK_MISSILE_EVENTS.map(ev => ({ ev, lat: ev.launchLat, lng: ev.launchLng }));
const impactData: ImpactPoint[] = NK_MISSILE_EVENTS.map(ev => ({ ev, lat: ev.impactLat, lng: ev.impactLng }));
// 발사→착탄 궤적선
const trajectoryData = NK_MISSILE_EVENTS.map(ev => ({
path: [[ev.launchLng, ev.launchLat], [ev.impactLng, ev.impactLat]] as [number, number][],
color: hexToRgb(getMissileColor(ev.type)),
}));
layers.push(
new PathLayer<{ path: [number, number][]; color: [number, number, number] }>({
id: 'static-nkmissile-trajectory',
data: trajectoryData,
getPath: (d) => d.path,
getColor: (d) => [...d.color, 150] as [number, number, number, number],
getWidth: 2,
widthUnits: 'pixels',
getDashArray: [6, 3],
dashJustified: true,
extensions: [],
}),
);
layers.push(
new IconLayer({
id: 'static-nkmissile-launch',
data: launchData,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getLaunchIconUrl(d.ev.type), width: 24, height: 24, anchorX: 12, anchorY: 12 }),
getSize: 12 * sc,
updateTriggers: { getSize: [sc] },
getColor: (d) => {
const today = new Date().toISOString().slice(0, 10) === d.ev.date;
return [255, 255, 255, today ? 255 : 90] as [number, number, number, number];
},
}),
new IconLayer({
id: 'static-nkmissile-impact',
data: impactData,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getImpactIconUrl(d.ev.type), width: 32, height: 32, anchorX: 16, anchorY: 16 }),
getSize: 16 * sc,
updateTriggers: { getSize: [sc] },
getColor: (d) => {
const today = new Date().toISOString().slice(0, 10) === d.ev.date;
return [255, 255, 255, today ? 255 : 100] as [number, number, number, number];
},
pickable: true,
onClick: (info: PickingInfo) => {
if (info.object) onPick({ kind: 'nkMissile', object: info.object.ev });
return true;
},
}),
new TextLayer({
id: 'static-nkmissile-label',
data: impactData,
getPosition: (d) => [d.lng, d.lat],
getText: (d) => `${d.ev.date.slice(5)} ${d.ev.time} ← ${d.ev.launchNameKo}`,
getSize: 11 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => [...hexToRgb(getMissileColor(d.ev.type)), 255] as [number, number, number, number],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 10],
fontFamily: 'monospace',
fontWeight: 700,
fontSettings: { sdf: true },
outlineWidth: 8,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
return layers;
}