- @fontsource-variable Inter, Noto Sans KR, Fira Code 자체 호스팅 - 전체 font-family 통일 (CSS, deck.gl, 인라인 스타일) - 이란 시설물 색상 사막 대비 고채도 팔레트로 교체 - 이란 라벨 fontWeight 600→700, alpha 200→255 - 접힘 패널 상하 패딩 균일화
223 lines
9.7 KiB
TypeScript
223 lines
9.7 KiB
TypeScript
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
|
import type { PickingInfo, Layer } from '@deck.gl/core';
|
|
import { svgToDataUri } from '../../utils/svgToDataUri';
|
|
import { FONT_MONO } from '../../styles/fonts';
|
|
import {
|
|
ME_ENERGY_HAZARD_FACILITIES,
|
|
SUB_TYPE_META,
|
|
layerKeyToSubType,
|
|
layerKeyToCountry,
|
|
type EnergyHazardFacility,
|
|
type FacilitySubType,
|
|
} from '../../data/meEnergyHazardFacilities';
|
|
|
|
// LayerVisibility overseas key → countryKey mapping
|
|
const COUNTRY_KEY_TO_LAYER_KEY: Record<string, string> = {
|
|
us: 'overseasUS',
|
|
ir: 'overseasIran',
|
|
ae: 'overseasUAE',
|
|
sa: 'overseasSaudi',
|
|
om: 'overseasOman',
|
|
qa: 'overseasQatar',
|
|
kw: 'overseasKuwait',
|
|
iq: 'overseasIraq',
|
|
bh: 'overseasBahrain',
|
|
// il (Israel) is shown when meFacilities is true (no dedicated overseas key)
|
|
il: 'meFacilities',
|
|
};
|
|
|
|
function isFacilityVisible(f: EnergyHazardFacility, layers: Record<string, boolean>): boolean {
|
|
const countryLayerKey = COUNTRY_KEY_TO_LAYER_KEY[f.countryKey];
|
|
if (!countryLayerKey || !layers[countryLayerKey]) return false;
|
|
|
|
// Check sub-type toggle if present, otherwise fall through to country-level toggle
|
|
// Sub-type keys: e.g. "irPower", "ilNuclear", "usOilTank"
|
|
const subTypeKey = f.countryKey + capitalizeFirst(f.subType.replace('_', ''));
|
|
if (subTypeKey in layers) return !!layers[subTypeKey];
|
|
|
|
// Check category-level parent key: e.g. "irEnergy", "usHazard"
|
|
const categoryKey = f.countryKey + capitalizeFirst(f.category);
|
|
if (categoryKey in layers) return !!layers[categoryKey];
|
|
|
|
// Fall back to country-level toggle
|
|
return true;
|
|
}
|
|
|
|
function capitalizeFirst(s: string): string {
|
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
}
|
|
|
|
// Pre-build layer-key → subType entries from layerKeyToSubType/layerKeyToCountry
|
|
// for reference — the actual filter uses the above isFacilityVisible logic.
|
|
// Exported for re-use elsewhere if needed.
|
|
export { layerKeyToSubType, layerKeyToCountry };
|
|
|
|
export interface MELayerConfig {
|
|
layers: Record<string, boolean>;
|
|
sc: number;
|
|
fs?: number;
|
|
onPick: (facility: EnergyHazardFacility) => void;
|
|
}
|
|
|
|
function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
|
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, alpha];
|
|
}
|
|
|
|
// ─── SVG icon functions ────────────────────────────────────────────────────────
|
|
|
|
function powerSvg(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="M13 5 L7 13 L12 13 L11 19 L17 11 L12 11 Z" fill="${color}" opacity="0.9"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function windSvg(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="1.5" fill="${color}"/>
|
|
<line x1="12" y1="10.5" x2="12" y2="5" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M12 5 Q15 5 15 7 Q15 9 12 10.5" fill="${color}" opacity="0.7"/>
|
|
<line x1="10.7" y1="11.25" x2="6" y2="8.5" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M6 8.5 Q4 11 5.5 13 Q7 14.5 10.7 13" fill="${color}" opacity="0.7"/>
|
|
<line x1="13.3" y1="12.75" x2="18" y2="15.5" stroke="${color}" stroke-width="1.2"/>
|
|
<path d="M18 15.5 Q20 13 18.5 11 Q17 9.5 13.3 11" fill="${color}" opacity="0.7"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function nuclearSvg(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="2" fill="${color}"/>
|
|
<path d="M12 10 Q10 7 7 7 Q6 9 7 11 Q9 12 12 12" fill="${color}" opacity="0.7"/>
|
|
<path d="M13.7 11 Q16 9 17 7 Q15 5 13 6 Q11 8 12 10" fill="${color}" opacity="0.7"/>
|
|
<path d="M10.3 13 Q7 13 6 16 Q8 18 11 17 Q13 15 13.7 13" fill="${color}" opacity="0.7"/>
|
|
<path d="M13.7 13 Q15 16 17 17 Q19 15 18 12 Q16 11 13.7 12" fill="${color}" opacity="0.7"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function thermalSvg(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"/>
|
|
<rect x="5" y="11" width="14" height="7" rx="1" fill="${color}" opacity="0.6"/>
|
|
<rect x="7" y="7" width="2" height="5" fill="${color}" opacity="0.6"/>
|
|
<rect x="11" y="5" width="2" height="7" fill="${color}" opacity="0.6"/>
|
|
<rect x="15" y="8" width="2" height="4" fill="${color}" opacity="0.6"/>
|
|
<path d="M8 5 Q8.5 3.5 9 5 Q9.5 3 10 5" fill="none" stroke="${color}" stroke-width="0.8" opacity="0.8"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function petrochemSvg(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"/>
|
|
<rect x="10" y="5" width="4" height="8" rx="1" fill="${color}" opacity="0.65"/>
|
|
<ellipse cx="12" cy="14.5" rx="4.5" ry="2.5" fill="${color}" opacity="0.75"/>
|
|
<line x1="7" y1="10" x2="10" y2="10" stroke="${color}" stroke-width="1"/>
|
|
<line x1="14" y1="10" x2="17" y2="10" stroke="${color}" stroke-width="1"/>
|
|
<path d="M7 10 Q5.5 13 8 15" fill="none" stroke="${color}" stroke-width="0.8"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function lngSvg(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="9" y1="7" x2="9" y2="10" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="12" y1="5" x2="12" y2="10" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="15" y1="7" x2="15" y2="10" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="7" y1="9" x2="17" y2="9" stroke="${color}" stroke-width="1"/>
|
|
<ellipse cx="12" cy="15" rx="5" ry="3.5" fill="${color}" opacity="0.65"/>
|
|
<line x1="12" y1="10" x2="12" y2="11.5" stroke="${color}" stroke-width="1"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function oilTankSvg(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="12" cy="8" rx="5" ry="2" fill="${color}" opacity="0.5"/>
|
|
<rect x="7" y="8" width="10" height="8" fill="${color}" opacity="0.6"/>
|
|
<ellipse cx="12" cy="16" rx="5" ry="2" fill="${color}" opacity="0.8"/>
|
|
<line x1="9" y1="8" x2="9" y2="16" stroke="rgba(0,0,0,0.2)" stroke-width="0.5"/>
|
|
<line x1="15" y1="8" x2="15" y2="16" stroke="rgba(0,0,0,0.2)" stroke-width="0.5"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function hazPortSvg(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 L20 18 L4 18 Z" fill="${color}" opacity="0.7"/>
|
|
<line x1="12" y1="10" x2="12" y2="14" stroke="#fff" stroke-width="1.8" stroke-linecap="round"/>
|
|
<circle cx="12" cy="16" r="1" fill="#fff"/>
|
|
</svg>`;
|
|
}
|
|
|
|
const SUB_TYPE_SVG_FN: Record<FacilitySubType, (color: string, size: number) => string> = {
|
|
power: powerSvg,
|
|
wind: windSvg,
|
|
nuclear: nuclearSvg,
|
|
thermal: thermalSvg,
|
|
petrochem: petrochemSvg,
|
|
lng: lngSvg,
|
|
oil_tank: oilTankSvg,
|
|
haz_port: hazPortSvg,
|
|
};
|
|
|
|
const iconCache = new Map<string, string>();
|
|
|
|
function getIconUrl(subType: FacilitySubType): string {
|
|
if (!iconCache.has(subType)) {
|
|
const color = SUB_TYPE_META[subType].color;
|
|
iconCache.set(subType, svgToDataUri(SUB_TYPE_SVG_FN[subType](color, 64)));
|
|
}
|
|
return iconCache.get(subType)!;
|
|
}
|
|
|
|
export function createMEEnergyHazardLayers(config: MELayerConfig): Layer[] {
|
|
const { layers, sc, onPick } = config;
|
|
const fs = config.fs ?? 1;
|
|
|
|
const visibleFacilities = ME_ENERGY_HAZARD_FACILITIES.filter(f =>
|
|
isFacilityVisible(f, layers),
|
|
);
|
|
|
|
if (visibleFacilities.length === 0) return [];
|
|
|
|
const iconLayer = new IconLayer<EnergyHazardFacility>({
|
|
id: 'me-energy-hazard-icon',
|
|
data: visibleFacilities,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: (d) => ({ url: getIconUrl(d.subType), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
|
|
getSize: (d) => (d.category === 'hazard' ? 20 : 18) * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<EnergyHazardFacility>) => {
|
|
if (info.object) onPick(info.object);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
const labelLayer = new TextLayer<EnergyHazardFacility>({
|
|
id: 'me-energy-hazard-label',
|
|
data: visibleFacilities,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => d.nameKo.length > 12 ? d.nameKo.slice(0, 12) + '..' : d.nameKo,
|
|
getSize: 12 * sc * fs,
|
|
updateTriggers: { getSize: [sc] },
|
|
getColor: (d) => hexToRgba(SUB_TYPE_META[d.subType].color),
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 12],
|
|
fontFamily: FONT_MONO,
|
|
fontWeight: 700,
|
|
fontSettings: { sdf: true },
|
|
outlineWidth: 3,
|
|
outlineColor: [0, 0, 0, 255],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
});
|
|
|
|
return [iconLayer, labelLayer];
|
|
}
|