import { IconLayer, TextLayer } from '@deck.gl/layers'; import { svgToDataUri } from '../../utils/svgToDataUri'; import { HAZARD_FACILITIES } from '../../data/hazardFacilities'; import { CN_POWER_PLANTS, CN_MILITARY_FACILITIES } from '../../data/cnFacilities'; import { JP_POWER_PLANTS, JP_MILITARY_FACILITIES } from '../../data/jpFacilities'; import { hexToRgb, type LayerFactoryConfig, type Layer, type PickingInfo, type PowerFacility, type HazardFacility, type HazardType, type CnFacility, type JpFacility, } from './types'; // ─── Infra SVG ──────────────────────────────────────────────────────────────── const INFRA_SOURCE_COLOR: Record = { nuclear: '#e040fb', coal: '#795548', gas: '#ff9800', oil: '#5d4037', hydro: '#2196f3', solar: '#ffc107', wind: '#00bcd4', biomass: '#4caf50', }; const INFRA_SUBSTATION_COLOR = '#ffeb3b'; const WIND_COLOR = '#00bcd4'; function windTurbineSvg(size: number): string { return ` `; } function infraColor(f: PowerFacility): string { if (f.type === 'substation') return INFRA_SUBSTATION_COLOR; return INFRA_SOURCE_COLOR[f.source ?? ''] ?? '#9e9e9e'; } function infraSvg(f: PowerFacility): string { const color = infraColor(f); if (f.source === 'wind') { return windTurbineSvg(14).replace(`stroke="${WIND_COLOR}"`, `stroke="${color}"`).replace(new RegExp(`fill="${WIND_COLOR}"`, 'g'), `fill="${color}"`); } const size = f.type === 'substation' ? 7 : 12; return ` `; } // ─── createFacilityLayers ───────────────────────────────────────────────────── export function createFacilityLayers( config: { infra: boolean; infraFacilities: PowerFacility[]; hazardTypes: HazardType[]; cnPower: boolean; cnMilitary: boolean; jpPower: boolean; jpMilitary: boolean; }, fc: LayerFactoryConfig, ): Layer[] { const layers: Layer[] = []; const sc = fc.sc; const onPick = fc.onPick; // ── Infra ────────────────────────────────────────────────────────────── if (config.infra && config.infraFacilities.length > 0) { const infraIconCache = new Map(); function getInfraIconUrl(f: PowerFacility): string { const key = `${f.type}-${f.source ?? ''}`; if (!infraIconCache.has(key)) { infraIconCache.set(key, svgToDataUri(infraSvg(f))); } return infraIconCache.get(key)!; } const plants = config.infraFacilities.filter(f => f.type === 'plant'); const substations = config.infraFacilities.filter(f => f.type === 'substation'); layers.push( new IconLayer({ id: 'static-infra-substation', data: substations, getPosition: (d) => [d.lng, d.lat], getIcon: (d) => ({ url: getInfraIconUrl(d), width: 14, height: 14, anchorX: 7, anchorY: 7 }), getSize: 7 * sc, pickable: true, onClick: (info: PickingInfo) => { if (info.object) onPick({ kind: 'infra', object: info.object }); return true; }, }), new IconLayer({ id: 'static-infra-plant', data: plants, getPosition: (d) => [d.lng, d.lat], getIcon: (d) => ({ url: getInfraIconUrl(d), width: 24, height: 24, anchorX: 12, anchorY: 12 }), getSize: 12 * sc, pickable: true, onClick: (info: PickingInfo) => { if (info.object) onPick({ kind: 'infra', object: info.object }); return true; }, }), new TextLayer({ id: 'static-infra-plant-label', data: plants, getPosition: (d) => [d.lng, d.lat], getText: (d) => (d.name.length > 10 ? d.name.slice(0, 10) + '..' : d.name), getSize: 8 * sc, getColor: (d) => [...hexToRgb(infraColor(d)), 255] as [number, number, number, number], getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 8], fontFamily: 'monospace', fontWeight: 600, outlineWidth: 2, outlineColor: [0, 0, 0, 200], billboard: false, characterSet: 'auto', }), ); } // ── Hazard Facilities ────────────────────────────────────────────────── if (config.hazardTypes.length > 0) { const hazardTypeSet = new Set(config.hazardTypes); const hazardData = HAZARD_FACILITIES.filter(f => hazardTypeSet.has(f.type)); const HAZARD_META: Record = { petrochemical: { icon: '🏭', color: [249, 115, 22, 255] }, lng: { icon: '🔵', color: [6, 182, 212, 255] }, oilTank: { icon: '🛢️', color: [234, 179, 8, 255] }, hazardPort: { icon: '⚠️', color: [239, 68, 68, 255] }, nuclear: { icon: '☢️', color: [168, 85, 247, 255] }, thermal: { icon: '🔥', color: [100, 116, 139, 255] }, shipyard: { icon: '🚢', color: [14, 165, 233, 255] }, wastewater: { icon: '💧', color: [16, 185, 129, 255] }, heavyIndustry: { icon: '⚙️', color: [148, 163, 184, 255] }, }; if (hazardData.length > 0) { layers.push( new TextLayer({ id: 'static-hazard-emoji', data: hazardData, getPosition: (d) => [d.lng, d.lat], getText: (d) => HAZARD_META[d.type]?.icon ?? '⚠️', getSize: 16 * sc, getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 255], getTextAnchor: 'middle', getAlignmentBaseline: 'center', pickable: true, onClick: (info: PickingInfo) => { if (info.object) onPick({ kind: 'hazard', object: info.object }); return true; }, billboard: false, characterSet: 'auto', }), ); layers.push( new TextLayer({ id: 'static-hazard-label', data: hazardData, getPosition: (d) => [d.lng, d.lat], getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo, getSize: 9 * sc, getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 200], getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], fontFamily: 'monospace', fontWeight: 600, outlineWidth: 2, outlineColor: [0, 0, 0, 200], billboard: false, characterSet: 'auto', }), ); } } // ── CN Facilities ────────────────────────────────────────────────────── { const CN_META: Record = { nuclear: { icon: '☢️', color: [239, 68, 68, 255] }, thermal: { icon: '🔥', color: [249, 115, 22, 255] }, naval: { icon: '⚓', color: [59, 130, 246, 255] }, airbase: { icon: '✈️', color: [34, 211, 238, 255] }, army: { icon: '🪖', color: [34, 197, 94, 255] }, shipyard: { icon: '🚢', color: [148, 163, 184, 255] }, }; const cnData: CnFacility[] = [ ...(config.cnPower ? CN_POWER_PLANTS : []), ...(config.cnMilitary ? CN_MILITARY_FACILITIES : []), ]; if (cnData.length > 0) { layers.push( new TextLayer({ id: 'static-cn-emoji', data: cnData, getPosition: (d) => [d.lng, d.lat], getText: (d) => CN_META[d.subType]?.icon ?? '📍', getSize: 16 * sc, getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 255], getTextAnchor: 'middle', getAlignmentBaseline: 'center', pickable: true, onClick: (info: PickingInfo) => { if (info.object) onPick({ kind: 'cnFacility', object: info.object }); return true; }, billboard: false, characterSet: 'auto', }), ); layers.push( new TextLayer({ id: 'static-cn-label', data: cnData, getPosition: (d) => [d.lng, d.lat], getText: (d) => d.name, getSize: 9 * sc, getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 200], getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], fontFamily: 'monospace', fontWeight: 600, outlineWidth: 2, outlineColor: [0, 0, 0, 200], billboard: false, characterSet: 'auto', }), ); } } // ── JP Facilities ────────────────────────────────────────────────────── { const JP_META: Record = { nuclear: { icon: '☢️', color: [239, 68, 68, 255] }, thermal: { icon: '🔥', color: [249, 115, 22, 255] }, naval: { icon: '⚓', color: [59, 130, 246, 255] }, airbase: { icon: '✈️', color: [34, 211, 238, 255] }, army: { icon: '🪖', color: [34, 197, 94, 255] }, }; const jpData: JpFacility[] = [ ...(config.jpPower ? JP_POWER_PLANTS : []), ...(config.jpMilitary ? JP_MILITARY_FACILITIES : []), ]; if (jpData.length > 0) { layers.push( new TextLayer({ id: 'static-jp-emoji', data: jpData, getPosition: (d) => [d.lng, d.lat], getText: (d) => JP_META[d.subType]?.icon ?? '📍', getSize: 16 * sc, getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 255], getTextAnchor: 'middle', getAlignmentBaseline: 'center', pickable: true, onClick: (info: PickingInfo) => { if (info.object) onPick({ kind: 'jpFacility', object: info.object }); return true; }, billboard: false, characterSet: 'auto', }), ); layers.push( new TextLayer({ id: 'static-jp-label', data: jpData, getPosition: (d) => [d.lng, d.lat], getText: (d) => d.name, getSize: 9 * sc, getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 200], getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 12], fontFamily: 'monospace', fontWeight: 600, outlineWidth: 2, outlineColor: [0, 0, 0, 200], billboard: false, characterSet: 'auto', }), ); } } return layers; }