From 3ba6c02ba0da3a53cbf70237a1bb3648589f18ca Mon Sep 17 00:00:00 2001 From: htlee Date: Mon, 16 Feb 2026 01:10:45 +0900 Subject: [PATCH] =?UTF-8?q?feat(map):=20=EC=84=A0=EB=B0=95=20=EC=99=B8?= =?UTF-8?q?=EA=B3=BD=EC=84=A0=20=EB=8C=80=EB=B9=84=20=EB=B0=8F=20=EC=A4=8C?= =?UTF-8?q?=20=EC=8A=A4=EC=BC=80=EC=9D=BC=EB=A7=81=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../web/src/pages/dashboard/DashboardPage.tsx | 4 +-- apps/web/src/widgets/map3d/constants.ts | 14 ++++++-- .../src/widgets/map3d/hooks/useDeckLayers.ts | 15 ++++----- .../src/widgets/map3d/hooks/useGlobeShips.ts | 33 ++++++++++++------- .../src/widgets/map3d/layers/bathymetry.ts | 18 +++++----- .../src/widgets/map3d/lib/mlExpressions.ts | 7 ++-- 6 files changed, 54 insertions(+), 37 deletions(-) diff --git a/apps/web/src/pages/dashboard/DashboardPage.tsx b/apps/web/src/pages/dashboard/DashboardPage.tsx index 4c1ab37..2d24f41 100644 --- a/apps/web/src/pages/dashboard/DashboardPage.tsx +++ b/apps/web/src/pages/dashboard/DashboardPage.tsx @@ -115,8 +115,8 @@ export function DashboardPage() { fcLines: true, zones: true, fleetCircles: true, - predictVectors: false, - shipLabels: false, + predictVectors: true, + shipLabels: true, }); const [fleetRelationSortMode, setFleetRelationSortMode] = useState("count"); diff --git a/apps/web/src/widgets/map3d/constants.ts b/apps/web/src/widgets/map3d/constants.ts index 05a2940..66b2b78 100644 --- a/apps/web/src/widgets/map3d/constants.ts +++ b/apps/web/src/widgets/map3d/constants.ts @@ -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_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 ── 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 ── export const BATHY_ZOOM_RANGES: BathyZoomRange[] = [ - { id: 'bathymetry-fill', mercator: [6, 24], globe: [8, 24] }, - { id: 'bathymetry-borders', mercator: [6, 24], globe: [8, 24] }, - { id: 'bathymetry-borders-major', mercator: [4, 24], globe: [8, 24] }, + { id: 'bathymetry-fill', mercator: [5, 24], globe: [7, 24] }, + { id: 'bathymetry-borders', mercator: [5, 24], globe: [7, 24] }, + { id: 'bathymetry-borders-major', mercator: [3, 24], globe: [7, 24] }, ]; diff --git a/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts b/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts index b22a6c4..4ab3b49 100644 --- a/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts +++ b/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts @@ -20,7 +20,9 @@ import { EMPTY_MMSI_SET, DEPTH_DISABLED_PARAMS, GLOBE_OVERLAY_PARAMS, - LEGACY_CODE_COLORS, + HALO_OUTLINE_COLOR, + HALO_OUTLINE_COLOR_SELECTED, + HALO_OUTLINE_COLOR_HIGHLIGHTED, PAIR_RANGE_NORMAL_DECK, PAIR_RANGE_WARN_DECK, PAIR_LINE_NORMAL_DECK, @@ -426,12 +428,7 @@ export function useDeckLayers( getRadius: () => FLAT_LEGACY_HALO_RADIUS, lineWidthUnits: 'pixels', getLineWidth: () => 2, - getLineColor: (d) => { - 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]; - }, + getLineColor: () => HALO_OUTLINE_COLOR, getPosition: (d) => [d.lon, d.lat] as [number, number], }), ); @@ -476,7 +473,7 @@ export function useDeckLayers( } if (settings.showShips && legacyOverlayTargets.length > 0) { - layers.push(new ScatterplotLayer({ 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({ 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) { @@ -621,7 +618,7 @@ export function useDeckLayers( } if (settings.showShips && legacyTargetsOrdered.length > 0) { - globeLayers.push(new ScatterplotLayer({ 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({ 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); diff --git a/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts b/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts index 525cb65..b5b16fb 100644 --- a/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts +++ b/apps/web/src/widgets/map3d/hooks/useGlobeShips.ts @@ -8,6 +8,8 @@ import type { Map3DSettings, MapProjectionId } from '../types'; import { ANCHORED_SHIP_ICON_ID, GLOBE_ICON_HEADING_OFFSET_DEG, + GLOBE_OUTLINE_PERMITTED, + GLOBE_OUTLINE_OTHER, DEG2RAD, } from '../constants'; import { isFiniteNumber } from '../lib/setUtils'; @@ -351,8 +353,9 @@ export function useGlobeShips( const iconScale = selected ? selectedScale : highlightScale; const iconSize3 = clampNumber(0.35 * sizeScale * selectedScale, 0.25, 1.3); const iconSize7 = clampNumber(0.45 * sizeScale * selectedScale, 0.3, 1.45); - const iconSize10 = clampNumber(0.56 * sizeScale * selectedScale, 0.35, 1.7); - const iconSize14 = clampNumber(0.72 * sizeScale * selectedScale, 0.45, 2.1); + const iconSize10 = clampNumber(0.58 * sizeScale * selectedScale, 0.35, 1.8); + const iconSize14 = clampNumber(0.85 * sizeScale * selectedScale, 0.45, 2.6); + const iconSize18 = clampNumber(2.5 * sizeScale * selectedScale, 1.0, 6.0); return { type: 'Feature', ...(isFiniteNumber(t.mmsi) ? { id: Math.trunc(t.mmsi) } : {}), @@ -373,6 +376,7 @@ export function useGlobeShips( iconSize7: iconSize7 * iconScale, iconSize10: iconSize10 * iconScale, iconSize14: iconSize14 * iconScale, + iconSize18: iconSize18 * iconScale, sizeScale, selected: selected ? 1 : 0, highlighted: highlighted ? 1 : 0, @@ -474,14 +478,15 @@ export function useGlobeShips( 'case', ['==', ['get', 'selected'], 1], 'rgba(14,234,255,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, 'circle-stroke-width': [ 'case', ['==', ['get', 'selected'], 1], 3.4, ['==', ['get', 'highlighted'], 1], 2.7, ['==', ['get', 'permitted'], 1], 1.8, - 0.0, + 0.7, ] as never, 'circle-stroke-opacity': 0.85, }, @@ -519,14 +524,15 @@ export function useGlobeShips( 'case', ['==', ['get', 'selected'], 1], 'rgba(14,234,255,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); map.setPaintProperty(outlineId, 'circle-stroke-width', [ 'case', ['==', ['get', 'selected'], 1], 3.4, ['==', ['get', 'highlighted'], 1], 2.7, ['==', ['get', 'permitted'], 1], 1.8, - 0.0, + 0.7, ] as never); } catch { // ignore @@ -561,8 +567,9 @@ export function useGlobeShips( 'interpolate', ['linear'], ['zoom'], 3, ['to-number', ['get', 'iconSize3'], 0.35], 7, ['to-number', ['get', 'iconSize7'], 0.45], - 10, ['to-number', ['get', 'iconSize10'], 0.56], - 14, ['to-number', ['get', 'iconSize14'], 0.72], + 10, ['to-number', ['get', 'iconSize10'], 0.58], + 14, ['to-number', ['get', 'iconSize14'], 0.85], + 18, ['to-number', ['get', 'iconSize18'], 2.5], ] as unknown as number[], 'icon-allow-overlap': true, 'icon-ignore-placement': true, @@ -791,8 +798,9 @@ export function useGlobeShips( }), iconSize3: clampNumber(0.35 * sizeScale * scale, 0.25, 1.45), iconSize7: clampNumber(0.45 * sizeScale * scale, 0.3, 1.7), - iconSize10: clampNumber(0.56 * sizeScale * scale, 0.35, 2.0), - iconSize14: clampNumber(0.72 * sizeScale * scale, 0.45, 2.4), + iconSize10: clampNumber(0.58 * sizeScale * scale, 0.35, 2.1), + iconSize14: clampNumber(0.85 * sizeScale * scale, 0.45, 3.0), + iconSize18: clampNumber(2.5 * sizeScale * scale, 1.0, 7.0), selected: selected ? 1 : 0, permitted: legacy ? 1 : 0, }, @@ -907,8 +915,9 @@ export function useGlobeShips( 'interpolate', ['linear'], ['zoom'], 3, ['to-number', ['get', 'iconSize3'], 0.35], 7, ['to-number', ['get', 'iconSize7'], 0.45], - 10, ['to-number', ['get', 'iconSize10'], 0.56], - 14, ['to-number', ['get', 'iconSize14'], 0.72], + 10, ['to-number', ['get', 'iconSize10'], 0.58], + 14, ['to-number', ['get', 'iconSize14'], 0.85], + 18, ['to-number', ['get', 'iconSize18'], 2.5], ] as unknown as number[], 'icon-allow-overlap': true, 'icon-ignore-placement': true, diff --git a/apps/web/src/widgets/map3d/layers/bathymetry.ts b/apps/web/src/widgets/map3d/layers/bathymetry.ts index e0f7004..6b55e33 100644 --- a/apps/web/src/widgets/map3d/layers/bathymetry.ts +++ b/apps/web/src/widgets/map3d/layers/bathymetry.ts @@ -66,11 +66,11 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK type: 'fill', source: oceanSourceId, 'source-layer': 'contour', - minzoom: 6, + minzoom: 5, maxzoom: 24, paint: { '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; @@ -79,7 +79,7 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK type: 'line', source: oceanSourceId, 'source-layer': 'contour', - minzoom: 6, + minzoom: 5, maxzoom: 24, paint: { 'line-color': 'rgba(255,255,255,0.06)', @@ -94,7 +94,7 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK type: 'line', source: oceanSourceId, 'source-layer': 'contour_line', - minzoom: 8, + minzoom: 7, paint: { 'line-color': [ 'interpolate', @@ -127,7 +127,7 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK type: 'line', source: oceanSourceId, 'source-layer': 'contour_line', - minzoom: 8, + minzoom: 7, maxzoom: 24, filter: bathyMajorDepthFilter as unknown as unknown[], paint: { @@ -143,14 +143,14 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK type: 'line', source: oceanSourceId, 'source-layer': 'contour', - minzoom: 4, + minzoom: 3, maxzoom: 24, filter: bathyMajorDepthFilter as unknown as unknown[], paint: { 'line-color': 'rgba(255,255,255,0.14)', - 'line-opacity': ['interpolate', ['linear'], ['zoom'], 4, 0.14, 8, 0.2, 12, 0.26], - 'line-blur': ['interpolate', ['linear'], ['zoom'], 4, 0.3, 10, 0.15], - 'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.35, 8, 0.55, 12, 0.85], + 'line-opacity': ['interpolate', ['linear'], ['zoom'], 3, 0.10, 6, 0.16, 8, 0.2, 12, 0.26], + 'line-blur': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 6, 0.35, 10, 0.15], + 'line-width': ['interpolate', ['linear'], ['zoom'], 3, 0.25, 6, 0.4, 8, 0.55, 12, 0.85], }, } as unknown as LayerSpecification; diff --git a/apps/web/src/widgets/map3d/lib/mlExpressions.ts b/apps/web/src/widgets/map3d/lib/mlExpressions.ts index 7d9ede2..72c3ceb 100644 --- a/apps/web/src/widgets/map3d/lib/mlExpressions.ts +++ b/apps/web/src/widgets/map3d/lib/mlExpressions.ts @@ -45,7 +45,8 @@ export function makeGlobeCircleRadiusExpr() { const base3 = 4; const base7 = 6; const base10 = 8; - const base14 = 11; + const base14 = 12; + const base18 = 32; return [ 'interpolate', @@ -58,7 +59,9 @@ export function makeGlobeCircleRadiusExpr() { 10, ['case', ['==', ['get', 'selected'], 1], 9.0, ['==', ['get', 'highlighted'], 1], 8.2, base10], 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], ]; }