From d5a8be3b96cf18a340795f0762b6a3e91f2b5e71 Mon Sep 17 00:00:00 2001 From: htlee Date: Fri, 20 Feb 2026 03:57:45 +0900 Subject: [PATCH] =?UTF-8?q?fix(map):=20Globe=20=EC=82=AC=EC=A7=84=20?= =?UTF-8?q?=EC=9D=B8=EB=94=94=EC=BC=80=EC=9D=B4=ED=84=B0=20=EB=84=A4?= =?UTF-8?q?=EC=9D=B4=ED=8B=B0=EB=B8=8C=20=EB=A0=88=EC=9D=B4=EC=96=B4=20?= =?UTF-8?q?=EC=A0=84=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Globe Deck.gl ScatterplotLayer 아티팩트(파란 막대) 수정 - MapLibre 네이티브 circle 레이어로 사진 인디케이터 구현 --- .../src/widgets/map3d/hooks/useDeckLayers.ts | 31 ++-------------- .../widgets/map3d/hooks/useGlobeShipLayers.ts | 36 ++++++++++++++++++- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts b/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts index 9e58b05..4d007ca 100644 --- a/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts +++ b/apps/web/src/widgets/map3d/hooks/useDeckLayers.ts @@ -347,32 +347,10 @@ export function useDeckLayers( if (!deckTarget) return; if (!ENABLE_GLOBE_DECK_OVERLAYS) { - // Ship photo indicator: 사진 유무 표시 (ScatterplotLayer) - const photoLayers: unknown[] = []; - if (settings.showShips && overlays.shipPhotos && shipPhotoTargets.length > 0) { - photoLayers.push( - new ScatterplotLayer({ - id: 'ship-photo-indicator', - data: shipPhotoTargets, - pickable: true, - billboard: false, - filled: true, - stroked: true, - radiusUnits: 'pixels', - getRadius: 5, - getFillColor: [0, 188, 212, 180], - getLineColor: [255, 255, 255, 200], - lineWidthUnits: 'pixels', - getLineWidth: 1, - getPosition: (d) => [d.lon, d.lat] as [number, number], - onClick: (info: PickingInfo) => { - if (info.object) onClickShipPhoto?.((info.object as AisTarget).mmsi); - }, - }), - ); - } + // Globe에서는 Deck.gl ScatterplotLayer가 프로젝션 공간 아티팩트(막대)를 유발하므로 + // 빈 레이어만 설정. 사진 인디케이터는 Mercator에서만 동작. try { - deckTarget.setProps({ layers: sanitizeDeckLayerList(photoLayers), getTooltip: undefined, onClick: undefined } as never); + deckTarget.setProps({ layers: [], getTooltip: undefined, onClick: undefined } as never); } catch { // ignore } @@ -439,8 +417,5 @@ export function useDeckLayers( toFleetMmsiList, touchDeckHoverState, legacyHits, - shipPhotoTargets, - onClickShipPhoto, - overlays.shipPhotos, ]); } diff --git a/apps/web/src/widgets/map3d/hooks/useGlobeShipLayers.ts b/apps/web/src/widgets/map3d/hooks/useGlobeShipLayers.ts index 165e19e..f58c377 100644 --- a/apps/web/src/widgets/map3d/hooks/useGlobeShipLayers.ts +++ b/apps/web/src/widgets/map3d/hooks/useGlobeShipLayers.ts @@ -120,6 +120,7 @@ export function useGlobeShipLayers( alarmKind: alarmKind ?? '', alarmBadgeLabel: alarmKind ? ALARM_BADGE[alarmKind].label : '', alarmBadgeColor: alarmKind ? ALARM_BADGE[alarmKind].color : '#000', + hasPhoto: t.shipImagePath ? 1 : 0, }, }; }), @@ -167,13 +168,14 @@ export function useGlobeShipLayers( const symbolLiteId = 'ships-globe-lite'; const symbolId = 'ships-globe'; const labelId = 'ships-globe-label'; + const photoId = 'ships-globe-photo'; const pulseId = 'ships-globe-alarm-pulse'; const badgeId = 'ships-globe-alarm-badge'; // 레이어를 제거하지 않고 visibility만 'none'으로 설정 // guardedSetVisibility로 현재 값과 동일하면 호출 생략 (style._changed 방지) const hide = () => { - for (const id of [badgeId, labelId, symbolId, symbolLiteId, pulseId, outlineId, haloId]) { + for (const id of [badgeId, photoId, labelId, symbolId, symbolLiteId, pulseId, outlineId, haloId]) { guardedSetVisibility(map, id, 'none'); } }; @@ -197,6 +199,7 @@ export function useGlobeShipLayers( // → style._changed 방지 → 불필요한 symbol placement 재계산 방지 → 라벨 사라짐 방지 const visibility: 'visible' | 'none' = projection === 'globe' ? 'visible' : 'none'; const labelVisibility: 'visible' | 'none' = projection === 'globe' && overlays.shipLabels ? 'visible' : 'none'; + const photoVisibility: 'visible' | 'none' = projection === 'globe' && overlays.shipPhotos ? 'visible' : 'none'; if (map.getLayer(symbolId) || map.getLayer(symbolLiteId)) { const changed = map.getLayoutProperty(symbolId, 'visibility') !== visibility || @@ -208,6 +211,7 @@ export function useGlobeShipLayers( if (projection === 'globe') kickRepaint(map); } guardedSetVisibility(map, labelId, labelVisibility); + guardedSetVisibility(map, photoId, photoVisibility); } // 데이터 업데이트는 projectionBusy 중에는 차단 @@ -512,6 +516,35 @@ export function useGlobeShipLayers( } } + // Photo indicator circle (above ship icons, below labels) + if (!map.getLayer(photoId)) { + needReorder = true; + try { + map.addLayer( + { + id: photoId, + type: 'circle', + source: srcId, + filter: ['==', ['get', 'hasPhoto'], 1] as never, + layout: { visibility: photoVisibility }, + paint: { + 'circle-radius': [ + 'interpolate', ['linear'], ['zoom'], + 3, 3, 7, 4, 10, 5, 14, 6, + ] as never, + 'circle-color': 'rgba(0, 188, 212, 0.7)', + 'circle-stroke-color': 'rgba(255, 255, 255, 0.8)', + 'circle-stroke-width': 1, + 'circle-translate': [8, -8], + }, + } as unknown as LayerSpecification, + before, + ); + } catch (e) { + console.warn('Ship photo indicator layer add failed:', e); + } + } + const labelFilter = [ 'all', ['!=', ['to-string', ['coalesce', ['get', 'labelName'], '']], ''], @@ -611,6 +644,7 @@ export function useGlobeShipLayers( projection, settings.showShips, overlays.shipLabels, + overlays.shipPhotos, globeShipGeoJson, alarmGeoJson, mapSyncEpoch,