- @fontsource-variable Inter, Noto Sans KR, Fira Code 자체 호스팅 - 전체 font-family 통일 (CSS, deck.gl, 인라인 스타일) - 이란 시설물 색상 사막 대비 고채도 팔레트로 교체 - 이란 라벨 fontWeight 600→700, alpha 200→255 - 접힘 패널 상하 패딩 균일화
107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
|
import type { Layer, PickingInfo } from '@deck.gl/core';
|
|
import { svgToDataUri } from '../../utils/svgToDataUri';
|
|
import { middleEastAirports } from '../../data/airports';
|
|
import type { Airport } from '../../data/airports';
|
|
import { FONT_MONO } from '../../styles/fonts';
|
|
|
|
export { type Airport };
|
|
|
|
export const IRAN_AIRPORT_COUNT = middleEastAirports.length;
|
|
|
|
const US_BASE_ICAOS = new Set([
|
|
'OMAD', 'OTBH', 'OKAJ', 'LTAG', 'OEPS', 'ORAA', 'ORBD', 'OBBS', 'OMTH', 'HDCL',
|
|
]);
|
|
|
|
function getAirportColor(airport: Airport): string {
|
|
const isMil = airport.type === 'military';
|
|
const isUS = isMil && US_BASE_ICAOS.has(airport.icao);
|
|
if (isUS) return '#60a5fa'; // blue-400
|
|
if (isMil) return '#f87171'; // red-400
|
|
if (airport.type === 'international') return '#38bdf8'; // sky-400 (was amber)
|
|
return '#a5b4fc'; // indigo-200 (was gray)
|
|
}
|
|
|
|
function airportSvg(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 C11.4 5 11 5.4 11 5.8 L11 10 L6 13 L6 14.2 L11 12.5 L11 16.5 L9.2 17.8 L9.2 18.8 L12 18 L14.8 18.8 L14.8 17.8 L13 16.5 L13 12.5 L18 14.2 L18 13 L13 10 L13 5.8 C13 5.4 12.6 5 12 5Z"
|
|
fill="${color}" stroke="#fff" stroke-width="0.3"/>
|
|
</svg>`;
|
|
}
|
|
|
|
const iconCache = new Map<string, string>();
|
|
|
|
function getIconUrl(airport: Airport): string {
|
|
const color = getAirportColor(airport);
|
|
const size = airport.type === 'military' && US_BASE_ICAOS.has(airport.icao) ? 48 : 40;
|
|
const key = `${color}-${size}`;
|
|
if (!iconCache.has(key)) {
|
|
iconCache.set(key, svgToDataUri(airportSvg(color, size)));
|
|
}
|
|
return iconCache.get(key)!;
|
|
}
|
|
|
|
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 IranAirportLayerConfig {
|
|
visible: boolean;
|
|
sc: number;
|
|
fs?: number;
|
|
onPick: (airport: Airport) => void;
|
|
}
|
|
|
|
export function createIranAirportLayers(config: IranAirportLayerConfig): Layer[] {
|
|
const { visible, sc, onPick } = config;
|
|
const fs = config.fs ?? 1;
|
|
if (!visible) return [];
|
|
|
|
const iconLayer = new IconLayer<Airport>({
|
|
id: 'iran-airport-icon',
|
|
data: middleEastAirports,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: (d) => {
|
|
const isMilUS = d.type === 'military' && US_BASE_ICAOS.has(d.icao);
|
|
const sz = isMilUS ? 48 : 40;
|
|
return { url: getIconUrl(d), width: sz, height: sz, anchorX: sz / 2, anchorY: sz / 2 };
|
|
},
|
|
getSize: (d) => (d.type === 'military' && US_BASE_ICAOS.has(d.icao) ? 20 : 16) * sc,
|
|
updateTriggers: { getSize: [sc] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<Airport>) => {
|
|
if (info.object) onPick(info.object);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
const labelLayer = new TextLayer<Airport>({
|
|
id: 'iran-airport-label',
|
|
data: middleEastAirports,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => {
|
|
const nameKo = d.nameKo ?? d.name;
|
|
return nameKo.length > 10 ? nameKo.slice(0, 10) + '..' : nameKo;
|
|
},
|
|
getSize: 11 * sc * fs,
|
|
updateTriggers: { getSize: [sc] },
|
|
getColor: (d) => hexToRgba(getAirportColor(d)),
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: FONT_MONO,
|
|
fontWeight: 700,
|
|
fontSettings: { sdf: true },
|
|
outlineWidth: 3,
|
|
outlineColor: [0, 0, 0, 255],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
});
|
|
|
|
return [iconLayer, labelLayer];
|
|
}
|