fix(map): Globe 사진 인디케이터 네이티브 레이어 전환

- Globe Deck.gl ScatterplotLayer 아티팩트(파란 막대) 수정
- MapLibre 네이티브 circle 레이어로 사진 인디케이터 구현
This commit is contained in:
htlee 2026-02-20 03:57:45 +09:00
부모 e72e2f14f6
커밋 d5a8be3b96
2개의 변경된 파일38개의 추가작업 그리고 29개의 파일을 삭제

파일 보기

@ -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<AisTarget>({
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,
]);
}

파일 보기

@ -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,