- FontScaleContext + FontScalePanel: 시설/선박/분석/지역 4그룹 × 0.5~2.0 범위 - LAYERS 패널 하단 슬라이더 UI, localStorage 영속화 - Korea static 14개 + Iran 4개 + 분석 3개 + KoreaMap 5개 TextLayer 적용 - MapLibre 선박 라벨/국가명 실시간 반영 - 모든 useMemo deps + updateTriggers에 fontScale 포함
502 lines
24 KiB
TypeScript
502 lines
24 KiB
TypeScript
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 `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<polygon points="12,2 22,20 2,20" fill="${color}" stroke="#fff" stroke-width="1"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function missileImpactSvg(color: string): string {
|
|
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="7" y1="7" x2="17" y2="17" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
<line x1="17" y1="7" x2="7" y2="17" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// ─── MilitaryBase SVG ─────────────────────────────────────────────────────────
|
|
|
|
function navalBaseSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="12" y1="5" x2="12" y2="19" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="7" y1="9" x2="17" y2="9" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M7 9 Q6 13 8 15" fill="none" stroke="${color}" stroke-width="1"/>
|
|
<path d="M17 9 Q18 13 16 15" fill="none" stroke="${color}" stroke-width="1"/>
|
|
<path d="M8 15 Q12 18 16 15" fill="none" stroke="${color}" stroke-width="1.2"/>
|
|
<circle cx="12" cy="5" r="1.5" fill="${color}"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function airforceBaseSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M12 5 C11.4 5 11 5.4 11 5.8 L11 10 L6 13 L6 14.2 L11 12.5 L11 16.5 L9.2 17.8 L9.2 18.8 L12 18 L14.8 18.8 L14.8 17.8 L13 16.5 L13 12.5 L18 14.2 L18 13 L13 10 L13 5.8 C13 5.4 12.6 5 12 5Z"
|
|
fill="${color}" stroke="#fff" stroke-width="0.3"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function armyBaseSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M6 10 Q6 8 8 8 L12 7 L16 8 Q18 8 18 10 L18 12 Q18 15 12 17 Q6 15 6 12 Z" fill="${color}" opacity="0.75"/>
|
|
<line x1="9" y1="10" x2="15" y2="10" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
|
|
<line x1="8.5" y1="12" x2="15.5" y2="12" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function missileBaseSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M12 5 L14 10 L14 17 L12 19 L10 17 L10 10 Z" fill="${color}" opacity="0.85"/>
|
|
<path d="M10 13 L7 16 L10 15 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M14 13 L17 16 L14 15 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M11 10 L13 10 L13 8 Q12 6 11 8 Z" fill="#fff" opacity="0.5"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function jointBaseSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<polygon points="12,5 13.8,10.1 19.2,10.1 14.7,13.2 16.5,18.3 12,15.2 7.5,18.3 9.3,13.2 4.8,10.1 10.2,10.1" fill="${color}" opacity="0.85"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// ─── GovBuilding SVG ──────────────────────────────────────────────────────────
|
|
|
|
function governmentBuildingSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="5" y1="18" x2="19" y2="18" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="12" y1="5" x2="5" y2="9" stroke="${color}" stroke-width="1"/>
|
|
<line x1="12" y1="5" x2="19" y2="9" stroke="${color}" stroke-width="1"/>
|
|
<line x1="5" y1="9" x2="19" y2="9" stroke="${color}" stroke-width="1"/>
|
|
<rect x="7" y="10" width="2" height="8" fill="${color}" opacity="0.7"/>
|
|
<rect x="11" y="10" width="2" height="8" fill="${color}" opacity="0.7"/>
|
|
<rect x="15" y="10" width="2" height="8" fill="${color}" opacity="0.7"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function militaryHqSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<polygon points="12,5 13.8,10.1 19.2,10.1 14.7,13.2 16.5,18.3 12,15.2 7.5,18.3 9.3,13.2 4.8,10.1 10.2,10.1" fill="${color}" opacity="0.85"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function intelligenceSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<ellipse cx="11" cy="11" rx="5" ry="5" fill="none" stroke="${color}" stroke-width="1.5"/>
|
|
<circle cx="11" cy="11" r="2" fill="${color}" opacity="0.7"/>
|
|
<line x1="15" y1="15" x2="18" y2="18" stroke="${color}" stroke-width="2" stroke-linecap="round"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function foreignSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<circle cx="12" cy="12" r="6" fill="none" stroke="${color}" stroke-width="1.2"/>
|
|
<ellipse cx="12" cy="12" rx="3" ry="6" fill="none" stroke="${color}" stroke-width="0.8"/>
|
|
<line x1="6" y1="12" x2="18" y2="12" stroke="${color}" stroke-width="0.8"/>
|
|
<line x1="7" y1="8.5" x2="17" y2="8.5" stroke="${color}" stroke-width="0.6" opacity="0.6"/>
|
|
<line x1="7" y1="15.5" x2="17" y2="15.5" stroke="${color}" stroke-width="0.6" opacity="0.6"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function maritimeSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="12" y1="5" x2="12" y2="19" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="7" y1="9" x2="17" y2="9" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M7 9 Q6 13 8 15" fill="none" stroke="${color}" stroke-width="1"/>
|
|
<path d="M17 9 Q18 13 16 15" fill="none" stroke="${color}" stroke-width="1"/>
|
|
<path d="M8 15 Q12 18 16 15" fill="none" stroke="${color}" stroke-width="1.2"/>
|
|
<circle cx="12" cy="5" r="1.5" fill="${color}"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function defenseSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M6 10 Q6 8 8 8 L12 7 L16 8 Q18 8 18 10 L18 12 Q18 15 12 17 Q6 15 6 12 Z" fill="${color}" opacity="0.75"/>
|
|
<line x1="9" y1="10" x2="15" y2="10" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
|
|
<line x1="8.5" y1="12" x2="15.5" y2="12" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// ─── NK Launch Site SVG ───────────────────────────────────────────────────────
|
|
|
|
function icbmSiteSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M12 4 L14.5 9 L14.5 18 L12 20 L9.5 18 L9.5 9 Z" fill="${color}" opacity="0.9"/>
|
|
<path d="M9.5 12 L6 15 L9.5 14 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M14.5 12 L18 15 L14.5 14 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M10.5 9 L13.5 9 L13.5 7 Q12 5 10.5 7 Z" fill="#fff" opacity="0.4"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function srbmSiteSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M12 5 L14 10 L14 17 L12 19 L10 17 L10 10 Z" fill="${color}" opacity="0.85"/>
|
|
<path d="M10 13 L7 16 L10 15 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M14 13 L17 16 L14 15 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M11 10 L13 10 L13 8 Q12 6 11 8 Z" fill="#fff" opacity="0.5"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function slbmSiteSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M2 16 Q12 20 22 16 L22 12 Q12 8 2 12 Z" fill="${color}" opacity="0.5"/>
|
|
<path d="M12 5 L13.5 9 L13.5 15 L12 17 L10.5 15 L10.5 9 Z" fill="${color}" opacity="0.9"/>
|
|
<path d="M10.5 12 L8 14 L10.5 13 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M13.5 12 L16 14 L13.5 13 Z" fill="${color}" opacity="0.7"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function cruiseSiteSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M5 13 Q8 11 12 11 Q16 11 19 13 L18 15 Q15 16 12 16 Q9 16 6 15 Z" fill="${color}" opacity="0.8"/>
|
|
<path d="M14 11 L14 8 L17 11" fill="${color}" opacity="0.6"/>
|
|
<path d="M12 11 L12 9 L14 11" fill="${color}" opacity="0.5"/>
|
|
<line x1="18" y1="13" x2="20" y2="12" stroke="${color}" stroke-width="1.2"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function artillerySiteSvg(color: string, size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="12" cy="12" r="10" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="5" y1="16" x2="17" y2="12" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
<circle cx="7" cy="17" r="2" fill="${color}" opacity="0.8"/>
|
|
<circle cx="11" cy="17" r="2" fill="${color}" opacity="0.8"/>
|
|
<line x1="17" y1="12" x2="20" y2="11" stroke="${color}" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// ─── Module-level icon caches ─────────────────────────────────────────────────
|
|
|
|
const launchIconCache = new Map<string, string>();
|
|
const impactIconCache = new Map<string, string>();
|
|
const milBaseIconCache = new Map<string, string>();
|
|
const govBuildingIconCache = new Map<string, string>();
|
|
const nkLaunchIconCache = new Map<string, string>();
|
|
|
|
// ─── MilitaryBase icon helpers ────────────────────────────────────────────────
|
|
|
|
const MIL_BASE_TYPE_COLOR: Record<string, string> = {
|
|
naval: '#3b82f6', airforce: '#f59e0b', army: '#22c55e',
|
|
missile: '#ef4444', joint: '#a78bfa',
|
|
};
|
|
|
|
type MilBaseSvgFn = (color: string, size: number) => string;
|
|
|
|
const MIL_BASE_SVG_FN: Record<string, MilBaseSvgFn> = {
|
|
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<string, string> = {
|
|
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<string, GovSvgFn> = {
|
|
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<string, NkLaunchSvgFn> = {
|
|
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<MilitaryBase>({
|
|
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<MilitaryBase>) => {
|
|
if (info.object) onPick({ kind: 'militaryBase', object: info.object });
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<MilitaryBase>({
|
|
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<GovBuilding>({
|
|
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<GovBuilding>) => {
|
|
if (info.object) onPick({ kind: 'govBuilding', object: info.object });
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<GovBuilding>({
|
|
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<NKLaunchSite>({
|
|
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<NKLaunchSite>) => {
|
|
if (info.object) onPick({ kind: 'nkLaunch', object: info.object });
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<NKLaunchSite>({
|
|
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<LaunchPoint>({
|
|
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<ImpactPoint>({
|
|
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<ImpactPoint>) => {
|
|
if (info.object) onPick({ kind: 'nkMissile', object: info.object.ev });
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<ImpactPoint>({
|
|
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;
|
|
}
|