155 lines
6.5 KiB
TypeScript
155 lines
6.5 KiB
TypeScript
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
|
import type { PickingInfo, Layer } from '@deck.gl/core';
|
|
import { svgToDataUri } from '../../utils/svgToDataUri';
|
|
import { EAST_ASIA_PORTS } from '../../data/ports';
|
|
import type { Port } from '../../data/ports';
|
|
import { KOREA_WIND_FARMS } from '../../data/windFarms';
|
|
import type { WindFarm } from '../../data/windFarms';
|
|
import { hexToRgb } from './types';
|
|
import type { LayerFactoryConfig } from './types';
|
|
|
|
// ─── Port colors ──────────────────────────────────────────────────────────────
|
|
|
|
const PORT_COUNTRY_COLOR: Record<string, string> = {
|
|
KR: '#3b82f6',
|
|
CN: '#ef4444',
|
|
JP: '#f472b6',
|
|
KP: '#f97316',
|
|
TW: '#10b981',
|
|
};
|
|
|
|
// ─── Port SVG ────────────────────────────────────────────────────────────────
|
|
|
|
function portSvg(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="5" r="2.5" stroke="${color}" stroke-width="1.5" fill="none"/>
|
|
<line x1="12" y1="7.5" x2="12" y2="21" stroke="${color}" stroke-width="1.5"/>
|
|
<line x1="7" y1="12" x2="17" y2="12" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M5 18 Q8 21 12 21 Q16 21 19 18" stroke="${color}" stroke-width="1.5" fill="none"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// ─── Wind Turbine SVG ─────────────────────────────────────────────────────────
|
|
|
|
const WIND_COLOR = '#00bcd4';
|
|
|
|
function windTurbineSvg(size: number): string {
|
|
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<line x1="12" y1="10" x2="11" y2="23" stroke="${WIND_COLOR}" stroke-width="1.5"/>
|
|
<line x1="12" y1="10" x2="13" y2="23" stroke="${WIND_COLOR}" stroke-width="1.5"/>
|
|
<circle cx="12" cy="9" r="1.8" fill="${WIND_COLOR}"/>
|
|
<path d="M12 9 L11.5 1 Q12 0 12.5 1 Z" fill="${WIND_COLOR}" opacity="0.9"/>
|
|
<path d="M12 9 L18 14 Q18.5 13 17.5 12.5 Z" fill="${WIND_COLOR}" opacity="0.9"/>
|
|
<path d="M12 9 L6 14 Q5.5 13 6.5 12.5 Z" fill="${WIND_COLOR}" opacity="0.9"/>
|
|
<line x1="8" y1="23" x2="16" y2="23" stroke="${WIND_COLOR}" stroke-width="1.5"/>
|
|
</svg>`;
|
|
}
|
|
|
|
// ─── Module-level icon caches ─────────────────────────────────────────────────
|
|
|
|
const portIconCache = new Map<string, string>();
|
|
const WIND_ICON_URL = svgToDataUri(windTurbineSvg(36));
|
|
|
|
export function createPortLayers(
|
|
config: { ports: boolean; windFarm: boolean },
|
|
fc: LayerFactoryConfig,
|
|
): Layer[] {
|
|
const layers: Layer[] = [];
|
|
const sc = fc.sc;
|
|
const onPick = fc.onPick;
|
|
|
|
// ── Ports ───────────────────────────────────────────────────────────────
|
|
if (config.ports) {
|
|
function getPortIconUrl(p: Port): string {
|
|
const key = `${p.country}-${p.type}`;
|
|
if (!portIconCache.has(key)) {
|
|
const color = PORT_COUNTRY_COLOR[p.country] ?? PORT_COUNTRY_COLOR.KR;
|
|
const size = p.type === 'major' ? 32 : 24;
|
|
portIconCache.set(key, svgToDataUri(portSvg(color, size)));
|
|
}
|
|
return portIconCache.get(key)!;
|
|
}
|
|
|
|
layers.push(
|
|
new IconLayer<Port>({
|
|
id: 'static-ports-icon',
|
|
data: EAST_ASIA_PORTS,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: (d) => ({
|
|
url: getPortIconUrl(d),
|
|
width: d.type === 'major' ? 32 : 24,
|
|
height: d.type === 'major' ? 32 : 24,
|
|
anchorX: d.type === 'major' ? 16 : 12,
|
|
anchorY: d.type === 'major' ? 16 : 12,
|
|
}),
|
|
getSize: (d) => (d.type === 'major' ? 16 : 12) * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<Port>) => {
|
|
if (info.object) onPick({ kind: 'port', object: info.object });
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<Port>({
|
|
id: 'static-ports-label',
|
|
data: EAST_ASIA_PORTS,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => d.nameKo.replace('항', ''),
|
|
getSize: 12 * sc * fc.fs,
|
|
updateTriggers: { getSize: [sc, fc.fs] },
|
|
getColor: (d) => [...hexToRgb(PORT_COUNTRY_COLOR[d.country] ?? PORT_COUNTRY_COLOR.KR), 255] as [number, number, number, number],
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 8],
|
|
fontFamily: 'monospace',
|
|
fontWeight: 700,
|
|
fontSettings: { sdf: true },
|
|
outlineWidth: 3,
|
|
outlineColor: [0, 0, 0, 255],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
}),
|
|
);
|
|
}
|
|
|
|
// ── Wind Farms ─────────────────────────────────────────────────────────
|
|
if (config.windFarm) {
|
|
layers.push(
|
|
new IconLayer<WindFarm>({
|
|
id: 'static-windfarm-icon',
|
|
data: KOREA_WIND_FARMS,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: () => ({ url: WIND_ICON_URL, width: 36, height: 36, anchorX: 18, anchorY: 18 }),
|
|
getSize: 18 * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<WindFarm>) => {
|
|
if (info.object) onPick({ kind: 'windFarm', object: info.object });
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<WindFarm>({
|
|
id: 'static-windfarm-label',
|
|
data: KOREA_WIND_FARMS,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => (d.name.length > 10 ? d.name.slice(0, 10) + '..' : d.name),
|
|
getSize: 12 * sc * fc.fs,
|
|
updateTriggers: { getSize: [sc, fc.fs] },
|
|
getColor: [...hexToRgb(WIND_COLOR), 255] as [number, number, number, number],
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: 'monospace',
|
|
fontWeight: 700,
|
|
fontSettings: { sdf: true },
|
|
outlineWidth: 3,
|
|
outlineColor: [0, 0, 0, 255],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
}),
|
|
);
|
|
}
|
|
|
|
return layers;
|
|
}
|