feat(map): 선박 외곽선 대비 및 줌 스케일링 개선

This commit is contained in:
htlee 2026-02-16 01:10:45 +09:00
부모 864fc44d0e
커밋 3ba6c02ba0
6개의 변경된 파일54개의 추가작업 그리고 37개의 파일을 삭제

파일 보기

@ -115,8 +115,8 @@ export function DashboardPage() {
fcLines: true, fcLines: true,
zones: true, zones: true,
fleetCircles: true, fleetCircles: true,
predictVectors: false, predictVectors: true,
shipLabels: false, shipLabels: true,
}); });
const [fleetRelationSortMode, setFleetRelationSortMode] = useState<FleetRelationSortMode>("count"); const [fleetRelationSortMode, setFleetRelationSortMode] = useState<FleetRelationSortMode>("count");

파일 보기

@ -47,6 +47,14 @@ export const MAP_SELECTED_SHIP_RGB: [number, number, number] = [34, 211, 238];
export const MAP_HIGHLIGHT_SHIP_RGB: [number, number, number] = [245, 158, 11]; export const MAP_HIGHLIGHT_SHIP_RGB: [number, number, number] = [245, 158, 11];
export const MAP_DEFAULT_SHIP_RGB: [number, number, number] = [100, 116, 139]; export const MAP_DEFAULT_SHIP_RGB: [number, number, number] = [100, 116, 139];
// ── Ship outline / halo contrast colors ──
export const HALO_OUTLINE_COLOR: [number, number, number, number] = [210, 225, 240, 155];
export const HALO_OUTLINE_COLOR_SELECTED: [number, number, number, number] = [14, 234, 255, 230];
export const HALO_OUTLINE_COLOR_HIGHLIGHTED: [number, number, number, number] = [245, 158, 11, 210];
export const GLOBE_OUTLINE_PERMITTED = 'rgba(210,225,240,0.62)';
export const GLOBE_OUTLINE_OTHER = 'rgba(160,175,195,0.35)';
// ── Flat map icon sizes ── // ── Flat map icon sizes ──
export const FLAT_SHIP_ICON_SIZE = 19; export const FLAT_SHIP_ICON_SIZE = 19;
@ -152,7 +160,7 @@ export const FLEET_LINE_ML_HL = rgbaCss(OVERLAY_FLEET_RANGE_RGB, 0.95);
// ── Bathymetry zoom ranges ── // ── Bathymetry zoom ranges ──
export const BATHY_ZOOM_RANGES: BathyZoomRange[] = [ export const BATHY_ZOOM_RANGES: BathyZoomRange[] = [
{ id: 'bathymetry-fill', mercator: [6, 24], globe: [8, 24] }, { id: 'bathymetry-fill', mercator: [5, 24], globe: [7, 24] },
{ id: 'bathymetry-borders', mercator: [6, 24], globe: [8, 24] }, { id: 'bathymetry-borders', mercator: [5, 24], globe: [7, 24] },
{ id: 'bathymetry-borders-major', mercator: [4, 24], globe: [8, 24] }, { id: 'bathymetry-borders-major', mercator: [3, 24], globe: [7, 24] },
]; ];

파일 보기

