180 lines
8.3 KiB
TypeScript
180 lines
8.3 KiB
TypeScript
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
|
import type { PickingInfo, Layer } from '@deck.gl/core';
|
|
import { svgToDataUri } from '../../utils/svgToDataUri';
|
|
import { ME_FACILITIES } from '../../data/middleEastFacilities';
|
|
import type { MEFacility } from '../../data/middleEastFacilities';
|
|
|
|
export const ME_FACILITY_COUNT = ME_FACILITIES.length;
|
|
|
|
// ─── Type colors ──────────────────────────────────────────────────────────────
|
|
|
|
const TYPE_COLORS: Record<MEFacility['type'], string> = {
|
|
naval: '#3b82f6',
|
|
military_hq: '#ef4444',
|
|
missile: '#dc2626',
|
|
intelligence: '#8b5cf6',
|
|
government: '#f59e0b',
|
|
radar: '#06b6d4',
|
|
};
|
|
|
|
// ─── SVG generators ──────────────────────────────────────────────────────────
|
|
|
|
// naval: anchor symbol
|
|
function navalSvg(color: string): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="1" y="1" width="34" height="34" rx="4" fill="rgba(0,0,0,0.65)" stroke="${color}" stroke-width="1.5"/>
|
|
<circle cx="18" cy="10" r="3" fill="none" stroke="${color}" stroke-width="1.8"/>
|
|
<line x1="18" y1="13" x2="18" y2="28" stroke="${color}" stroke-width="1.8"/>
|
|
<line x1="10" y1="17" x2="26" y2="17" stroke="${color}" stroke-width="1.8"/>
|
|
<path d="M10 26 Q12 30 18 30 Q24 30 26 26" fill="none" stroke="${color}" stroke-width="1.8"/>
|
|
<line x1="10" y1="26" x2="8" y2="28" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="26" y1="26" x2="28" y2="28" stroke="${color}" stroke-width="1.2"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// military_hq: star symbol
|
|
function militaryHqSvg(color: string): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="1" y="1" width="34" height="34" rx="4" fill="rgba(0,0,0,0.65)" stroke="${color}" stroke-width="1.5"/>
|
|
<polygon points="18,6 21,15 30,15 23,20 26,29 18,24 10,29 13,20 6,15 15,15" fill="${color}" opacity="0.9"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// missile: upward arrow / rocket shape
|
|
function missileSvg(color: string): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="1" y="1" width="34" height="34" rx="4" fill="rgba(0,0,0,0.65)" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M18 5 L21 14 L21 24 L18 27 L15 24 L15 14 Z" fill="${color}" opacity="0.85"/>
|
|
<path d="M15 14 L10 18 L15 18 Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M21 14 L26 18 L21 18 Z" fill="${color}" opacity="0.7"/>
|
|
<line x1="16" y1="27" x2="14" y2="32" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="20" y1="27" x2="22" y2="32" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
<circle cx="18" cy="10" r="2" fill="rgba(0,0,0,0.5)" stroke="${color}" stroke-width="1"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// intelligence: magnifying glass
|
|
function intelligenceSvg(color: string): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="1" y="1" width="34" height="34" rx="4" fill="rgba(0,0,0,0.65)" stroke="${color}" stroke-width="1.5"/>
|
|
<circle cx="16" cy="16" r="8" fill="none" stroke="${color}" stroke-width="2.2"/>
|
|
<line x1="22" y1="22" x2="30" y2="30" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
<circle cx="14" cy="14" r="3" fill="${color}" opacity="0.2"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// government: pillars / building
|
|
function governmentSvg(color: string): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="1" y="1" width="34" height="34" rx="4" fill="rgba(0,0,0,0.65)" stroke="${color}" stroke-width="1.5"/>
|
|
<rect x="6" y="30" width="24" height="2.5" rx="0.5" fill="${color}" opacity="0.8"/>
|
|
<rect x="8" y="27" width="20" height="3" rx="0.5" fill="${color}" opacity="0.6"/>
|
|
<rect x="9" y="14" width="2.5" height="13" fill="${color}" opacity="0.75"/>
|
|
<rect x="14" y="14" width="2.5" height="13" fill="${color}" opacity="0.75"/>
|
|
<rect x="19" y="14" width="2.5" height="13" fill="${color}" opacity="0.75"/>
|
|
<rect x="24" y="14" width="2.5" height="13" fill="${color}" opacity="0.75"/>
|
|
<path d="M6 14 L18 6 L30 14 Z" fill="${color}" opacity="0.8"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// radar: radio waves / dish
|
|
function radarSvg(color: string): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="1" y="1" width="34" height="34" rx="4" fill="rgba(0,0,0,0.65)" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M9 28 Q9 14 18 10 Q27 14 27 28" fill="${color}" opacity="0.15" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M12 28 Q12 17 18 13 Q24 17 24 28" fill="${color}" opacity="0.2" stroke="${color}" stroke-width="1"/>
|
|
<line x1="18" y1="10" x2="18" y2="28" stroke="${color}" stroke-width="1.5" opacity="0.6"/>
|
|
<circle cx="18" cy="10" r="2" fill="${color}" opacity="0.9"/>
|
|
<path d="M7 22 Q10 18 14 18" fill="none" stroke="${color}" stroke-width="1.2" opacity="0.55"/>
|
|
<path d="M29 22 Q26 18 22 18" fill="none" stroke="${color}" stroke-width="1.2" opacity="0.55"/>
|
|
<line x1="14" y1="28" x2="22" y2="28" stroke="${color}" stroke-width="2" opacity="0.6"/>
|
|
<line x1="18" y1="28" x2="18" y2="32" stroke="${color}" stroke-width="2" opacity="0.6"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function buildMESvg(type: MEFacility['type'], color: string): string {
|
|
switch (type) {
|
|
case 'naval': return navalSvg(color);
|
|
case 'military_hq': return militaryHqSvg(color);
|
|
case 'missile': return missileSvg(color);
|
|
case 'intelligence': return intelligenceSvg(color);
|
|
case 'government': return governmentSvg(color);
|
|
case 'radar': return radarSvg(color);
|
|
}
|
|
}
|
|
|
|
// ─── Module-level icon cache ─────────────────────────────────────────────────
|
|
|
|
const meIconCache = new Map<string, string>();
|
|
|
|
function getMEIconUrl(type: MEFacility['type']): string {
|
|
if (!meIconCache.has(type)) {
|
|
meIconCache.set(type, svgToDataUri(buildMESvg(type, TYPE_COLORS[type])));
|
|
}
|
|
return meIconCache.get(type)!;
|
|
}
|
|
|
|
// ─── Label color ─────────────────────────────────────────────────────────────
|
|
|
|
function getMELabelColor(type: MEFacility['type']): [number, number, number, number] {
|
|
const hex = TYPE_COLORS[type];
|
|
const r = parseInt(hex.slice(1, 3), 16);
|
|
const g = parseInt(hex.slice(3, 5), 16);
|
|
const b = parseInt(hex.slice(5, 7), 16);
|
|
return [r, g, b, 255];
|
|
}
|
|
|
|
// ─── Public interface ────────────────────────────────────────────────────────
|
|
|
|
export interface MEFacilityLayerConfig {
|
|
visible: boolean;
|
|
sc: number;
|
|
onPick: (f: MEFacility) => void;
|
|
}
|
|
|
|
export function createMEFacilityLayers(config: MEFacilityLayerConfig): Layer[] {
|
|
if (!config.visible) return [];
|
|
|
|
const { sc, onPick } = config;
|
|
|
|
return [
|
|
new IconLayer<MEFacility>({
|
|
id: 'me-facility-icon',
|
|
data: ME_FACILITIES,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: (d) => ({
|
|
url: getMEIconUrl(d.type),
|
|
width: 36,
|
|
height: 36,
|
|
anchorX: 18,
|
|
anchorY: 18,
|
|
}),
|
|
getSize: 16 * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<MEFacility>) => {
|
|
if (info.object) onPick(info.object);
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<MEFacility>({
|
|
id: 'me-facility-label',
|
|
data: ME_FACILITIES,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => (d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo),
|
|
getSize: 9 * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
getColor: (d) => getMELabelColor(d.type),
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: 'monospace',
|
|
fontWeight: 700,
|
|
outlineWidth: 2,
|
|
outlineColor: [0, 0, 0, 200],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
}),
|
|
];
|
|
}
|