diff --git a/frontend/src/components/layers/ShipLayer.tsx b/frontend/src/components/layers/ShipLayer.tsx index dd40192..afe2ad0 100644 --- a/frontend/src/components/layers/ShipLayer.tsx +++ b/frontend/src/components/layers/ShipLayer.tsx @@ -1,8 +1,9 @@ import { memo, useMemo, useState, useEffect, useRef, useCallback } from 'react'; import { Marker, Popup, Source, Layer, useMap } from 'react-map-gl/maplibre'; import { useTranslation } from 'react-i18next'; -import type { Ship, ShipCategory, VesselAnalysisDto } from '../../types'; +import type { Ship, VesselAnalysisDto } from '../../types'; import maplibregl from 'maplibre-gl'; +import { MT_TYPE_HEX, getMTType, NAVY_COLORS, FLAG_EMOJI, SIZE_MAP, isMilitary, getShipColor } from '../../utils/shipClassification'; interface Props { ships: Ship[]; @@ -14,100 +15,6 @@ interface Props { analysisMap?: Map; } -// ── MarineTraffic-style vessel type colors (CSS variable references) ── -const MT_TYPE_COLORS: Record = { - cargo: 'var(--kcg-ship-cargo)', - tanker: 'var(--kcg-ship-tanker)', - passenger: 'var(--kcg-ship-passenger)', - fishing: 'var(--kcg-ship-fishing)', - fishing_gear: '#f97316', - pleasure: 'var(--kcg-ship-pleasure)', - military: 'var(--kcg-ship-military)', - tug_special: 'var(--kcg-ship-tug)', - other: 'var(--kcg-ship-other)', - unknown: 'var(--kcg-ship-unknown)', -}; - -// Resolved hex colors for MapLibre paint (which cannot use CSS vars) -const MT_TYPE_HEX: Record = { - cargo: '#f0a830', - tanker: '#e74c3c', - passenger: '#4caf50', - fishing: '#42a5f5', - fishing_gear: '#f97316', - pleasure: '#e91e8c', - military: '#d32f2f', - tug_special: '#2e7d32', - other: '#5c6bc0', - unknown: '#9e9e9e', -}; - -// Map our internal ShipCategory + typecode → MT visual type -function getMTType(ship: Ship): string { - const tc = (ship.typecode || '').toUpperCase(); - const cat = ship.category; - - // Military first - if (cat === 'carrier' || cat === 'destroyer' || cat === 'warship' || cat === 'submarine' || cat === 'patrol') return 'military'; - if (tc === 'DDG' || tc === 'DDH' || tc === 'CVN' || tc === 'FFG' || tc === 'LCS' || tc === 'MCM' || tc === 'PC' || tc === 'LPH') return 'military'; - - // Tanker - if (cat === 'tanker') return 'tanker'; - if (tc === 'VLCC' || tc === 'LNG' || tc === 'LPG') return 'tanker'; - if (tc.startsWith('A1')) return 'tanker'; - - // Cargo - if (cat === 'cargo') return 'cargo'; - if (tc === 'CONT' || tc === 'BULK') return 'cargo'; - if (tc.startsWith('A2') || tc.startsWith('A3')) return 'cargo'; - - // Passenger - if (tc === 'PASS' || tc.startsWith('B')) return 'passenger'; - - // Fishing - if (tc.startsWith('C')) return 'fishing'; - - // Tug / Special - if (tc.startsWith('D') || tc.startsWith('E')) return 'tug_special'; - - // Pleasure - if (tc === 'SAIL' || tc === 'YACHT') return 'pleasure'; - - if (cat === 'civilian') return 'other'; - return 'unknown'; -} - -// Legacy navy flag colors (for popup header accent only) -const NAVY_COLORS: Record = { - US: '#1e90ff', UK: '#e63946', FR: '#ffd60a', KR: '#00e5ff', - IR: '#2ecc40', JP: '#ff6b6b', AU: '#f4a261', DE: '#b5b5b5', IN: '#ff9f43', -}; - -const FLAG_EMOJI: Record = { - US: '\u{1F1FA}\u{1F1F8}', UK: '\u{1F1EC}\u{1F1E7}', FR: '\u{1F1EB}\u{1F1F7}', - KR: '\u{1F1F0}\u{1F1F7}', IR: '\u{1F1EE}\u{1F1F7}', JP: '\u{1F1EF}\u{1F1F5}', - AU: '\u{1F1E6}\u{1F1FA}', DE: '\u{1F1E9}\u{1F1EA}', IN: '\u{1F1EE}\u{1F1F3}', - CN: '\u{1F1E8}\u{1F1F3}', PA: '\u{1F1F5}\u{1F1E6}', LR: '\u{1F1F1}\u{1F1F7}', - MH: '\u{1F1F2}\u{1F1ED}', HK: '\u{1F1ED}\u{1F1F0}', SG: '\u{1F1F8}\u{1F1EC}', - BZ: '\u{1F1E7}\u{1F1FF}', OM: '\u{1F1F4}\u{1F1F2}', AE: '\u{1F1E6}\u{1F1EA}', - SA: '\u{1F1F8}\u{1F1E6}', BH: '\u{1F1E7}\u{1F1ED}', QA: '\u{1F1F6}\u{1F1E6}', -}; - -// icon-size multiplier (symbol layer, base=64px) -const SIZE_MAP: Record = { - carrier: 0.32, destroyer: 0.22, warship: 0.22, submarine: 0.18, patrol: 0.16, - tanker: 0.16, cargo: 0.16, fishing: 0.14, civilian: 0.14, unknown: 0.12, -}; - -const MIL_CATEGORIES: ShipCategory[] = ['carrier', 'destroyer', 'warship', 'submarine', 'patrol']; - -function isMilitary(category: ShipCategory): boolean { - return MIL_CATEGORIES.includes(category); -} - -function getShipColor(ship: Ship): string { - return MT_TYPE_COLORS[getMTType(ship)] || MT_TYPE_COLORS.unknown; -} function getShipHex(ship: Ship): string { return MT_TYPE_HEX[getMTType(ship)] || MT_TYPE_HEX.unknown; diff --git a/frontend/src/utils/shipClassification.ts b/frontend/src/utils/shipClassification.ts new file mode 100644 index 0000000..f154020 --- /dev/null +++ b/frontend/src/utils/shipClassification.ts @@ -0,0 +1,84 @@ +import type { Ship, ShipCategory } from '../types'; + +// ── MarineTraffic-style vessel type colors (CSS variable references) ── +export const MT_TYPE_COLORS: Record = { + cargo: 'var(--kcg-ship-cargo)', + tanker: 'var(--kcg-ship-tanker)', + passenger: 'var(--kcg-ship-passenger)', + fishing: 'var(--kcg-ship-fishing)', + fishing_gear: '#f97316', + pleasure: 'var(--kcg-ship-pleasure)', + military: 'var(--kcg-ship-military)', + tug_special: 'var(--kcg-ship-tug)', + other: 'var(--kcg-ship-other)', + unknown: 'var(--kcg-ship-unknown)', +}; + +// Resolved hex colors for MapLibre paint (which cannot use CSS vars) +export const MT_TYPE_HEX: Record = { + cargo: '#f0a830', + tanker: '#e74c3c', + passenger: '#4caf50', + fishing: '#42a5f5', + fishing_gear: '#f97316', + pleasure: '#e91e8c', + military: '#d32f2f', + tug_special: '#2e7d32', + other: '#5c6bc0', + unknown: '#9e9e9e', +}; + +export function getMTType(ship: Ship): string { + const tc = (ship.typecode || '').toUpperCase(); + const cat = ship.category; + + if (cat === 'carrier' || cat === 'destroyer' || cat === 'warship' || cat === 'submarine' || cat === 'patrol') return 'military'; + if (tc === 'DDG' || tc === 'DDH' || tc === 'CVN' || tc === 'FFG' || tc === 'LCS' || tc === 'MCM' || tc === 'PC' || tc === 'LPH') return 'military'; + + if (cat === 'tanker') return 'tanker'; + if (tc === 'VLCC' || tc === 'LNG' || tc === 'LPG') return 'tanker'; + if (tc.startsWith('A1')) return 'tanker'; + + if (cat === 'cargo') return 'cargo'; + if (tc === 'CONT' || tc === 'BULK') return 'cargo'; + if (tc.startsWith('A2') || tc.startsWith('A3')) return 'cargo'; + + if (tc === 'PASS' || tc.startsWith('B')) return 'passenger'; + if (tc.startsWith('C')) return 'fishing'; + if (tc.startsWith('D') || tc.startsWith('E')) return 'tug_special'; + if (tc === 'SAIL' || tc === 'YACHT') return 'pleasure'; + + if (cat === 'civilian') return 'other'; + return 'unknown'; +} + +// Legacy navy flag colors (for popup header accent only) +export const NAVY_COLORS: Record = { + US: '#1e90ff', UK: '#e63946', FR: '#ffd60a', KR: '#00e5ff', + IR: '#2ecc40', JP: '#ff6b6b', AU: '#f4a261', DE: '#b5b5b5', IN: '#ff9f43', +}; + +export const FLAG_EMOJI: Record = { + US: '\u{1F1FA}\u{1F1F8}', UK: '\u{1F1EC}\u{1F1E7}', FR: '\u{1F1EB}\u{1F1F7}', + KR: '\u{1F1F0}\u{1F1F7}', IR: '\u{1F1EE}\u{1F1F7}', JP: '\u{1F1EF}\u{1F1F5}', + AU: '\u{1F1E6}\u{1F1FA}', DE: '\u{1F1E9}\u{1F1EA}', IN: '\u{1F1EE}\u{1F1F3}', + CN: '\u{1F1E8}\u{1F1F3}', PA: '\u{1F1F5}\u{1F1E6}', LR: '\u{1F1F1}\u{1F1F7}', + MH: '\u{1F1F2}\u{1F1ED}', HK: '\u{1F1ED}\u{1F1F0}', SG: '\u{1F1F8}\u{1F1EC}', + BZ: '\u{1F1E7}\u{1F1FF}', OM: '\u{1F1F4}\u{1F1F2}', AE: '\u{1F1E6}\u{1F1EA}', + SA: '\u{1F1F8}\u{1F1E6}', BH: '\u{1F1E7}\u{1F1ED}', QA: '\u{1F1F6}\u{1F1E6}', +}; + +export const SIZE_MAP: Record = { + carrier: 0.32, destroyer: 0.22, warship: 0.22, submarine: 0.18, patrol: 0.16, + tanker: 0.16, cargo: 0.16, fishing: 0.14, civilian: 0.14, unknown: 0.12, +}; + +export const MIL_CATEGORIES: ShipCategory[] = ['carrier', 'destroyer', 'warship', 'submarine', 'patrol']; + +export function isMilitary(category: ShipCategory): boolean { + return MIL_CATEGORIES.includes(category); +} + +export function getShipColor(ship: Ship): string { + return MT_TYPE_COLORS[getMTType(ship)] || MT_TYPE_COLORS.unknown; +}