@ -20,7 +20,9 @@ import {
EMPTY_MMSI_SET, EMPTY_MMSI_SET,
DEPTH_DISABLED_PARAMS, DEPTH_DISABLED_PARAMS,
GLOBE_OVERLAY_PARAMS, GLOBE_OVERLAY_PARAMS,
LEGACY_CODE_COLORS, HALO_OUTLINE_COLOR,
HALO_OUTLINE_COLOR_SELECTED,
HALO_OUTLINE_COLOR_HIGHLIGHTED,
PAIR_RANGE_NORMAL_DECK, PAIR_RANGE_NORMAL_DECK,
PAIR_RANGE_WARN_DECK, PAIR_RANGE_WARN_DECK,
PAIR_LINE_NORMAL_DECK, PAIR_LINE_NORMAL_DECK,
@ -426,12 +428,7 @@ export function useDeckLayers(
getRadius: () => FLAT_LEGACY_HALO_RADIUS, getRadius: () => FLAT_LEGACY_HALO_RADIUS,
lineWidthUnits: 'pixels', lineWidthUnits: 'pixels',
getLineWidth: () => 2, getLineWidth: () => 2,
getLineColor: (d) => { getLineColor: () => HALO_OUTLINE_COLOR,
const l = legacyHits?.get(d.mmsi);
const rgb = l ? LEGACY_CODE_COLORS[l.shipCode] : null;
if (!rgb) return [245, 158, 11, 200];
return [rgb[0], rgb[1], rgb[2], 200];
},
getPosition: (d) => [d.lon, d.lat] as [number, number], getPosition: (d) => [d.lon, d.lat] as [number, number],
}), }),
); );
@ -476,7 +473,7 @@ export function useDeckLayers(
} }
if (settings.showShips && legacyOverlayTargets.length > 0) { if (settings.showShips && legacyOverlayTargets.length > 0) {
layers.push(new ScatterplotLayer<AisTarget>({ id: 'legacy-halo-overlay', data: legacyOverlayTargets, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 2.5 : 2.2), getLineColor: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return [14, 234, 255, 230]; const l = legacyHits?.get(d.mmsi); const rgb = l ? LEGACY_CODE_COLORS[l.shipCode] : null; if (!rgb) return [245, 158, 11, 210]; return [rgb[0], rgb[1], rgb[2], 210]; }, getPosition: (d) => [d.lon, d.lat] as [number, number] })); layers.push(new ScatterplotLayer<AisTarget>({ id: 'legacy-halo-overlay', data: legacyOverlayTargets, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS_HIGHLIGHTED; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 2.5 : 2.2), getLineColor: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return HALO_OUTLINE_COLOR_SELECTED; if (shipHighlightSet.has(d.mmsi)) return HALO_OUTLINE_COLOR_HIGHLIGHTED; return HALO_OUTLINE_COLOR; }, getPosition: (d) => [d.lon, d.lat] as [number, number] }));
} }
if (settings.showShips && shipOverlayLayerData.filter((t) => legacyHits?.has(t.mmsi)).length > 0) { if (settings.showShips && shipOverlayLayerData.filter((t) => legacyHits?.has(t.mmsi)).length > 0) {
@ -621,7 +618,7 @@ export function useDeckLayers(
} }
if (settings.showShips && legacyTargetsOrdered.length > 0) { if (settings.showShips && legacyTargetsOrdered.length > 0) {
globeLayers.push(new ScatterplotLayer<AisTarget>({ id: 'legacy-halo-globe', data: legacyTargetsOrdered, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 2.5 : 2), getLineColor: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return [14, 234, 255, 230]; const l = legacyHits?.get(d.mmsi); const rgb = l ? LEGACY_CODE_COLORS[l.shipCode] : null; if (!rgb) return [245, 158, 11, 200]; return [rgb[0], rgb[1], rgb[2], 200]; }, getPosition: (d) => [d.lon, d.lat] as [number, number] })); globeLayers.push(new ScatterplotLayer<AisTarget>({ id: 'legacy-halo-globe', data: legacyTargetsOrdered, pickable: false, billboard: false, parameters: overlayParams, filled: false, stroked: true, radiusUnits: 'pixels', getRadius: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return FLAT_LEGACY_HALO_RADIUS_SELECTED; return FLAT_LEGACY_HALO_RADIUS; }, lineWidthUnits: 'pixels', getLineWidth: (d) => (selectedMmsi && d.mmsi === selectedMmsi ? 2.5 : 2), getLineColor: (d) => { if (selectedMmsi && d.mmsi === selectedMmsi) return HALO_OUTLINE_COLOR_SELECTED; return HALO_OUTLINE_COLOR; }, getPosition: (d) => [d.lon, d.lat] as [number, number] }));
} }
const normalizedLayers = sanitizeDeckLayerList(globeLayers); const normalizedLayers = sanitizeDeckLayerList(globeLayers);

파일 보기

