fix(map): 라벨 사라짐 + easing 경고 + vertex 경고 수정
- guardedSetVisibility 도입: 현재 값과 동일하면 setLayoutProperty 호출 생략하여 style._changed 트리거 방지 → symbol 재배치로 인한 text-allow-overlap:false 라벨 사라짐 현상 해결 - useGlobeShips 기존 레이어 else 블록의 중복 expression 재설정 제거 (data-driven 표현식은 addLayer 시 1회 설정으로 충분) - _render 래퍼에서 globe scrollZoom easing 경고 억제 - fleet-circles-ml-fill 레이어 완전 제거 (vertex 65535 초과 원인) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
91df90b528
커밋
95d9ea8aef
@ -118,7 +118,7 @@ export function useGlobeInteraction(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (layerId === 'fleet-circles-ml' || layerId === 'fleet-circles-ml-fill') {
|
if (layerId === 'fleet-circles-ml') {
|
||||||
return getFleetCircleTooltipHtml({
|
return getFleetCircleTooltipHtml({
|
||||||
ownerKey: String(props.ownerKey ?? ''),
|
ownerKey: String(props.ownerKey ?? ''),
|
||||||
ownerLabel: String(props.ownerLabel ?? props.ownerKey ?? ''),
|
ownerLabel: String(props.ownerLabel ?? props.ownerKey ?? ''),
|
||||||
@ -186,7 +186,7 @@ export function useGlobeInteraction(
|
|||||||
candidateLayerIds = [
|
candidateLayerIds = [
|
||||||
'ships-globe', 'ships-globe-halo', 'ships-globe-outline',
|
'ships-globe', 'ships-globe-halo', 'ships-globe-outline',
|
||||||
'pair-lines-ml', 'fc-lines-ml',
|
'pair-lines-ml', 'fc-lines-ml',
|
||||||
'fleet-circles-ml', 'fleet-circles-ml-fill',
|
'fleet-circles-ml',
|
||||||
'pair-range-ml',
|
'pair-range-ml',
|
||||||
'zones-fill', 'zones-line', 'zones-label',
|
'zones-fill', 'zones-line', 'zones-label',
|
||||||
].filter((id) => map.getLayer(id));
|
].filter((id) => map.getLayer(id));
|
||||||
@ -213,7 +213,7 @@ export function useGlobeInteraction(
|
|||||||
const priority = [
|
const priority = [
|
||||||
'ships-globe', 'ships-globe-halo', 'ships-globe-outline',
|
'ships-globe', 'ships-globe-halo', 'ships-globe-outline',
|
||||||
'pair-lines-ml', 'fc-lines-ml', 'pair-range-ml',
|
'pair-lines-ml', 'fc-lines-ml', 'pair-range-ml',
|
||||||
'fleet-circles-ml-fill', 'fleet-circles-ml',
|
'fleet-circles-ml',
|
||||||
'zones-fill', 'zones-line', 'zones-label',
|
'zones-fill', 'zones-line', 'zones-label',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -232,7 +232,7 @@ export function useGlobeInteraction(
|
|||||||
const isShipLayer = layerId === 'ships-globe' || layerId === 'ships-globe-halo' || layerId === 'ships-globe-outline';
|
const isShipLayer = layerId === 'ships-globe' || layerId === 'ships-globe-halo' || layerId === 'ships-globe-outline';
|
||||||
const isPairLayer = layerId === 'pair-lines-ml' || layerId === 'pair-range-ml';
|
const isPairLayer = layerId === 'pair-lines-ml' || layerId === 'pair-range-ml';
|
||||||
const isFcLayer = layerId === 'fc-lines-ml';
|
const isFcLayer = layerId === 'fc-lines-ml';
|
||||||
const isFleetLayer = layerId === 'fleet-circles-ml' || layerId === 'fleet-circles-ml-fill';
|
const isFleetLayer = layerId === 'fleet-circles-ml';
|
||||||
const isZoneLayer = layerId === 'zones-fill' || layerId === 'zones-line' || layerId === 'zones-label';
|
const isZoneLayer = layerId === 'zones-fill' || layerId === 'zones-line' || layerId === 'zones-label';
|
||||||
|
|
||||||
if (isShipLayer) {
|
if (isShipLayer) {
|
||||||
|
|||||||
@ -11,7 +11,6 @@ import {
|
|||||||
PAIR_RANGE_NORMAL_ML_HL, PAIR_RANGE_WARN_ML_HL,
|
PAIR_RANGE_NORMAL_ML_HL, PAIR_RANGE_WARN_ML_HL,
|
||||||
FC_LINE_NORMAL_ML, FC_LINE_SUSPICIOUS_ML,
|
FC_LINE_NORMAL_ML, FC_LINE_SUSPICIOUS_ML,
|
||||||
FC_LINE_NORMAL_ML_HL, FC_LINE_SUSPICIOUS_ML_HL,
|
FC_LINE_NORMAL_ML_HL, FC_LINE_SUSPICIOUS_ML_HL,
|
||||||
FLEET_FILL_ML, FLEET_FILL_ML_HL,
|
|
||||||
FLEET_LINE_ML, FLEET_LINE_ML_HL,
|
FLEET_LINE_ML, FLEET_LINE_ML_HL,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { makeUniqueSorted } from '../lib/setUtils';
|
import { makeUniqueSorted } from '../lib/setUtils';
|
||||||
@ -28,6 +27,7 @@ import {
|
|||||||
} from '../lib/mlExpressions';
|
} from '../lib/mlExpressions';
|
||||||
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';
|
import { kickRepaint, onMapStyleReady } from '../lib/mapCore';
|
||||||
import { circleRingLngLat } from '../lib/geometry';
|
import { circleRingLngLat } from '../lib/geometry';
|
||||||
|
import { guardedSetVisibility } from '../lib/layerHelpers';
|
||||||
import { dashifyLine } from '../lib/dashifyLine';
|
import { dashifyLine } from '../lib/dashifyLine';
|
||||||
|
|
||||||
export function useGlobeOverlays(
|
export function useGlobeOverlays(
|
||||||
@ -60,11 +60,7 @@ export function useGlobeOverlays(
|
|||||||
const layerId = 'pair-lines-ml';
|
const layerId = 'pair-lines-ml';
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'none');
|
||||||
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, 'visibility', 'none');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
@ -132,11 +128,7 @@ export function useGlobeOverlays(
|
|||||||
console.warn('Pair lines layer add failed:', e);
|
console.warn('Pair lines layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'visible');
|
||||||
map.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reorderGlobeFeatureLayers();
|
reorderGlobeFeatureLayers();
|
||||||
@ -159,11 +151,7 @@ export function useGlobeOverlays(
|
|||||||
const layerId = 'fc-lines-ml';
|
const layerId = 'fc-lines-ml';
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'none');
|
||||||
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, 'visibility', 'none');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
@ -235,11 +223,7 @@ export function useGlobeOverlays(
|
|||||||
console.warn('FC lines layer add failed:', e);
|
console.warn('FC lines layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'visible');
|
||||||
map.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reorderGlobeFeatureLayers();
|
reorderGlobeFeatureLayers();
|
||||||
@ -259,21 +243,13 @@ export function useGlobeOverlays(
|
|||||||
if (!map) return;
|
if (!map) return;
|
||||||
|
|
||||||
const srcId = 'fleet-circles-ml-src';
|
const srcId = 'fleet-circles-ml-src';
|
||||||
const fillSrcId = 'fleet-circles-ml-fill-src';
|
|
||||||
const layerId = 'fleet-circles-ml';
|
const layerId = 'fleet-circles-ml';
|
||||||
const fillLayerId = 'fleet-circles-ml-fill';
|
|
||||||
|
// fill layer 제거됨 — globe tessellation에서 vertex 65535 초과 경고 원인
|
||||||
|
// 라인만으로 fleet circle 시각화 충분
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'none');
|
||||||
if (map.getLayer(fillLayerId)) map.setLayoutProperty(fillLayerId, 'visibility', 'none');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, 'visibility', 'none');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
@ -304,29 +280,6 @@ export function useGlobeOverlays(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
// fill 폴리곤은 globe 테셀레이션으로 vertex 수가 급격히 증가하므로
|
|
||||||
// 스텝 수를 줄이고 (24), 반경을 500nm으로 상한 설정
|
|
||||||
const MAX_FILL_RADIUS_M = 500 * 1852;
|
|
||||||
const fcFill: GeoJSON.FeatureCollection<GeoJSON.Polygon> = {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: (fleetCircles || []).map((c) => {
|
|
||||||
const ring = circleRingLngLat(c.center, Math.min(c.radiusNm * 1852, MAX_FILL_RADIUS_M), 24);
|
|
||||||
return {
|
|
||||||
type: 'Feature',
|
|
||||||
id: `${makeFleetCircleFeatureId(c.ownerKey)}-fill`,
|
|
||||||
geometry: { type: 'Polygon', coordinates: [ring] },
|
|
||||||
properties: {
|
|
||||||
type: 'fleet-fill',
|
|
||||||
ownerKey: c.ownerKey,
|
|
||||||
ownerLabel: c.ownerLabel,
|
|
||||||
count: c.count,
|
|
||||||
vesselMmsis: c.vesselMmsis,
|
|
||||||
highlighted: 0,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
const existing = map.getSource(srcId) as GeoJSONSource | undefined;
|
||||||
if (existing) existing.setData(fcLine);
|
if (existing) existing.setData(fcLine);
|
||||||
@ -336,41 +289,6 @@ export function useGlobeOverlays(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const existingFill = map.getSource(fillSrcId) as GeoJSONSource | undefined;
|
|
||||||
if (existingFill) existingFill.setData(fcFill);
|
|
||||||
else map.addSource(fillSrcId, { type: 'geojson', data: fcFill } as GeoJSONSourceSpecification);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Fleet circles source setup failed:', e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.getLayer(fillLayerId)) {
|
|
||||||
try {
|
|
||||||
map.addLayer(
|
|
||||||
{
|
|
||||||
id: fillLayerId,
|
|
||||||
type: 'fill',
|
|
||||||
source: fillSrcId,
|
|
||||||
layout: { visibility: 'visible' },
|
|
||||||
paint: {
|
|
||||||
'fill-color': ['case', ['==', ['get', 'highlighted'], 1], FLEET_FILL_ML_HL, FLEET_FILL_ML] as never,
|
|
||||||
'fill-opacity': ['case', ['==', ['get', 'highlighted'], 1], 0.7, 0.36] as never,
|
|
||||||
},
|
|
||||||
} as unknown as LayerSpecification,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Fleet circles fill layer add failed:', e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
map.setLayoutProperty(fillLayerId, 'visibility', 'visible');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!map.getLayer(layerId)) {
|
if (!map.getLayer(layerId)) {
|
||||||
try {
|
try {
|
||||||
map.addLayer(
|
map.addLayer(
|
||||||
@ -391,11 +309,7 @@ export function useGlobeOverlays(
|
|||||||
console.warn('Fleet circles layer add failed:', e);
|
console.warn('Fleet circles layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'visible');
|
||||||
map.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reorderGlobeFeatureLayers();
|
reorderGlobeFeatureLayers();
|
||||||
@ -418,11 +332,7 @@ export function useGlobeOverlays(
|
|||||||
const layerId = 'pair-range-ml';
|
const layerId = 'pair-range-ml';
|
||||||
|
|
||||||
const remove = () => {
|
const remove = () => {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'none');
|
||||||
if (map.getLayer(layerId)) map.setLayoutProperty(layerId, 'visibility', 'none');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const ensure = () => {
|
const ensure = () => {
|
||||||
@ -506,11 +416,7 @@ export function useGlobeOverlays(
|
|||||||
console.warn('Pair range layer add failed:', e);
|
console.warn('Pair range layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
guardedSetVisibility(map, layerId, 'visible');
|
||||||
map.setLayoutProperty(layerId, 'visibility', 'visible');
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
kickRepaint(map);
|
kickRepaint(map);
|
||||||
@ -596,10 +502,7 @@ export function useGlobeOverlays(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (map.getLayer('fleet-circles-ml-fill')) {
|
// fleet-circles-ml-fill 제거됨 (vertex 65535 경고 원인)
|
||||||
map.setPaintProperty('fleet-circles-ml-fill', 'fill-color', ['case', fleetHighlightExpr, FLEET_FILL_ML_HL, FLEET_FILL_ML] as never);
|
|
||||||
map.setPaintProperty('fleet-circles-ml-fill', 'fill-opacity', ['case', fleetHighlightExpr, 0.7, 0.28] as never);
|
|
||||||
}
|
|
||||||
if (map.getLayer('fleet-circles-ml')) {
|
if (map.getLayer('fleet-circles-ml')) {
|
||||||
map.setPaintProperty('fleet-circles-ml', 'line-color', ['case', fleetHighlightExpr, FLEET_LINE_ML_HL, FLEET_LINE_ML] as never);
|
map.setPaintProperty('fleet-circles-ml', 'line-color', ['case', fleetHighlightExpr, FLEET_LINE_ML_HL, FLEET_LINE_ML] as never);
|
||||||
map.setPaintProperty('fleet-circles-ml', 'line-width', ['case', fleetHighlightExpr, 2, 1.1] as never);
|
map.setPaintProperty('fleet-circles-ml', 'line-width', ['case', fleetHighlightExpr, 2, 1.1] as never);
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import {
|
|||||||
ensureFallbackShipImage,
|
ensureFallbackShipImage,
|
||||||
} from '../lib/globeShipIcon';
|
} from '../lib/globeShipIcon';
|
||||||
import { clampNumber } from '../lib/geometry';
|
import { clampNumber } from '../lib/geometry';
|
||||||
|
import { guardedSetVisibility } from '../lib/layerHelpers';
|
||||||
|
|
||||||
export function useGlobeShips(
|
export function useGlobeShips(
|
||||||
mapRef: MutableRefObject<maplibregl.Map | null>,
|
mapRef: MutableRefObject<maplibregl.Map | null>,
|
||||||
@ -273,11 +274,10 @@ export function useGlobeShips(
|
|||||||
const labelId = 'ships-globe-label';
|
const labelId = 'ships-globe-label';
|
||||||
|
|
||||||
// 레이어를 제거하지 않고 visibility만 'none'으로 설정
|
// 레이어를 제거하지 않고 visibility만 'none'으로 설정
|
||||||
|
// guardedSetVisibility로 현재 값과 동일하면 호출 생략 (style._changed 방지)
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
for (const id of [labelId, symbolId, outlineId, haloId]) {
|
for (const id of [labelId, symbolId, outlineId, haloId]) {
|
||||||
try {
|
guardedSetVisibility(map, id, 'none');
|
||||||
if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'none');
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -296,15 +296,19 @@ export function useGlobeShips(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 빠른 visibility 토글 — projectionBusy 중에도 실행
|
// 빠른 visibility 토글 — projectionBusy 중에도 실행
|
||||||
// 이미 생성된 레이어의 표시 상태만 즉시 전환하여 projection 전환 체감 속도 개선
|
// guardedSetVisibility로 값이 실제로 바뀔 때만 setLayoutProperty 호출
|
||||||
const visibility = projection === 'globe' ? 'visible' : 'none';
|
// → style._changed 방지 → 불필요한 symbol placement 재계산 방지 → 라벨 사라짐 방지
|
||||||
const labelVisibility = projection === 'globe' && overlays.shipLabels ? 'visible' : 'none';
|
const visibility: 'visible' | 'none' = projection === 'globe' ? 'visible' : 'none';
|
||||||
|
const labelVisibility: 'visible' | 'none' = projection === 'globe' && overlays.shipLabels ? 'visible' : 'none';
|
||||||
if (map.getLayer(symbolId)) {
|
if (map.getLayer(symbolId)) {
|
||||||
for (const id of [haloId, outlineId, symbolId]) {
|
const changed = map.getLayoutProperty(symbolId, 'visibility') !== visibility;
|
||||||
try { map.setLayoutProperty(id, 'visibility', visibility); } catch { /* ignore */ }
|
if (changed) {
|
||||||
|
for (const id of [haloId, outlineId, symbolId]) {
|
||||||
|
guardedSetVisibility(map, id, visibility);
|
||||||
|
}
|
||||||
|
if (projection === 'globe') kickRepaint(map);
|
||||||
}
|
}
|
||||||
try { map.setLayoutProperty(labelId, 'visibility', labelVisibility); } catch { /* ignore */ }
|
guardedSetVisibility(map, labelId, labelVisibility);
|
||||||
if (projection === 'globe') kickRepaint(map);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 데이터 업데이트는 projectionBusy 중에는 차단
|
// 데이터 업데이트는 projectionBusy 중에는 차단
|
||||||
@ -374,35 +378,8 @@ export function useGlobeShips(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Ship halo layer add failed:', e);
|
console.warn('Ship halo layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
map.setLayoutProperty(haloId, 'visibility', visibility);
|
|
||||||
map.setLayoutProperty(haloId, 'circle-sort-key', [
|
|
||||||
'case',
|
|
||||||
['all', ['==', ['get', 'selected'], 1], ['==', ['get', 'permitted'], 1]], 120,
|
|
||||||
['all', ['==', ['get', 'highlighted'], 1], ['==', ['get', 'permitted'], 1]], 115,
|
|
||||||
['==', ['get', 'permitted'], 1], 110,
|
|
||||||
['==', ['get', 'selected'], 1], 60,
|
|
||||||
['==', ['get', 'highlighted'], 1], 55,
|
|
||||||
20,
|
|
||||||
] as never);
|
|
||||||
map.setPaintProperty(haloId, 'circle-color', [
|
|
||||||
'case',
|
|
||||||
['==', ['get', 'selected'], 1], 'rgba(14,234,255,1)',
|
|
||||||
['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,1)',
|
|
||||||
['coalesce', ['get', 'shipColor'], '#64748b'],
|
|
||||||
] as never);
|
|
||||||
map.setPaintProperty(haloId, 'circle-opacity', [
|
|
||||||
'case',
|
|
||||||
['==', ['get', 'selected'], 1], 0.38,
|
|
||||||
['==', ['get', 'highlighted'], 1], 0.34,
|
|
||||||
0.16,
|
|
||||||
] as never);
|
|
||||||
map.setPaintProperty(haloId, 'circle-radius', GLOBE_SHIP_CIRCLE_RADIUS_EXPR);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// halo: data-driven expressions are static — visibility handled by fast toggle above
|
||||||
|
|
||||||
if (!map.getLayer(outlineId)) {
|
if (!map.getLayer(outlineId)) {
|
||||||
try {
|
try {
|
||||||
@ -448,36 +425,8 @@ export function useGlobeShips(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Ship outline layer add failed:', e);
|
console.warn('Ship outline layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
map.setLayoutProperty(outlineId, 'visibility', visibility);
|
|
||||||
map.setLayoutProperty(outlineId, 'circle-sort-key', [
|
|
||||||
'case',
|
|
||||||
['all', ['==', ['get', 'selected'], 1], ['==', ['get', 'permitted'], 1]], 130,
|
|
||||||
['all', ['==', ['get', 'highlighted'], 1], ['==', ['get', 'permitted'], 1]], 125,
|
|
||||||
['==', ['get', 'permitted'], 1], 120,
|
|
||||||
['==', ['get', 'selected'], 1], 70,
|
|
||||||
['==', ['get', 'highlighted'], 1], 65,
|
|
||||||
30,
|
|
||||||
] as never);
|
|
||||||
map.setPaintProperty(outlineId, 'circle-stroke-color', [
|
|
||||||
'case',
|
|
||||||
['==', ['get', 'selected'], 1], 'rgba(14,234,255,0.95)',
|
|
||||||
['==', ['get', 'highlighted'], 1], 'rgba(245,158,11,0.95)',
|
|
||||||
['==', ['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.7,
|
|
||||||
] as never);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// outline: data-driven expressions are static — visibility handled by fast toggle
|
||||||
|
|
||||||
if (!map.getLayer(symbolId)) {
|
if (!map.getLayer(symbolId)) {
|
||||||
try {
|
try {
|
||||||
@ -538,29 +487,8 @@ export function useGlobeShips(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Ship symbol layer add failed:', e);
|
console.warn('Ship symbol layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
map.setLayoutProperty(symbolId, 'visibility', visibility);
|
|
||||||
map.setLayoutProperty(symbolId, 'symbol-sort-key', [
|
|
||||||
'case',
|
|
||||||
['all', ['==', ['get', 'selected'], 1], ['==', ['get', 'permitted'], 1]], 140,
|
|
||||||
['all', ['==', ['get', 'highlighted'], 1], ['==', ['get', 'permitted'], 1]], 135,
|
|
||||||
['==', ['get', 'permitted'], 1], 130,
|
|
||||||
['==', ['get', 'selected'], 1], 80,
|
|
||||||
['==', ['get', 'highlighted'], 1], 75,
|
|
||||||
45,
|
|
||||||
] as never);
|
|
||||||
map.setPaintProperty(symbolId, 'icon-opacity', [
|
|
||||||
'case',
|
|
||||||
['==', ['get', 'permitted'], 1], 1,
|
|
||||||
['==', ['get', 'selected'], 1], 0.86,
|
|
||||||
['==', ['get', 'highlighted'], 1], 0.82,
|
|
||||||
0.66,
|
|
||||||
] as never);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// symbol: data-driven expressions are static — visibility handled by fast toggle
|
||||||
|
|
||||||
const labelFilter = [
|
const labelFilter = [
|
||||||
'all',
|
'all',
|
||||||
@ -611,15 +539,8 @@ export function useGlobeShips(
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Ship label layer add failed:', e);
|
console.warn('Ship label layer add failed:', e);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
map.setLayoutProperty(labelId, 'visibility', labelVisibility);
|
|
||||||
map.setFilter(labelId, labelFilter as never);
|
|
||||||
map.setLayoutProperty(labelId, 'text-field', ['get', 'labelName'] as never);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// label: filter/text-field are static — visibility handled by fast toggle
|
||||||
|
|
||||||
// 레이어가 준비되었음을 알림 (projection과 무관 — 토글 버튼 활성화용)
|
// 레이어가 준비되었음을 알림 (projection과 무관 — 토글 버튼 활성화용)
|
||||||
onGlobeShipsReady?.(true);
|
onGlobeShipsReady?.(true);
|
||||||
@ -658,9 +579,7 @@ export function useGlobeShips(
|
|||||||
|
|
||||||
const hideHover = () => {
|
const hideHover = () => {
|
||||||
for (const id of [symbolId, outlineId, haloId]) {
|
for (const id of [symbolId, outlineId, haloId]) {
|
||||||
try {
|
guardedSetVisibility(map, id, 'none');
|
||||||
if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', 'none');
|
|
||||||
} catch { /* ignore */ }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -93,11 +93,19 @@ export function useMapInit(
|
|||||||
map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: true }), 'top-left');
|
map.addControl(new maplibregl.NavigationControl({ showZoom: true, showCompass: true }), 'top-left');
|
||||||
map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: 'metric' }), 'bottom-left');
|
map.addControl(new maplibregl.ScaleControl({ maxWidth: 120, unit: 'metric' }), 'bottom-left');
|
||||||
|
|
||||||
// MapLibre 내부 placement TypeError 방어
|
// MapLibre 내부 placement TypeError 방어 + globe easing 경고 억제
|
||||||
// symbol layer 추가/제거와 render cycle 간 타이밍 이슈로 발생하는 에러 억제
|
// symbol layer 추가/제거와 render cycle 간 타이밍 이슈로 발생하는 에러 억제
|
||||||
|
// globe projection에서 scrollZoom이 easeTo(around)를 호출하면 경고 발생 → 구조적 한계로 억제
|
||||||
{
|
{
|
||||||
const origRender = (map as unknown as { _render: (arg?: number) => void })._render;
|
const origRender = (map as unknown as { _render: (arg?: number) => void })._render;
|
||||||
|
const origWarn = console.warn;
|
||||||
(map as unknown as { _render: (arg?: number) => void })._render = function (arg?: number) {
|
(map as unknown as { _render: (arg?: number) => void })._render = function (arg?: number) {
|
||||||
|
// globe 모드에서 scrollZoom의 easeTo around 경고 억제
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn = function (...args: unknown[]) {
|
||||||
|
if (typeof args[0] === 'string' && (args[0] as string).includes('Easing around a point')) return;
|
||||||
|
origWarn.apply(console, args as [unknown, ...unknown[]]);
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
origRender.call(this, arg);
|
origRender.call(this, arg);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -105,6 +113,9 @@ export function useMapInit(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
|
} finally {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.warn = origWarn;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -111,7 +111,6 @@ export function useProjectionToggle(
|
|||||||
'pair-lines-ml',
|
'pair-lines-ml',
|
||||||
'fc-lines-ml',
|
'fc-lines-ml',
|
||||||
'pair-range-ml',
|
'pair-range-ml',
|
||||||
'fleet-circles-ml-fill',
|
|
||||||
'fleet-circles-ml',
|
'fleet-circles-ml',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,6 @@ export function removeSourceIfExists(map: maplibregl.Map, sourceId: string) {
|
|||||||
const GLOBE_NATIVE_LAYER_IDS = [
|
const GLOBE_NATIVE_LAYER_IDS = [
|
||||||
'pair-lines-ml',
|
'pair-lines-ml',
|
||||||
'fc-lines-ml',
|
'fc-lines-ml',
|
||||||
'fleet-circles-ml-fill',
|
|
||||||
'fleet-circles-ml',
|
'fleet-circles-ml',
|
||||||
'pair-range-ml',
|
'pair-range-ml',
|
||||||
'subcables-hitarea',
|
'subcables-hitarea',
|
||||||
@ -44,7 +43,6 @@ const GLOBE_NATIVE_SOURCE_IDS = [
|
|||||||
'pair-lines-ml-src',
|
'pair-lines-ml-src',
|
||||||
'fc-lines-ml-src',
|
'fc-lines-ml-src',
|
||||||
'fleet-circles-ml-src',
|
'fleet-circles-ml-src',
|
||||||
'fleet-circles-ml-fill-src',
|
|
||||||
'pair-range-ml-src',
|
'pair-range-ml-src',
|
||||||
'subcables-src',
|
'subcables-src',
|
||||||
'subcables-pts-src',
|
'subcables-pts-src',
|
||||||
@ -96,6 +94,22 @@ export function setLayerVisibility(map: maplibregl.Map, layerId: string, visible
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* setLayoutProperty('visibility') wrapper — 현재 값과 동일하면 호출 생략.
|
||||||
|
* MapLibre는 setLayoutProperty 호출 시 항상 style._changed = true를 설정하여
|
||||||
|
* 모든 symbol layer의 placement를 재계산시킴. text-allow-overlap:false 라벨이
|
||||||
|
* 충돌 검사에 의해 사라지는 문제를 방지하기 위해, 값이 실제로 바뀔 때만 호출.
|
||||||
|
*/
|
||||||
|
export function guardedSetVisibility(map: maplibregl.Map, layerId: string, target: 'visible' | 'none') {
|
||||||
|
if (!map.getLayer(layerId)) return;
|
||||||
|
try {
|
||||||
|
if (map.getLayoutProperty(layerId, 'visibility') === target) return;
|
||||||
|
map.setLayoutProperty(layerId, 'visibility', target);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function cleanupLayers(
|
export function cleanupLayers(
|
||||||
map: maplibregl.Map,
|
map: maplibregl.Map,
|
||||||
layerIds: string[],
|
layerIds: string[],
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user