kcg-monitoring/frontend/src/hooks/layers/createFacilityLayers.ts

539 lines
23 KiB
TypeScript

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<string, string> = {
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 `<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>`;
}
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 `<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.5" y="0.5" width="${size - 1}" height="${size - 1}" rx="1" fill="#111" stroke="${color}" stroke-width="1"/>
</svg>`;
}
// ─── Hazard 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>`;
}
function shipyardSvg(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="M6 15 Q7 13 9 13 L15 13 Q17 13 18 15 L17 17 Q12 19 7 17 Z" fill="${color}" opacity="0.75"/>
<line x1="12" y1="6" x2="12" y2="13" stroke="${color}" stroke-width="1.2"/>
<line x1="12" y1="6" x2="16" y2="10" stroke="${color}" stroke-width="1.2"/>
<line x1="16" y1="10" x2="16" y2="13" stroke="${color}" stroke-width="1"/>
</svg>`;
}
function wastewaterSvg(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 Q15 9 15 12 Q15 15.3 12 17 Q9 15.3 9 12 Q9 9 12 5 Z" fill="${color}" opacity="0.75"/>
<path d="M9 14 Q10.5 15.5 12 16" fill="none" stroke="#fff" stroke-width="0.7" opacity="0.5"/>
<path d="M8.5 12 Q9.5 13.5 11 14" fill="none" stroke="#fff" stroke-width="0.7" opacity="0.5"/>
</svg>`;
}
function heavyIndustrySvg(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="4" fill="none" stroke="${color}" stroke-width="1.2"/>
<circle cx="12" cy="12" r="1.5" fill="${color}"/>
<line x1="12" y1="6" x2="12" y2="8" stroke="${color}" stroke-width="1.5"/>
<line x1="12" y1="16" x2="12" y2="18" stroke="${color}" stroke-width="1.5"/>
<line x1="6" y1="12" x2="8" y2="12" stroke="${color}" stroke-width="1.5"/>
<line x1="16" y1="12" x2="18" y2="12" stroke="${color}" stroke-width="1.5"/>
<line x1="7.8" y1="7.8" x2="9.2" y2="9.2" stroke="${color}" stroke-width="1.2"/>
<line x1="14.8" y1="14.8" x2="16.2" y2="16.2" stroke="${color}" stroke-width="1.2"/>
<line x1="16.2" y1="7.8" x2="14.8" y2="9.2" stroke="${color}" stroke-width="1.2"/>
<line x1="9.2" y1="14.8" x2="7.8" y2="16.2" stroke="${color}" stroke-width="1.2"/>
</svg>`;
}
// ─── Naval/Airbase/Army SVG (reused from createMilitaryLayers pattern) ─────────
function navalFacSvg(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"/>
<line x1="7" y1="9" x2="17" y2="9" stroke="${color}" stroke-width="1.5"/>
<path d="M7 9 Q6 13 8 15" fill="none" stroke="${color}" stroke-width="1"/>
<path d="M17 9 Q18 13 16 15" fill="none" stroke="${color}" stroke-width="1"/>
<path d="M8 15 Q12 18 16 15" fill="none" stroke="${color}" stroke-width="1.2"/>
<circle cx="12" cy="5" r="1.5" fill="${color}"/>
</svg>`;
}
function airbaseFacSvg(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>`;
}
function armyFacSvg(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="M6 10 Q6 8 8 8 L12 7 L16 8 Q18 8 18 10 L18 12 Q18 15 12 17 Q6 15 6 12 Z" fill="${color}" opacity="0.75"/>
<line x1="9" y1="10" x2="15" y2="10" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
<line x1="8.5" y1="12" x2="15.5" y2="12" stroke="#fff" stroke-width="0.8" opacity="0.5"/>
</svg>`;
}
// ─── Module-level icon caches ─────────────────────────────────────────────────
const infraIconCache = new Map<string, string>();
const hazardIconCache = new Map<string, string>();
const cnIconCache = new Map<string, string>();
const jpIconCache = new Map<string, string>();
// ─── Hazard icon helpers ───────────────────────────────────────────────────────
const HAZARD_SVG: Record<string, (c: string, s: number) => string> = {
petrochemical: petrochemSvg,
lng: lngSvg,
oilTank: oilTankSvg,
hazardPort: hazPortSvg,
nuclear: nuclearSvg,
thermal: thermalSvg,
shipyard: shipyardSvg,
wastewater: wastewaterSvg,
heavyIndustry: heavyIndustrySvg,
};
const HAZARD_COLOR: Record<string, string> = {
petrochemical: '#f97316',
lng: '#06b6d4',
oilTank: '#eab308',
hazardPort: '#ef4444',
nuclear: '#a855f7',
thermal: '#64748b',
shipyard: '#0ea5e9',
wastewater: '#10b981',
heavyIndustry: '#94a3b8',
};
function getHazardIconUrl(type: string): string {
if (!hazardIconCache.has(type)) {
const color = HAZARD_COLOR[type] ?? '#888';
const svgFn = HAZARD_SVG[type] ?? hazPortSvg;
hazardIconCache.set(type, svgToDataUri(svgFn(color, 64)));
}
return hazardIconCache.get(type)!;
}
// ─── CN icon helpers ───────────────────────────────────────────────────────────
const CN_SVG: Record<string, (c: string, s: number) => string> = {
nuclear: nuclearSvg,
thermal: thermalSvg,
naval: navalFacSvg,
airbase: airbaseFacSvg,
army: armyFacSvg,
shipyard: shipyardSvg,
};
const CN_COLOR: Record<string, string> = {
nuclear: '#ef4444',
thermal: '#f97316',
naval: '#3b82f6',
airbase: '#22d3ee',
army: '#22c55e',
shipyard: '#94a3b8',
};
function getCnIconUrl(subType: string): string {
if (!cnIconCache.has(subType)) {
const color = CN_COLOR[subType] ?? '#888';
const svgFn = CN_SVG[subType] ?? armyFacSvg;
cnIconCache.set(subType, svgToDataUri(svgFn(color, 64)));
}
return cnIconCache.get(subType)!;
}
// ─── JP icon helpers ───────────────────────────────────────────────────────────
const JP_SVG: Record<string, (c: string, s: number) => string> = {
nuclear: nuclearSvg,
thermal: thermalSvg,
naval: navalFacSvg,
airbase: airbaseFacSvg,
army: armyFacSvg,
};
const JP_COLOR: Record<string, string> = {
nuclear: '#ef4444',
thermal: '#f97316',
naval: '#3b82f6',
airbase: '#22d3ee',
army: '#22c55e',
};
function getJpIconUrl(subType: string): string {
if (!jpIconCache.has(subType)) {
const color = JP_COLOR[subType] ?? '#888';
const svgFn = JP_SVG[subType] ?? armyFacSvg;
jpIconCache.set(subType, svgToDataUri(svgFn(color, 64)));
}
return jpIconCache.get(subType)!;
}
// ─── 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) {
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<PowerFacility>({
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,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo<PowerFacility>) => {
if (info.object) onPick({ kind: 'infra', object: info.object });
return true;
},
}),
new IconLayer<PowerFacility>({
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,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo<PowerFacility>) => {
if (info.object) onPick({ kind: 'infra', object: info.object });
return true;
},
}),
new TextLayer<PowerFacility>({
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: 12 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => [...hexToRgb(infraColor(d)), 255] as [number, number, number, number],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 8],
fontFamily: 'monospace',
fontWeight: 600,
fontSettings: { sdf: true },
outlineWidth: 3,
outlineColor: [0, 0, 0, 255],
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<string, { color: [number, number, number, number] }> = {
petrochemical: { color: [249, 115, 22, 255] },
lng: { color: [6, 182, 212, 255] },
oilTank: { color: [234, 179, 8, 255] },
hazardPort: { color: [239, 68, 68, 255] },
nuclear: { color: [168, 85, 247, 255] },
thermal: { color: [100, 116, 139, 255] },
shipyard: { color: [14, 165, 233, 255] },
wastewater: { color: [16, 185, 129, 255] },
heavyIndustry: { color: [148, 163, 184, 255] },
};
if (hazardData.length > 0) {
layers.push(
new IconLayer<HazardFacility>({
id: 'static-hazard-icon',
data: hazardData,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getHazardIconUrl(d.type), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
getSize: 18 * sc,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo<HazardFacility>) => {
if (info.object) onPick({ kind: 'hazard', object: info.object });
return true;
},
}),
);
layers.push(
new TextLayer<HazardFacility>({
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: 12 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => HAZARD_META[d.type]?.color ?? [200, 200, 200, 200],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 12],
fontFamily: 'monospace',
fontWeight: 600,
fontSettings: { sdf: true },
outlineWidth: 3,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
}
// ── CN Facilities ──────────────────────────────────────────────────────
{
const CN_META: Record<string, { color: [number, number, number, number] }> = {
nuclear: { color: [239, 68, 68, 255] },
thermal: { color: [249, 115, 22, 255] },
naval: { color: [59, 130, 246, 255] },
airbase: { color: [34, 211, 238, 255] },
army: { color: [34, 197, 94, 255] },
shipyard: { 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 IconLayer<CnFacility>({
id: 'static-cn-icon',
data: cnData,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getCnIconUrl(d.subType), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
getSize: 18 * sc,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo<CnFacility>) => {
if (info.object) onPick({ kind: 'cnFacility', object: info.object });
return true;
},
}),
);
layers.push(
new TextLayer<CnFacility>({
id: 'static-cn-label',
data: cnData,
getPosition: (d) => [d.lng, d.lat],
getText: (d) => d.name,
getSize: 12 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => CN_META[d.subType]?.color ?? [200, 200, 200, 200],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 12],
fontFamily: 'monospace',
fontWeight: 600,
fontSettings: { sdf: true },
outlineWidth: 3,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
}
// ── JP Facilities ──────────────────────────────────────────────────────
{
const JP_META: Record<string, { color: [number, number, number, number] }> = {
nuclear: { color: [239, 68, 68, 255] },
thermal: { color: [249, 115, 22, 255] },
naval: { color: [59, 130, 246, 255] },
airbase: { color: [34, 211, 238, 255] },
army: { 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 IconLayer<JpFacility>({
id: 'static-jp-icon',
data: jpData,
getPosition: (d) => [d.lng, d.lat],
getIcon: (d) => ({ url: getJpIconUrl(d.subType), width: 64, height: 64, anchorX: 32, anchorY: 32 }),
getSize: 18 * sc,
updateTriggers: { getSize: [sc] },
pickable: true,
onClick: (info: PickingInfo<JpFacility>) => {
if (info.object) onPick({ kind: 'jpFacility', object: info.object });
return true;
},
}),
);
layers.push(
new TextLayer<JpFacility>({
id: 'static-jp-label',
data: jpData,
getPosition: (d) => [d.lng, d.lat],
getText: (d) => d.name,
getSize: 12 * sc * fc.fs,
updateTriggers: { getSize: [sc, fc.fs] },
getColor: (d) => JP_META[d.subType]?.color ?? [200, 200, 200, 200],
getTextAnchor: 'middle',
getAlignmentBaseline: 'top',
getPixelOffset: [0, 12],
fontFamily: 'monospace',
fontWeight: 600,
fontSettings: { sdf: true },
outlineWidth: 3,
outlineColor: [0, 0, 0, 255],
billboard: false,
characterSet: 'auto',
}),
);
}
}
return layers;
}