@ -8,6 +8,8 @@ import type { Map3DSettings, MapProjectionId } from '../types';
import { import {
ANCHORED_SHIP_ICON_ID, ANCHORED_SHIP_ICON_ID,
GLOBE_ICON_HEADING_OFFSET_DEG, GLOBE_ICON_HEADING_OFFSET_DEG,
GLOBE_OUTLINE_PERMITTED,
GLOBE_OUTLINE_OTHER,
DEG2RAD, DEG2RAD,
} from '../constants'; } from '../constants';
import { isFiniteNumber } from '../lib/setUtils'; import { isFiniteNumber } from '../lib/setUtils';
@ -351,8 +353,9 @@ export function useGlobeShips(
const iconScale = selected ? selectedScale : highlightScale; const iconScale = selected ? selectedScale : highlightScale;
const iconSize3 = clampNumber(0.35 * sizeScale * selectedScale, 0.25, 1.3); const iconSize3 = clampNumber(0.35 * sizeScale * selectedScale, 0.25, 1.3);
const iconSize7 = clampNumber(0.45 * sizeScale * selectedScale, 0.3, 1.45); const iconSize7 = clampNumber(0.45 * sizeScale * selectedScale, 0.3, 1.45);
const iconSize10 = clampNumber(0.56 * sizeScale * selectedScale, 0.35, 1.7); const iconSize10 = clampNumber(0.58 * sizeScale * selectedScale, 0.35, 1.8);
const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1); const iconSize14 = clampNumber(0.85 * sizeScale * selectedScale, 0.45, 2.6);
const iconSize18 = clampNumber(2.5 * sizeScale * selectedScale, 1.0, 6.0);
return { return {
type: 'Feature', type: 'Feature',
...(isFiniteNumber(t.mmsi) ? { id: Math.trunc(t.mmsi) } : {}), ...(isFiniteNumber(t.mmsi) ? { id: Math.trunc(t.mmsi) } : {}),
@ -373,6 +376,7 @@ export function useGlobeShips(
iconSize7: iconSize7 * iconScale, iconSize7: iconSize7 * iconScale,
iconSize10: iconSize10 * iconScale, iconSize10: iconSize10 * iconScale,
iconSize14: iconSize14 * iconScale, iconSize14: iconSize14 * iconScale,
iconSize18: iconSize18 * iconScale,
sizeScale, sizeScale,
selected: selected ? 1 : 0, selected: selected ? 1 : 0,
highlighted: highlighted ? 1 : 0, highlighted: highlighted ? 1 : 0,
@ -474,14 +478,15 @@ export function useGlobeShips(
'case', 'case',
['==', ['get', 'selected'], 1], 'rgba(14,234,255,0.95)', ['==', ['get', 'selected'], 1], 'rgba(14,234,255,0.95)',
['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,0.95)', ['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,0.95)',
['coalesce', ['get', 'shipColor'], '#64748b'], ['==', ['get', 'permitted'], 1], GLOBE_OUTLINE_PERMITTED,
GLOBE_OUTLINE_OTHER,
] as never, ] as never,
'circle-stroke-width': [ 'circle-stroke-width': [
'case', 'case',
['==', ['get', 'selected'], 1], 3.4, ['==', ['get', 'selected'], 1], 3.4,
['==', ['get', 'highlighted'], 1], 2.7, ['==', ['get', 'highlighted'], 1], 2.7,
['==', ['get', 'permitted'], 1], 1.8, ['==', ['get', 'permitted'], 1], 1.8,
0.0, 0.7,
] as never, ] as never,
'circle-stroke-opacity': 0.85, 'circle-stroke-opacity': 0.85,
}, },
@ -519,14 +524,15 @@ export function useGlobeShips(
'case', 'case',
['==', ['get', 'selected'], 1], 'rgba(14,234,255,0.95)', ['==', ['get', 'selected'], 1], 'rgba(14,234,255,0.95)',
['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,0.95)', ['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,0.95)',
['coalesce', ['get', 'shipColor'], '#64748b'], ['==', ['get', 'permitted'], 1], GLOBE_OUTLINE_PERMITTED,
GLOBE_OUTLINE_OTHER,
] as never); ] as never);
map.setPaintProperty(outlineId, 'circle-stroke-width', [ map.setPaintProperty(outlineId, 'circle-stroke-width', [
'case', 'case',
['==', ['get', 'selected'], 1], 3.4, ['==', ['get', 'selected'], 1], 3.4,
['==', ['get', 'highlighted'], 1], 2.7, ['==', ['get', 'highlighted'], 1], 2.7,
['==', ['get', 'permitted'], 1], 1.8, ['==', ['get', 'permitted'], 1], 1.8,
0.0, 0.7,
] as never); ] as never);
} catch { } catch {
// ignore // ignore
@ -561,8 +567,9 @@ export function useGlobeShips(
'interpolate', ['linear'], ['zoom'], 'interpolate', ['linear'], ['zoom'],
3, ['to-number', ['get', 'iconSize3'], 0.35], 3, ['to-number', ['get', 'iconSize3'], 0.35],
7, ['to-number', ['get', 'iconSize7'], 0.45], 7, ['to-number', ['get', 'iconSize7'], 0.45],
10, ['to-number', ['get', 'iconSize10'], 0.56], 10, ['to-number', ['get', 'iconSize10'], 0.58],
14, ['to-number', ['get', 'iconSize14'], 0.72], 14, ['to-number', ['get', 'iconSize14'], 0.85],
18, ['to-number', ['get', 'iconSize18'], 2.5],
] as unknown as number[], ] as unknown as number[],
'icon-allow-overlap': true, 'icon-allow-overlap': true,
'icon-ignore-placement': true, 'icon-ignore-placement': true,
@ -791,8 +798,9 @@ export function useGlobeShips(
}), }),
iconSize3: clampNumber(0.35 * sizeScale * scale, 0.25, 1.45), iconSize3: clampNumber(0.35 * sizeScale * scale, 0.25, 1.45),
iconSize7: clampNumber(0.45 * sizeScale * scale, 0.3, 1.7), iconSize7: clampNumber(0.45 * sizeScale * scale, 0.3, 1.7),
iconSize10: clampNumber(0.56 * sizeScale * scale, 0.35, 2.0), iconSize10: clampNumber(0.58 * sizeScale * scale, 0.35, 2.1),
iconSize14: clampNumber(0.72 * sizeScale * scale, 0.45, 2.4), iconSize14: clampNumber(0.85 * sizeScale * scale, 0.45, 3.0),
iconSize18: clampNumber(2.5 * sizeScale * scale, 1.0, 7.0),
selected: selected ? 1 : 0, selected: selected ? 1 : 0,
permitted: legacy ? 1 : 0, permitted: legacy ? 1 : 0,
}, },
@ -907,8 +915,9 @@ export function useGlobeShips(
'interpolate', ['linear'], ['zoom'], 'interpolate', ['linear'], ['zoom'],
3, ['to-number', ['get', 'iconSize3'], 0.35], 3, ['to-number', ['get', 'iconSize3'], 0.35],
7, ['to-number', ['get', 'iconSize7'], 0.45], 7, ['to-number', ['get', 'iconSize7'], 0.45],
10, ['to-number', ['get', 'iconSize10'], 0.56], 10, ['to-number', ['get', 'iconSize10'], 0.58],
14, ['to-number', ['get', 'iconSize14'], 0.72], 14, ['to-number', ['get', 'iconSize14'], 0.85],
18, ['to-number', ['get', 'iconSize18'], 2.5],
] as unknown as number[], ] as unknown as number[],
'icon-allow-overlap': true, 'icon-allow-overlap': true,
'icon-ignore-placement': true, 'icon-ignore-placement': true,

