- FontScaleContext + FontScalePanel: 시설/선박/분석/지역 4그룹 × 0.5~2.0 범위 - LAYERS 패널 하단 슬라이더 UI, localStorage 영속화 - Korea static 14개 + Iran 4개 + 분석 3개 + KoreaMap 5개 TextLayer 적용 - MapLibre 선박 라벨/국가명 실시간 반영 - 모든 useMemo deps + updateTriggers에 fontScale 포함
156 lines
6.4 KiB
TypeScript
156 lines
6.4 KiB
TypeScript
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
|
import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
import { svgToDataUri } from '../../utils/svgToDataUri';
|
|
import { iranOilFacilities } from '../../data/oilFacilities';
|
|
import type { OilFacility, OilFacilityType } from '../../types';
|
|
|
|
export { type OilFacility };
|
|
|
|
export const IRAN_OIL_COUNT = iranOilFacilities.length;
|
|
|
|
const TYPE_COLORS: Record<OilFacilityType, string> = {
|
|
refinery: '#f59e0b',
|
|
oilfield: '#10b981',
|
|
gasfield: '#6366f1',
|
|
terminal: '#ec4899',
|
|
petrochemical: '#8b5cf6',
|
|
desalination: '#06b6d4',
|
|
};
|
|
|
|
function refinerySvg(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="10" width="14" height="8" rx="1" fill="${color}" opacity="0.7"/>
|
|
<rect x="7" y="6" width="2" height="5" fill="${color}" opacity="0.6"/>
|
|
<rect x="11" y="4" width="2" height="7" fill="${color}" opacity="0.6"/>
|
|
<rect x="15" y="7" width="2" height="4" fill="${color}" opacity="0.6"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function oilfieldSvg(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="6" y1="18" x2="18" y2="18" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="12" y1="8" x2="8" y2="18" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="12" y1="8" x2="16" y2="18" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="5" y1="10" x2="17" y2="9" stroke="${color}" stroke-width="2"/>
|
|
<circle cx="12" cy="8" r="1.5" fill="${color}"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function gasfieldSvg(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="14" rx="5" ry="4" fill="${color}" opacity="0.6"/>
|
|
<line x1="12" y1="10" x2="12" y2="5" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M9 7 Q12 4 15 7" fill="none" stroke="${color}" stroke-width="1"/>
|
|
<path d="M10 5.5 Q12 3 14 5.5" fill="none" stroke="${color}" stroke-width="0.8" opacity="0.7"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function terminalSvg(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"/>
|
|
<path d="M8 9 Q12 6 16 9 Q16 14 12 16 Q8 14 8 9Z" fill="${color}" opacity="0.5"/>
|
|
<path d="M6 16 Q12 20 18 16" fill="none" stroke="${color}" stroke-width="1.2"/>
|
|
<line x1="8" y1="12" x2="16" y2="12" stroke="${color}" stroke-width="1"/>
|
|
</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="9" rx="1" fill="${color}" opacity="0.6"/>
|
|
<ellipse cx="12" cy="15" rx="4" ry="2.5" fill="${color}" opacity="0.7"/>
|
|
<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 Q6 13 8 15" fill="none" stroke="${color}" stroke-width="0.8"/>
|
|
</svg>`;
|
|
}
|
|
|
|
function desalinationSvg(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 Q16 10 16 14 Q16 18 12 18 Q8 18 8 14 Q8 10 12 5Z" fill="${color}" opacity="0.7"/>
|
|
<path d="M10 14 Q12 16 14 14" fill="none" stroke="#fff" stroke-width="0.8" opacity="0.6"/>
|
|
</svg>`;
|
|
}
|
|
|
|
type SvgFn = (color: string, size: number) => string;
|
|
|
|
const TYPE_SVG_FN: Record<OilFacilityType, SvgFn> = {
|
|
refinery: refinerySvg,
|
|
oilfield: oilfieldSvg,
|
|
gasfield: gasfieldSvg,
|
|
terminal: terminalSvg,
|
|
petrochemical: petrochemSvg,
|
|
desalination: desalinationSvg,
|
|
};
|
|
|
|
const iconCache = new Map<string, string>();
|
|
|
|
function getIconUrl(type: OilFacilityType): string {
|
|
if (!iconCache.has(type)) {
|
|
const color = TYPE_COLORS[type];
|
|
iconCache.set(type, svgToDataUri(TYPE_SVG_FN[type](color, 64)));
|
|
}
|
|
return iconCache.get(type)!;
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
export interface IranOilLayerConfig {
|
|
visible: boolean;
|
|
sc: number;
|
|
fs?: number;
|
|
onPick: (facility: OilFacility) => void;
|
|
}
|
|
|
|
export function createIranOilLayers(config: IranOilLayerConfig): Layer[] {
|
|
const { visible, sc, onPick } = config;
|
|
const fs = config.fs ?? 1;
|
|
if (!visible) return [];
|
|
|
|
const iconLayer = new IconLayer<OilFacility>({
|
|
id: 'iran-oil-icon',
|
|
data: iranOilFacilities,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: (d) => ({ url: getIconUrl(d.type), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
|
|
getSize: 18 * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<OilFacility>) => {
|
|
if (info.object) onPick(info.object);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
const labelLayer = new TextLayer<OilFacility>({
|
|
id: 'iran-oil-label',
|
|
data: iranOilFacilities,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => d.nameKo.length > 10 ? d.nameKo.slice(0, 10) + '..' : d.nameKo,
|
|
getSize: 12 * sc * fs,
|
|
updateTriggers: { getSize: [sc] },
|
|
getColor: (d) => hexToRgba(TYPE_COLORS[d.type]),
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: 'monospace',
|
|
fontWeight: 600,
|
|
fontSettings: { sdf: true },
|
|
outlineWidth: 8,
|
|
outlineColor: [0, 0, 0, 255],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
});
|
|
|
|
return [iconLayer, labelLayer];
|
|
}
|