253 lines
14 KiB
TypeScript
253 lines
14 KiB
TypeScript
import { IconLayer, TextLayer } from '@deck.gl/layers';
|
|
import type { PickingInfo, Layer } from '@deck.gl/core';
|
|
import { svgToDataUri } from '../../utils/svgToDataUri';
|
|
import { iranOilFacilities } from '../../data/oilFacilities';
|
|
import type { OilFacility, OilFacilityType } from '../../types';
|
|
|
|
export const IRAN_OIL_COUNT = iranOilFacilities.length;
|
|
|
|
// ─── Type colors ──────────────────────────────────────────────────────────────
|
|
|
|
const TYPE_COLORS: Record<OilFacilityType, string> = {
|
|
refinery: '#f59e0b',
|
|
oilfield: '#10b981',
|
|
gasfield: '#6366f1',
|
|
terminal: '#ec4899',
|
|
petrochemical: '#8b5cf6',
|
|
desalination: '#06b6d4',
|
|
};
|
|
|
|
// ─── SVG generators ──────────────────────────────────────────────────────────
|
|
|
|
function damageOverlaySvg(): string {
|
|
return `<line x1="4" y1="4" x2="32" y2="32" stroke="#ff0000" stroke-width="2.5" opacity="0.8"/>
|
|
<line x1="32" y1="4" x2="4" y2="32" stroke="#ff0000" stroke-width="2.5" opacity="0.8"/>
|
|
<circle cx="18" cy="18" r="15" fill="none" stroke="#ff0000" stroke-width="1.5" opacity="0.4"/>`;
|
|
}
|
|
|
|
function plannedOverlaySvg(): string {
|
|
return `<circle cx="18" cy="18" r="15" fill="none" stroke="#ff6600" stroke-width="2" stroke-dasharray="4 3" opacity="0.9"/>
|
|
<line x1="18" y1="0" x2="18" y2="4" stroke="#ff6600" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="18" y1="32" x2="18" y2="36" stroke="#ff6600" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="0" y1="18" x2="4" y2="18" stroke="#ff6600" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="32" y1="18" x2="36" y2="18" stroke="#ff6600" stroke-width="1.5" opacity="0.7"/>`;
|
|
}
|
|
|
|
function refinerySvg(color: string, damaged: boolean, planned: boolean): string {
|
|
const sc = damaged ? '#ff0000' : color;
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient id="refGrad" x1="0" y1="0" x2="1" y2="1">
|
|
<stop offset="0%" stop-color="${color}" stop-opacity="0.5"/>
|
|
<stop offset="100%" stop-color="${color}" stop-opacity="0.2"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<circle cx="18" cy="18" r="17" fill="url(#refGrad)" stroke="${sc}" stroke-width="${damaged ? 2 : 1}" opacity="0.9"/>
|
|
<rect x="6" y="19" width="24" height="11" rx="1" fill="${color}" opacity="0.6"/>
|
|
<rect x="16" y="7" width="4" height="13" fill="${color}" opacity="0.7"/>
|
|
<rect x="9" y="12" width="4" height="8" fill="${color}" opacity="0.65"/>
|
|
<rect x="23" y="10" width="4" height="10" fill="${color}" opacity="0.65"/>
|
|
<circle cx="11" cy="10" r="1.5" fill="${color}" opacity="0.3"/>
|
|
<circle cx="18" cy="5" r="2" fill="${color}" opacity="0.3"/>
|
|
<circle cx="25" cy="8" r="1.5" fill="${color}" opacity="0.3"/>
|
|
<rect x="10" y="22" width="3" height="3" rx="0.5" fill="rgba(0,0,0,0.4)"/>
|
|
<rect x="16" y="22" width="3" height="3" rx="0.5" fill="rgba(0,0,0,0.4)"/>
|
|
<rect x="23" y="22" width="3" height="3" rx="0.5" fill="rgba(0,0,0,0.4)"/>
|
|
<line x1="13" y1="15" x2="16" y2="15" stroke="${color}" stroke-width="0.8" opacity="0.5"/>
|
|
<line x1="20" y1="13" x2="23" y2="13" stroke="${color}" stroke-width="0.8" opacity="0.5"/>
|
|
${damaged ? damageOverlaySvg() : ''}
|
|
${planned ? plannedOverlaySvg() : ''}
|
|
</svg>`;
|
|
}
|
|
|
|
function oilfieldSvg(color: string, damaged: boolean, planned: boolean): string {
|
|
const sc = damaged ? '#ff0000' : color;
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<rect x="4" y="31" width="28" height="2.5" rx="1" fill="${color}" opacity="0.7"/>
|
|
<line x1="18" y1="14" x2="12" y2="31" stroke="${sc}" stroke-width="1.5" opacity="0.85"/>
|
|
<line x1="18" y1="14" x2="24" y2="31" stroke="${sc}" stroke-width="1.5" opacity="0.85"/>
|
|
<line x1="14" y1="25" x2="22" y2="25" stroke="${color}" stroke-width="1" opacity="0.7"/>
|
|
<line x1="4" y1="12" x2="28" y2="10" stroke="${sc}" stroke-width="2" opacity="0.9"/>
|
|
<circle cx="18" cy="13" r="2" fill="${color}" opacity="0.8" stroke="${sc}" stroke-width="1"/>
|
|
<path d="M4 12 L4 17 L7 17 L7 12" fill="none" stroke="${sc}" stroke-width="1.5" opacity="0.85"/>
|
|
<line x1="5.5" y1="17" x2="5.5" y2="31" stroke="${color}" stroke-width="1.2" opacity="0.75"/>
|
|
<rect x="25" y="10" width="5" height="6" rx="1" fill="${color}" opacity="0.6" stroke="${sc}" stroke-width="1"/>
|
|
<line x1="27.5" y1="16" x2="27.5" y2="24" stroke="${color}" stroke-width="1.2" opacity="0.75"/>
|
|
<rect x="24" y="24" width="7" height="5" rx="1" fill="${color}" opacity="0.55" stroke="${sc}" stroke-width="1"/>
|
|
<rect x="3" y="28" width="5" height="3" rx="0.5" fill="${color}" opacity="0.65" stroke="${sc}" stroke-width="0.8"/>
|
|
<path d="M5.5 29 C5.5 29 4.5 30 4.5 30.5 C4.5 31 5 31.2 5.5 31.2 C6 31.2 6.5 31 6.5 30.5 C6.5 30 5.5 29 5.5 29Z" fill="${color}" opacity="0.85"/>
|
|
${damaged ? damageOverlaySvg() : ''}
|
|
${planned ? plannedOverlaySvg() : ''}
|
|
</svg>`;
|
|
}
|
|
|
|
function gasfieldSvg(color: string, damaged: boolean, planned: boolean): string {
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<line x1="10" y1="24" x2="8" y2="33" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="14" y1="25" x2="13" y2="33" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="22" y1="25" x2="23" y2="33" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="26" y1="24" x2="28" y2="33" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
<line x1="9" y1="29" x2="14" y2="27" stroke="${color}" stroke-width="0.8" opacity="0.55"/>
|
|
<line x1="22" y1="27" x2="27" y2="29" stroke="${color}" stroke-width="0.8" opacity="0.55"/>
|
|
<line x1="7" y1="33" x2="29" y2="33" stroke="${color}" stroke-width="1.2" opacity="0.6"/>
|
|
<ellipse cx="18" cy="16" rx="12" ry="10" fill="${color}" opacity="0.45" stroke="${damaged ? '#ff0000' : color}" stroke-width="1.5"/>
|
|
<ellipse cx="16" cy="12" rx="7" ry="5" fill="${color}" opacity="0.3"/>
|
|
<ellipse cx="18" cy="16" rx="12" ry="2.5" fill="none" stroke="${color}" stroke-width="0.8" opacity="0.55"/>
|
|
<rect x="16.5" y="4" width="3" height="3" rx="0.5" fill="${color}" opacity="0.7"/>
|
|
<line x1="18" y1="4" x2="18" y2="6" stroke="${color}" stroke-width="1.5" opacity="0.7"/>
|
|
${damaged ? damageOverlaySvg() : ''}
|
|
${planned ? plannedOverlaySvg() : ''}
|
|
</svg>`;
|
|
}
|
|
|
|
function terminalSvg(color: string, damaged: boolean, planned: boolean): string {
|
|
const sc = damaged ? '#ff0000' : color;
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<circle cx="18" cy="10" r="4" fill="none" stroke="${sc}" stroke-width="2"/>
|
|
<line x1="18" y1="14" x2="18" y2="28" stroke="${color}" stroke-width="2"/>
|
|
<path d="M10 24 C10 28 14 32 18 32 C22 32 26 28 26 24" fill="none" stroke="${color}" stroke-width="2"/>
|
|
<line x1="18" y1="8" x2="18" y2="6" stroke="${color}" stroke-width="2"/>
|
|
<line x1="16" y1="6" x2="20" y2="6" stroke="${color}" stroke-width="2.5"/>
|
|
<path d="M6 24 L10 24" stroke="${color}" stroke-width="1.5"/>
|
|
<path d="M26 24 L30 24" stroke="${color}" stroke-width="1.5"/>
|
|
<polygon points="5,24 8,22 8,26" fill="${color}" opacity="0.7"/>
|
|
<polygon points="31,24 28,22 28,26" fill="${color}" opacity="0.7"/>
|
|
${damaged ? damageOverlaySvg() : ''}
|
|
${planned ? plannedOverlaySvg() : ''}
|
|
</svg>`;
|
|
}
|
|
|
|
function petrochemSvg(color: string, damaged: boolean, planned: boolean): string {
|
|
const sc = damaged ? '#ff0000' : '#fff';
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M14 6 L14 16 L8 30 L28 30 L22 16 L22 6Z" fill="${color}" opacity="0.7" stroke="${sc}" stroke-width="1"/>
|
|
<rect x="13" y="4" width="10" height="4" rx="1" fill="${color}" opacity="0.9" stroke="${sc}" stroke-width="0.8"/>
|
|
<path d="M11 22 L25 22 L28 30 L8 30Z" fill="${color}" opacity="0.5"/>
|
|
<circle cx="16" cy="25" r="1.5" fill="#c4b5fd" opacity="0.7"/>
|
|
<circle cx="20" cy="23" r="1" fill="#c4b5fd" opacity="0.6"/>
|
|
<circle cx="18" cy="27" r="1.2" fill="#c4b5fd" opacity="0.5"/>
|
|
${damaged ? damageOverlaySvg() : ''}
|
|
${planned ? plannedOverlaySvg() : ''}
|
|
</svg>`;
|
|
}
|
|
|
|
function desalSvg(color: string, damaged: boolean, planned: boolean): string {
|
|
const sc = damaged ? '#ff0000' : color;
|
|
return `<svg width="36" height="36" viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M14 3 C14 3 6 14 6 19 C6 23.5 9.5 27 14 27 C18.5 27 22 23.5 22 19 C22 14 14 3 14 3Z" fill="${color}" opacity="0.4" stroke="${sc}" stroke-width="1.2"/>
|
|
<path d="M14 10 C14 10 10 16 10 19 C10 21.5 11.8 23.5 14 23.5 C16.2 23.5 18 21.5 18 19 C18 16 14 10 14 10Z" fill="${color}" opacity="0.3"/>
|
|
<rect x="24" y="5" width="6" height="3" rx="1" fill="${color}" opacity="0.5" stroke="${sc}" stroke-width="0.8"/>
|
|
<line x1="27" y1="8" x2="27" y2="12" stroke="${sc}" stroke-width="1.5" opacity="0.7"/>
|
|
<path d="M24 8 L24 10 Q24 12 26 12 L28 12 Q30 12 30 10 L30 8" fill="none" stroke="${sc}" stroke-width="0.8" opacity="0.6"/>
|
|
<circle cx="27" cy="14.5" r="1" fill="${color}" opacity="0.55"/>
|
|
<circle cx="27" cy="17" r="0.7" fill="${color}" opacity="0.45"/>
|
|
<rect x="23" y="20" width="9" height="12" rx="1.5" fill="${color}" opacity="0.4" stroke="${sc}" stroke-width="1"/>
|
|
<line x1="24" y1="24" x2="31" y2="24" stroke="${color}" stroke-width="0.6" opacity="0.5"/>
|
|
<line x1="24" y1="27" x2="31" y2="27" stroke="${color}" stroke-width="0.6" opacity="0.5"/>
|
|
<path d="M18 22 L23 22" stroke="${sc}" stroke-width="1" opacity="0.6"/>
|
|
<line x1="27.5" y1="32" x2="27.5" y2="34" stroke="${color}" stroke-width="1" opacity="0.55"/>
|
|
<line x1="4" y1="34" x2="33" y2="34" stroke="${color}" stroke-width="1" opacity="0.25"/>
|
|
${damaged ? damageOverlaySvg() : ''}
|
|
${planned ? plannedOverlaySvg() : ''}
|
|
</svg>`;
|
|
}
|
|
|
|
function buildSvg(type: OilFacilityType, color: string, damaged: boolean, planned: boolean): string {
|
|
switch (type) {
|
|
case 'refinery': return refinerySvg(color, damaged, planned);
|
|
case 'oilfield': return oilfieldSvg(color, damaged, planned);
|
|
case 'gasfield': return gasfieldSvg(color, damaged, planned);
|
|
case 'terminal': return terminalSvg(color, damaged, planned);
|
|
case 'petrochemical': return petrochemSvg(color, damaged, planned);
|
|
case 'desalination': return desalSvg(color, damaged, planned);
|
|
}
|
|
}
|
|
|
|
// ─── Module-level icon cache ─────────────────────────────────────────────────
|
|
|
|
const oilIconCache = new Map<string, string>();
|
|
|
|
function getOilIconUrl(type: OilFacilityType, damaged: boolean, planned: boolean): string {
|
|
const key = `${type}-${damaged ? 'd' : 'n'}-${planned ? 'p' : 'n'}`;
|
|
if (!oilIconCache.has(key)) {
|
|
const color = TYPE_COLORS[type];
|
|
oilIconCache.set(key, svgToDataUri(buildSvg(type, color, damaged, planned)));
|
|
}
|
|
return oilIconCache.get(key)!;
|
|
}
|
|
|
|
// ─── Label color ─────────────────────────────────────────────────────────────
|
|
|
|
function getLabelColor(type: OilFacilityType, damaged: boolean, planned: boolean): [number, number, number, number] {
|
|
if (damaged) return [255, 0, 0, 255];
|
|
if (planned) return [255, 102, 0, 255];
|
|
const hex = TYPE_COLORS[type];
|
|
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, 255];
|
|
}
|
|
|
|
// ─── Public interface ────────────────────────────────────────────────────────
|
|
|
|
export interface OilLayerConfig {
|
|
visible: boolean;
|
|
sc: number;
|
|
currentTime: number;
|
|
onPick: (f: OilFacility) => void;
|
|
}
|
|
|
|
export function createIranOilLayers(config: OilLayerConfig): Layer[] {
|
|
if (!config.visible) return [];
|
|
|
|
const { sc, currentTime, onPick } = config;
|
|
|
|
return [
|
|
new IconLayer<OilFacility>({
|
|
id: 'iran-oil-icon',
|
|
data: iranOilFacilities,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getIcon: (d) => {
|
|
const isDamaged = !!(d.damaged && d.damagedAt && currentTime >= d.damagedAt);
|
|
const isPlanned = !!d.planned && !isDamaged;
|
|
return {
|
|
url: getOilIconUrl(d.type, isDamaged, isPlanned),
|
|
width: 36,
|
|
height: 36,
|
|
anchorX: 18,
|
|
anchorY: 18,
|
|
};
|
|
},
|
|
getSize: 18 * sc,
|
|
updateTriggers: { getSize: [sc], getIcon: [currentTime] },
|
|
pickable: true,
|
|
onClick: (info: PickingInfo<OilFacility>) => {
|
|
if (info.object) onPick(info.object);
|
|
return true;
|
|
},
|
|
}),
|
|
new TextLayer<OilFacility>({
|
|
id: 'iran-oil-label',
|
|
data: iranOilFacilities,
|
|
getPosition: (d) => [d.lng, d.lat],
|
|
getText: (d) => d.nameKo,
|
|
getSize: 9 * sc,
|
|
updateTriggers: { getSize: [sc], getColor: [currentTime] },
|
|
getColor: (d) => {
|
|
const isDamaged = !!(d.damaged && d.damagedAt && currentTime >= d.damagedAt);
|
|
const isPlanned = !!d.planned && !isDamaged;
|
|
return getLabelColor(d.type, isDamaged, isPlanned);
|
|
},
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: 'monospace',
|
|
fontWeight: 700,
|
|
outlineWidth: 2,
|
|
outlineColor: [0, 0, 0, 200],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
}),
|
|
];
|
|
}
|