파일 보기

@ -66,11 +66,11 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
type: 'fill', type: 'fill',
source: oceanSourceId, source: oceanSourceId,
'source-layer': 'contour', 'source-layer': 'contour',
minzoom: 6, minzoom: 5,
maxzoom: 24, maxzoom: 24,
paint: { paint: {
'fill-color': bathyFillColor, 'fill-color': bathyFillColor,
'fill-opacity': ['interpolate', ['linear'], ['zoom'], 0, 0.9, 6, 0.86, 10, 0.78], 'fill-opacity': ['interpolate', ['linear'], ['zoom'], 0, 0.9, 5, 0.88, 8, 0.84, 10, 0.78],
}, },
} as unknown as LayerSpecification; } as unknown as LayerSpecification;
@ -79,7 +79,7 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
type: 'line', type: 'line',
source: oceanSourceId, source: oceanSourceId,
'source-layer': 'contour', 'source-layer': 'contour',
minzoom: 6, minzoom: 5,
maxzoom: 24, maxzoom: 24,
paint: { paint: {
'line-color': 'rgba(255,255,255,0.06)', 'line-color': 'rgba(255,255,255,0.06)',
@ -94,7 +94,7 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
type: 'line', type: 'line',
source: oceanSourceId, source: oceanSourceId,
'source-layer': 'contour_line', 'source-layer': 'contour_line',
minzoom: 8, minzoom: 7,
paint: { paint: {
'line-color': [ 'line-color': [
'interpolate', 'interpolate',
@ -127,7 +127,7 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
type: 'line', type: 'line',
source: oceanSourceId, source: oceanSourceId,
'source-layer': 'contour_line', 'source-layer': 'contour_line',
minzoom: 8, minzoom: 7,
maxzoom: 24, maxzoom: 24,
filter: bathyMajorDepthFilter as unknown as unknown[], filter: bathyMajorDepthFilter as unknown as unknown[],
paint: { paint: {
@ -143,14 +143,14 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
type: 'line', type: 'line',
source: oceanSourceId, source: oceanSourceId,
'source-layer': 'contour', 'source-layer': 'contour',
minzoom: 4, minzoom: 3,
maxzoom: 24, maxzoom: 24,
filter: bathyMajorDepthFilter as unknown as unknown[], filter: bathyMajorDepthFilter as unknown as unknown[],
paint: { paint: {
'line-color': 'rgba(255,255,255,0.14)', 'line-color': 'rgba(255,255,255,0.14)',
'line-opacity': ['interpolate', ['linear'], ['zoom'], 4, 0.14, 8, 0.2, 12, 0.26], 'line-opacity': ['interpolate', ['linear'], ['zoom'], 3, 0.10, 6, 0.16, 8, 0.2, 12, 0.26],
'line-blur': ['interpolate', ['linear'], ['zoom'], 4, 0.3, 10, 0.15], 'line-blur': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 6, 0.35, 10, 0.15],
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.35, 8, 0.55, 12, 0.85], 'line-width': ['interpolate', ['linear'], ['zoom'], 3, 0.25, 6, 0.4, 8, 0.55, 12, 0.85],
}, },
} as unknown as LayerSpecification; } as unknown as LayerSpecification;

파일 보기

@ -45,7 +45,8 @@ export function makeGlobeCircleRadiusExpr() {
const base3 = 4; const base3 = 4;
const base7 = 6; const base7 = 6;
const base10 = 8; const base10 = 8;
const base14 = 11; const base14 = 12;
const base18 = 32;
return [ return [
'interpolate', 'interpolate',
@ -58,7 +59,9 @@ export function makeGlobeCircleRadiusExpr() {
10, 10,
['case', ['==', ['get', 'selected'], 1], 9.0, ['==', ['get', 'highlighted'], 1], 8.2, base10], ['case', ['==', ['get', 'selected'], 1], 9.0, ['==', ['get', 'highlighted'], 1], 8.2, base10],
14, 14,
['case', ['==', ['get', 'selected'], 1], 11.8, ['==', ['get', 'highlighted'], 1], 10.8, base14], ['case', ['==', ['get', 'selected'], 1], 13.5, ['==', ['get', 'highlighted'], 1], 12.6, base14],
18,
['case', ['==', ['get', 'selected'], 1], 36, ['==', ['get', 'highlighted'], 1], 34, base18],
]; ];
} }