- 서브클러스터별 독립 폴리곤/센터/center trail 렌더링 - 반경 밖 이탈 선박 강제 감쇠 (OUT_OF_RANGE) - Backend correlation API에 sub_cluster_id 추가 - 모델 패널 5개 항상 표시, 드롭다운 기본값 70% - DISPLAY_STALE_SEC (time_bucket 기반) 폴리곤 노출 필터 - AIS 수집 bbox 122~132E/31~39N 확장 - historyActive 시 deck.gl 이중 렌더링 수정 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
553 lines
21 KiB
TypeScript
553 lines
21 KiB
TypeScript
import { useMemo } from 'react';
|
|
import type { Layer } from '@deck.gl/core';
|
|
import { GeoJsonLayer, IconLayer, ScatterplotLayer, TextLayer } from '@deck.gl/layers';
|
|
import { SHIP_ICON_MAPPING } from '../utils/shipIconSvg';
|
|
import { MODEL_ORDER, MODEL_COLORS } from '../components/korea/fleetClusterConstants';
|
|
import type { FleetClusterGeoJsonResult } from '../components/korea/useFleetClusterGeoJson';
|
|
import { FONT_MONO } from '../styles/fonts';
|
|
import { clusterLabels } from '../utils/labelCluster';
|
|
|
|
// ── Config ────────────────────────────────────────────────────────────────────
|
|
|
|
export interface FleetClusterDeckConfig {
|
|
selectedGearGroup: string | null;
|
|
hoveredMmsi: string | null;
|
|
hoveredGearGroup: string | null; // gear polygon hover highlight
|
|
enabledModels: Set<string>;
|
|
historyActive: boolean;
|
|
hasCorrelationTracks: boolean;
|
|
zoomScale: number;
|
|
zoomLevel: number; // integer zoom for label clustering
|
|
fontScale?: number; // fontScale.analysis (default 1)
|
|
focusMode?: boolean; // 집중 모드 — 라이브 폴리곤/마커 숨김
|
|
onPolygonClick?: (features: PickedPolygonFeature[], coordinate: [number, number]) => void;
|
|
onPolygonHover?: (info: { lng: number; lat: number; type: 'fleet' | 'gear'; id: string | number } | null) => void;
|
|
}
|
|
|
|
export interface PickedPolygonFeature {
|
|
type: 'fleet' | 'gear';
|
|
clusterId?: number;
|
|
name?: string;
|
|
gearCount?: number;
|
|
inZone?: boolean;
|
|
}
|
|
|
|
// ── Hex → RGBA (module-level cache) ──────────────────────────────────────────
|
|
|
|
const hexRgbaCache = new Map<string, [number, number, number, number]>();
|
|
|
|
function hexToRgba(hex: string, alpha = 255): [number, number, number, number] {
|
|
const cacheKey = `${hex}-${alpha}`;
|
|
const cached = hexRgbaCache.get(cacheKey);
|
|
if (cached) return cached;
|
|
const h = hex.replace('#', '');
|
|
let r = parseInt(h.substring(0, 2), 16) || 0;
|
|
let g = parseInt(h.substring(2, 4), 16) || 0;
|
|
let b = parseInt(h.substring(4, 6), 16) || 0;
|
|
// 어두운 색상 밝기 보정 (바다 배경 대비)
|
|
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
if (lum < 0.3) {
|
|
const boost = 0.3 / Math.max(lum, 0.01);
|
|
r = Math.min(255, Math.round(r * boost));
|
|
g = Math.min(255, Math.round(g * boost));
|
|
b = Math.min(255, Math.round(b * boost));
|
|
}
|
|
const rgba: [number, number, number, number] = [r, g, b, alpha];
|
|
hexRgbaCache.set(cacheKey, rgba);
|
|
return rgba;
|
|
}
|
|
|
|
// ── Gear cluster color helpers ────────────────────────────────────────────────
|
|
|
|
const GEAR_IN_ZONE_FILL: [number, number, number, number] = [220, 38, 38, 25]; // #dc2626 opacity 0.10
|
|
const GEAR_IN_ZONE_LINE: [number, number, number, number] = [220, 38, 38, 200]; // #dc2626
|
|
const GEAR_OUT_ZONE_FILL: [number, number, number, number] = [249, 115, 22, 25]; // #f97316 opacity 0.10
|
|
const GEAR_OUT_ZONE_LINE: [number, number, number, number] = [249, 115, 22, 200]; // #f97316
|
|
|
|
const ICON_PX = 64;
|
|
|
|
// ── Point-in-polygon (ray casting) ──────────────────────────────────────────
|
|
|
|
function pointInRing(point: [number, number], ring: number[][]): boolean {
|
|
const [px, py] = point;
|
|
let inside = false;
|
|
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
|
const xi = ring[i][0], yi = ring[i][1];
|
|
const xj = ring[j][0], yj = ring[j][1];
|
|
if ((yi > py) !== (yj > py) && px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
|
|
inside = !inside;
|
|
}
|
|
}
|
|
return inside;
|
|
}
|
|
|
|
function pointInPolygon(point: [number, number], geometry: GeoJSON.Geometry): boolean {
|
|
if (geometry.type === 'Polygon') {
|
|
return pointInRing(point, geometry.coordinates[0]);
|
|
}
|
|
if (geometry.type === 'MultiPolygon') {
|
|
return geometry.coordinates.some(poly => pointInRing(point, poly[0]));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Find all fleet/gear polygons at a given coordinate */
|
|
function findPolygonsAtPoint(
|
|
point: [number, number],
|
|
fleetFc: GeoJSON.FeatureCollection,
|
|
gearFc: GeoJSON.FeatureCollection,
|
|
): PickedPolygonFeature[] {
|
|
const results: PickedPolygonFeature[] = [];
|
|
for (const f of fleetFc.features) {
|
|
if (pointInPolygon(point, f.geometry)) {
|
|
results.push({
|
|
type: 'fleet',
|
|
clusterId: f.properties?.clusterId,
|
|
name: f.properties?.name,
|
|
});
|
|
}
|
|
}
|
|
for (const f of gearFc.features) {
|
|
if (pointInPolygon(point, f.geometry)) {
|
|
results.push({
|
|
type: 'gear',
|
|
name: f.properties?.name,
|
|
gearCount: f.properties?.gearCount,
|
|
inZone: f.properties?.inZone === 1,
|
|
});
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
// ── Hook ──────────────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Converts FleetClusterGeoJsonResult (produced by useFleetClusterGeoJson) into
|
|
* deck.gl Layer instances.
|
|
*
|
|
* Uses useMemo — fleet data changes infrequently (every 5 minutes) and on user
|
|
* interaction (hover, select). No Zustand subscribe pattern needed.
|
|
*/
|
|
export function useFleetClusterDeckLayers(
|
|
geo: FleetClusterGeoJsonResult | null,
|
|
config: FleetClusterDeckConfig,
|
|
): Layer[] {
|
|
const {
|
|
selectedGearGroup,
|
|
hoveredMmsi,
|
|
hoveredGearGroup,
|
|
enabledModels,
|
|
historyActive,
|
|
zoomScale,
|
|
zoomLevel,
|
|
fontScale: fs = 1,
|
|
onPolygonClick,
|
|
onPolygonHover,
|
|
} = config;
|
|
|
|
const focusMode = config.focusMode ?? false;
|
|
|
|
return useMemo((): Layer[] => {
|
|
if (!geo || focusMode) return [];
|
|
|
|
const layers: Layer[] = [];
|
|
|
|
// ── 1. Fleet polygons (fleetPolygonGeoJSON) ──────────────────────────────
|
|
const fleetPoly = geo.fleetPolygonGeoJSON as GeoJSON.FeatureCollection;
|
|
if (fleetPoly.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'fleet-polygons',
|
|
data: fleetPoly,
|
|
getFillColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#63b3ed', 25),
|
|
getLineColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#63b3ed', 128),
|
|
getLineWidth: 1.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: true,
|
|
onHover: (info) => {
|
|
if (info.object) {
|
|
const f = info.object as GeoJSON.Feature;
|
|
const cid = f.properties?.clusterId;
|
|
if (cid != null) {
|
|
onPolygonHover?.({ lng: info.coordinate![0], lat: info.coordinate![1], type: 'fleet', id: cid });
|
|
}
|
|
} else {
|
|
onPolygonHover?.(null);
|
|
}
|
|
},
|
|
onClick: (info) => {
|
|
if (!info.object || !info.coordinate || !onPolygonClick) return;
|
|
const pt: [number, number] = [info.coordinate[0], info.coordinate[1]];
|
|
onPolygonClick(findPolygonsAtPoint(pt, fleetPoly, gearFc), pt);
|
|
},
|
|
updateTriggers: {},
|
|
}));
|
|
}
|
|
|
|
// ── 2. Hovered fleet highlight (hoveredGeoJSON) ──────────────────────────
|
|
const hoveredPoly = geo.hoveredGeoJSON as GeoJSON.FeatureCollection;
|
|
if (hoveredPoly.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'fleet-hover-highlight',
|
|
data: hoveredPoly,
|
|
getFillColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#63b3ed', 64),
|
|
getLineColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#63b3ed', 200),
|
|
getLineWidth: 2,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
// ── 3. Fleet 2-ship lines (lineGeoJSON) ──────────────────────────────────
|
|
// Currently always empty (server handles 2-ship fleets as Polygon), kept for future
|
|
const lineFc = geo.lineGeoJSON as GeoJSON.FeatureCollection;
|
|
if (lineFc.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'fleet-lines',
|
|
data: lineFc,
|
|
getLineColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#63b3ed', 180),
|
|
getLineWidth: 1.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: false,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
// ── 4. Gear cluster polygons (gearClusterGeoJson) ────────────────────────
|
|
const gearFc = geo.gearClusterGeoJson as GeoJSON.FeatureCollection;
|
|
if (gearFc.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'gear-cluster-polygons',
|
|
data: gearFc,
|
|
getFillColor: (f: GeoJSON.Feature) =>
|
|
f.properties?.inZone === 1 ? GEAR_IN_ZONE_FILL : GEAR_OUT_ZONE_FILL,
|
|
getLineColor: (f: GeoJSON.Feature) =>
|
|
f.properties?.inZone === 1 ? GEAR_IN_ZONE_LINE : GEAR_OUT_ZONE_LINE,
|
|
getLineWidth: 1.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: true,
|
|
onHover: (info) => {
|
|
if (info.object) {
|
|
const f = info.object as GeoJSON.Feature;
|
|
const name = f.properties?.name;
|
|
if (name) {
|
|
onPolygonHover?.({ lng: info.coordinate![0], lat: info.coordinate![1], type: 'gear', id: name });
|
|
}
|
|
} else {
|
|
onPolygonHover?.(null);
|
|
}
|
|
},
|
|
onClick: (info) => {
|
|
if (!info.object || !info.coordinate || !onPolygonClick) return;
|
|
const pt: [number, number] = [info.coordinate[0], info.coordinate[1]];
|
|
onPolygonClick(findPolygonsAtPoint(pt, fleetPoly, gearFc), pt);
|
|
},
|
|
}));
|
|
}
|
|
|
|
// ── 4b. Gear hover highlight ──────────────────────────────────────────
|
|
if (hoveredGearGroup && gearFc.features.length > 0) {
|
|
const hoveredGearFeatures = gearFc.features.filter(
|
|
f => f.properties?.name === hoveredGearGroup,
|
|
);
|
|
if (hoveredGearFeatures.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'gear-hover-highlight',
|
|
data: { type: 'FeatureCollection' as const, features: hoveredGearFeatures },
|
|
getFillColor: (f: GeoJSON.Feature) =>
|
|
f.properties?.inZone === 1 ? [220, 38, 38, 64] : [249, 115, 22, 64],
|
|
getLineColor: (f: GeoJSON.Feature) =>
|
|
f.properties?.inZone === 1 ? [220, 38, 38, 255] : [249, 115, 22, 255],
|
|
getLineWidth: 2.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
}
|
|
|
|
// ── 5. Selected gear highlight (selectedGearHighlightGeoJson) ────────────
|
|
if (geo.selectedGearHighlightGeoJson && geo.selectedGearHighlightGeoJson.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'gear-selected-highlight',
|
|
data: geo.selectedGearHighlightGeoJson,
|
|
getFillColor: [249, 115, 22, 40],
|
|
getLineColor: [249, 115, 22, 230],
|
|
getLineWidth: 2,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
// ── 6. Member markers (memberMarkersGeoJson) — skip when historyActive ───
|
|
if (!historyActive) {
|
|
const memberFc = geo.memberMarkersGeoJson as GeoJSON.FeatureCollection;
|
|
if (memberFc.features.length > 0) {
|
|
layers.push(new IconLayer<GeoJSON.Feature>({
|
|
id: 'fleet-member-icons',
|
|
data: memberFc.features,
|
|
getPosition: (f: GeoJSON.Feature) =>
|
|
(f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
getIcon: (f: GeoJSON.Feature) =>
|
|
f.properties?.isGear === 1
|
|
? SHIP_ICON_MAPPING['gear-diamond']
|
|
: SHIP_ICON_MAPPING['ship-triangle'],
|
|
getSize: (f: GeoJSON.Feature) =>
|
|
(f.properties?.baseSize ?? 0.14) * zoomScale * ICON_PX,
|
|
getAngle: (f: GeoJSON.Feature) =>
|
|
f.properties?.isGear === 1 ? 0 : -(f.properties?.cog ?? 0),
|
|
getColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#9e9e9e'),
|
|
sizeUnits: 'pixels',
|
|
sizeMinPixels: 3,
|
|
billboard: false,
|
|
pickable: false,
|
|
updateTriggers: {
|
|
getSize: [zoomScale, fs],
|
|
},
|
|
}));
|
|
|
|
const clusteredMembers = clusterLabels(
|
|
memberFc.features,
|
|
f => (f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
zoomLevel,
|
|
);
|
|
layers.push(new TextLayer<GeoJSON.Feature>({
|
|
id: 'fleet-member-labels',
|
|
data: clusteredMembers,
|
|
getPosition: (f: GeoJSON.Feature) =>
|
|
(f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
getText: (f: GeoJSON.Feature) => {
|
|
const isParent = f.properties?.isParent === 1;
|
|
return isParent ? `\u2605 ${f.properties?.name ?? ''}` : (f.properties?.name ?? '');
|
|
},
|
|
getSize: 8 * zoomScale * fs,
|
|
getColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#e2e8f0'),
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: FONT_MONO,
|
|
background: true,
|
|
getBackgroundColor: [0, 0, 0, 200],
|
|
backgroundPadding: [3, 1],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
updateTriggers: {
|
|
getSize: [zoomScale, fs],
|
|
},
|
|
}));
|
|
}
|
|
}
|
|
|
|
// ── 7. Picker highlight (pickerHighlightGeoJson) ──────────────────────────
|
|
const pickerFc = geo.pickerHighlightGeoJson as GeoJSON.FeatureCollection;
|
|
if (pickerFc.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'fleet-picker-highlight',
|
|
data: pickerFc,
|
|
getFillColor: [255, 255, 255, 25],
|
|
getLineColor: [255, 255, 255, 200],
|
|
getLineWidth: 2,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
// ── Correlation layers (only when gear group selected, skip during replay) ─
|
|
if (selectedGearGroup && !historyActive) {
|
|
|
|
// ── 8. Operational polygons (per model) ────────────────────────────────
|
|
for (const op of geo.operationalPolygons) {
|
|
if (!enabledModels.has(op.modelName)) continue;
|
|
if (op.geojson.features.length === 0) continue;
|
|
const modelColor = MODEL_COLORS[op.modelName] ?? '#94a3b8';
|
|
layers.push(new GeoJsonLayer({
|
|
id: `fleet-op-polygon-${op.modelName}`,
|
|
data: op.geojson,
|
|
getFillColor: hexToRgba(modelColor, 30),
|
|
getLineColor: hexToRgba(modelColor, 180),
|
|
getLineWidth: 1.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: true,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
// ── 9. Correlation trails (correlationTrailGeoJson) ────────────────────
|
|
const trailFc = geo.correlationTrailGeoJson as GeoJSON.FeatureCollection;
|
|
if (trailFc.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'fleet-correlation-trails',
|
|
data: trailFc,
|
|
getLineColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#60a5fa', 160),
|
|
getLineWidth: 1.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: false,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
// ── 10. Correlation vessels (correlationVesselGeoJson) ─────────────────
|
|
const corrVesselFc = geo.correlationVesselGeoJson as GeoJSON.FeatureCollection;
|
|
if (corrVesselFc.features.length > 0) {
|
|
layers.push(new IconLayer<GeoJSON.Feature>({
|
|
id: 'fleet-correlation-vessel-icons',
|
|
data: corrVesselFc.features,
|
|
getPosition: (f: GeoJSON.Feature) =>
|
|
(f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
getIcon: () => SHIP_ICON_MAPPING['ship-triangle'],
|
|
getSize: () =>
|
|
0.14 * zoomScale * ICON_PX,
|
|
getAngle: (f: GeoJSON.Feature) => -(f.properties?.cog ?? 0),
|
|
getColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#60a5fa'),
|
|
sizeUnits: 'pixels',
|
|
sizeMinPixels: 3,
|
|
billboard: false,
|
|
pickable: false,
|
|
updateTriggers: {
|
|
getSize: [zoomScale, fs],
|
|
},
|
|
}));
|
|
|
|
const clusteredCorr = clusterLabels(
|
|
corrVesselFc.features,
|
|
f => (f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
zoomLevel,
|
|
);
|
|
layers.push(new TextLayer<GeoJSON.Feature>({
|
|
id: 'fleet-correlation-vessel-labels',
|
|
data: clusteredCorr,
|
|
getPosition: (f: GeoJSON.Feature) =>
|
|
(f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
getText: (f: GeoJSON.Feature) => f.properties?.name ?? '',
|
|
getSize: 8 * zoomScale * fs,
|
|
getColor: (f: GeoJSON.Feature) =>
|
|
hexToRgba(f.properties?.color ?? '#60a5fa'),
|
|
getTextAnchor: 'middle',
|
|
getAlignmentBaseline: 'top',
|
|
getPixelOffset: [0, 10],
|
|
fontFamily: FONT_MONO,
|
|
background: true,
|
|
getBackgroundColor: [0, 0, 0, 200],
|
|
backgroundPadding: [3, 1],
|
|
billboard: false,
|
|
characterSet: 'auto',
|
|
updateTriggers: {
|
|
getSize: [zoomScale, fs],
|
|
},
|
|
}));
|
|
}
|
|
|
|
// ── 11. Model badges (modelBadgesGeoJson) ─────────────────────────────
|
|
// Rendered as small ScatterplotLayer dots, one layer per active model.
|
|
// Position is offset in world coordinates (small lng offset per model index).
|
|
// Badge size is intentionally small (4px) as visual indicators only.
|
|
const badgeFc = geo.modelBadgesGeoJson as GeoJSON.FeatureCollection;
|
|
if (badgeFc.features.length > 0) {
|
|
MODEL_ORDER.forEach((modelName, i) => {
|
|
if (!enabledModels.has(modelName)) return;
|
|
const modelColor = MODEL_COLORS[modelName] ?? '#94a3b8';
|
|
const activeFeatures = badgeFc.features.filter(
|
|
(f) => f.properties?.[`m${i}`] === 1,
|
|
);
|
|
if (activeFeatures.length === 0) return;
|
|
|
|
// Small lng offset per model index to avoid overlap (≈ 300m at z10)
|
|
const lngOffset = i * 0.003;
|
|
|
|
layers.push(new ScatterplotLayer<GeoJSON.Feature>({
|
|
id: `fleet-model-badge-${modelName}`,
|
|
data: activeFeatures,
|
|
getPosition: (f: GeoJSON.Feature) => {
|
|
const [lng, lat] = (f.geometry as GeoJSON.Point).coordinates;
|
|
return [lng + lngOffset, lat] as [number, number];
|
|
},
|
|
getRadius: 4,
|
|
getFillColor: hexToRgba(modelColor, 230),
|
|
getLineColor: [0, 0, 0, 200],
|
|
getLineWidth: 1,
|
|
stroked: true,
|
|
filled: true,
|
|
radiusUnits: 'pixels',
|
|
lineWidthUnits: 'pixels',
|
|
pickable: false,
|
|
}));
|
|
});
|
|
}
|
|
|
|
// ── 12. Hover highlight (hoverHighlightGeoJson + trail) ───────────────
|
|
if (hoveredMmsi) {
|
|
const hoverFc = geo.hoverHighlightGeoJson as GeoJSON.FeatureCollection;
|
|
if (hoverFc.features.length > 0) {
|
|
layers.push(new ScatterplotLayer<GeoJSON.Feature>({
|
|
id: 'fleet-hover-ring',
|
|
data: hoverFc.features,
|
|
getPosition: (f: GeoJSON.Feature) =>
|
|
(f.geometry as GeoJSON.Point).coordinates as [number, number],
|
|
getRadius: 18,
|
|
getFillColor: [255, 255, 255, 20],
|
|
getLineColor: [255, 255, 255, 200],
|
|
getLineWidth: 2,
|
|
stroked: true,
|
|
filled: true,
|
|
radiusUnits: 'pixels',
|
|
lineWidthUnits: 'pixels',
|
|
pickable: false,
|
|
}));
|
|
}
|
|
|
|
const hoverTrailFc = geo.hoverHighlightTrailGeoJson as GeoJSON.FeatureCollection;
|
|
if (hoverTrailFc.features.length > 0) {
|
|
layers.push(new GeoJsonLayer({
|
|
id: 'fleet-hover-trail',
|
|
data: hoverTrailFc,
|
|
getLineColor: [255, 255, 255, 150],
|
|
getLineWidth: 1.5,
|
|
lineWidthUnits: 'pixels',
|
|
filled: false,
|
|
stroked: true,
|
|
pickable: false,
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
|
|
return layers;
|
|
}, [
|
|
geo,
|
|
selectedGearGroup,
|
|
hoveredMmsi,
|
|
hoveredGearGroup,
|
|
enabledModels,
|
|
historyActive,
|
|
zoomScale,
|
|
zoomLevel,
|
|
fs,
|
|
focusMode,
|
|
onPolygonClick,
|
|
onPolygonHover,
|
|
]);
|
|
}
|