perf(map): 줌 기반 LOD + 심해 등심선 제거
- applyLandLayerLOD: 베이스맵 육지 레이어에 minzoom 적용 (landcover z9, transportation z8, building z14 등) - 수심 3-tier LOD: coarse(z3-7), medium(z7-9), detail(z9+) - shallowFilter: depth >= -2000, 심해 feature GPU 미전달 - applyDepthGradient ascending order 에러 수정 - vertex 경고 passthrough (디버깅용 유지) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
d5700ba587
커밋
b022e4bc36
@ -110,7 +110,7 @@ export function useBaseMapToggle(
|
||||
if (!map) return;
|
||||
if (showSeamark) {
|
||||
try {
|
||||
ensureSeamarkOverlay(map, 'bathymetry-lines');
|
||||
ensureSeamarkOverlay(map, 'bathymetry-lines-coarse');
|
||||
map.setPaintProperty('seamark', 'raster-opacity', 0.85);
|
||||
} catch {
|
||||
// ignore until style is ready
|
||||
|
||||
@ -103,7 +103,15 @@ export function useMapInit(
|
||||
// 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;
|
||||
if (typeof args[0] === 'string') {
|
||||
const msg = args[0] as string;
|
||||
if (msg.includes('Easing around a point')) return;
|
||||
// vertex 경고는 디버그용으로 1회만 출력 후 억제
|
||||
if (msg.includes('Max vertices per segment')) {
|
||||
origWarn.apply(console, args as [unknown, ...unknown[]]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
origWarn.apply(console, args as [unknown, ...unknown[]]);
|
||||
};
|
||||
try {
|
||||
@ -177,7 +185,7 @@ export function useMapInit(
|
||||
}
|
||||
if (!showSeamarkRef.current) return;
|
||||
try {
|
||||
ensureSeamarkOverlay(map!, 'bathymetry-lines');
|
||||
ensureSeamarkOverlay(map!, 'bathymetry-lines-coarse');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@ -203,7 +211,7 @@ export function useMapInit(
|
||||
map.once('load', () => {
|
||||
if (showSeamarkRef.current) {
|
||||
try {
|
||||
ensureSeamarkOverlay(map!, 'bathymetry-lines');
|
||||
ensureSeamarkOverlay(map!, 'bathymetry-lines-coarse');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
@ -35,7 +35,7 @@ function applyLabelLanguage(map: maplibregl.Map, lang: MapLabelLanguage) {
|
||||
if (layer.type !== 'symbol') continue;
|
||||
const layout = (layer as { layout?: Record<string, unknown> }).layout;
|
||||
if (!layout?.['text-field']) continue;
|
||||
if (layer.id === 'bathymetry-labels') continue;
|
||||
if (layer.id.startsWith('bathymetry-labels')) continue;
|
||||
const textField =
|
||||
lang === 'local'
|
||||
? ['get', 'name']
|
||||
@ -105,14 +105,16 @@ function applyDepthGradient(map: maplibregl.Map, stops: DepthColorStop[]) {
|
||||
if (!map.getLayer('bathymetry-fill')) return;
|
||||
const depth = ['to-number', ['get', 'depth']];
|
||||
const sorted = [...stops].sort((a, b) => a.depth - b.depth);
|
||||
if (sorted.length === 0) return;
|
||||
const expr: unknown[] = ['interpolate', ['linear'], depth];
|
||||
const deepest = sorted[0];
|
||||
if (deepest) expr.push(-11000, darkenHex(deepest.color, 0.5));
|
||||
for (const s of sorted) {
|
||||
expr.push(s.depth, s.color);
|
||||
}
|
||||
// 0m까지 확장 (최천층 stop이 0보다 깊으면)
|
||||
const shallowest = sorted[sorted.length - 1];
|
||||
if (shallowest) expr.push(0, lightenHex(shallowest.color, 1.8));
|
||||
if (shallowest.depth < 0) {
|
||||
expr.push(0, lightenHex(shallowest.color, 1.8));
|
||||
}
|
||||
try {
|
||||
map.setPaintProperty('bathymetry-fill', 'fill-color', expr as never);
|
||||
} catch {
|
||||
@ -122,7 +124,7 @@ function applyDepthGradient(map: maplibregl.Map, stops: DepthColorStop[]) {
|
||||
|
||||
function applyDepthFontSize(map: maplibregl.Map, size: DepthFontSize) {
|
||||
const expr = DEPTH_FONT_SIZE_MAP[size];
|
||||
for (const layerId of ['bathymetry-labels', 'bathymetry-landforms']) {
|
||||
for (const layerId of ['bathymetry-labels', 'bathymetry-labels-coarse', 'bathymetry-landforms']) {
|
||||
if (!map.getLayer(layerId)) continue;
|
||||
try {
|
||||
map.setLayoutProperty(layerId, 'text-size', expr);
|
||||
@ -133,7 +135,7 @@ function applyDepthFontSize(map: maplibregl.Map, size: DepthFontSize) {
|
||||
}
|
||||
|
||||
function applyDepthFontColor(map: maplibregl.Map, color: string) {
|
||||
for (const layerId of ['bathymetry-labels', 'bathymetry-landforms']) {
|
||||
for (const layerId of ['bathymetry-labels', 'bathymetry-labels-coarse', 'bathymetry-landforms']) {
|
||||
if (!map.getLayer(layerId)) continue;
|
||||
try {
|
||||
map.setPaintProperty(layerId, 'text-color', color);
|
||||
|
||||
@ -11,10 +11,53 @@ export const SHALLOW_WATER_LINE_DEFAULT = '#114f5c';
|
||||
|
||||
const BATHY_ZOOM_RANGES: BathyZoomRange[] = [
|
||||
{ id: 'bathymetry-fill', mercator: [3, 24], globe: [3, 24] },
|
||||
{ id: 'bathymetry-borders', mercator: [5, 24], globe: [5, 24] },
|
||||
{ id: 'bathymetry-borders-major', mercator: [3, 24], globe: [3, 24] },
|
||||
{ id: 'bathymetry-borders-major', mercator: [3, 7], globe: [3, 7] },
|
||||
{ id: 'bathymetry-borders', mercator: [7, 24], globe: [7, 24] },
|
||||
{ id: 'bathymetry-lines-coarse', mercator: [5, 7], globe: [5, 7] },
|
||||
{ id: 'bathymetry-lines-major', mercator: [7, 9], globe: [7, 9] },
|
||||
{ id: 'bathymetry-lines-detail', mercator: [9, 24], globe: [9, 24] },
|
||||
{ id: 'bathymetry-labels-coarse', mercator: [6, 9], globe: [6, 9] },
|
||||
{ id: 'bathymetry-labels', mercator: [9, 24], globe: [9, 24] },
|
||||
];
|
||||
|
||||
/**
|
||||
* 줌 기반 LOD — 줌아웃 시 vertex가 폭증하는 육지 레이어의 minzoom을 올려
|
||||
* 광역 뷰에서는 생략하고, 줌인 시 자연스럽게 디테일이 나타나도록 함.
|
||||
* 해양 서비스 특성상 육지 디테일은 연안 확대 시에만 필요.
|
||||
*/
|
||||
function applyLandLayerLOD(style: StyleSpecification): void {
|
||||
if (!style.layers || !Array.isArray(style.layers)) return;
|
||||
|
||||
// source-layer → 렌더링을 시작할 최소 줌 레벨
|
||||
// globe 모드 줌아웃 시 vertex 65535 초과로 GPU 렌더링 아티팩트(노란 막대) 방지
|
||||
const LOD_MINZOOM: Record<string, number> = {
|
||||
'landcover': 9,
|
||||
'globallandcover': 9,
|
||||
'landuse': 11,
|
||||
'boundary': 5,
|
||||
'transportation': 8,
|
||||
'transportation_name': 10,
|
||||
'building': 14,
|
||||
'housenumber': 16,
|
||||
'aeroway': 11,
|
||||
'park': 10,
|
||||
'mountain_peak': 11,
|
||||
};
|
||||
|
||||
for (const layer of style.layers as unknown as LayerSpecification[]) {
|
||||
const spec = layer as Record<string, unknown>;
|
||||
const sourceLayer = spec['source-layer'] as string | undefined;
|
||||
if (!sourceLayer) continue;
|
||||
const lodMin = LOD_MINZOOM[sourceLayer];
|
||||
if (lodMin === undefined) continue;
|
||||
// 기존 minzoom보다 높을 때만 덮어씀
|
||||
const current = (spec.minzoom as number) ?? 0;
|
||||
if (lodMin > current) {
|
||||
spec.minzoom = lodMin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerKey: string) {
|
||||
const oceanSourceId = 'maptiler-ocean';
|
||||
|
||||
@ -31,19 +74,11 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
|
||||
const depth = ['to-number', ['get', 'depth']] as unknown as number[];
|
||||
const depthLabel = ['concat', ['to-string', ['*', depth, -1]], 'm'] as unknown as string[];
|
||||
|
||||
// Bug #3 fix: shallow depths now use brighter teal tones to distinguish from deep ocean
|
||||
// 수심 색상: -2000m에서 절단 — 심해는 베이스 수색과 동일하게 처리
|
||||
const bathyFillColor = [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
depth,
|
||||
-11000,
|
||||
'#00040b',
|
||||
-8000,
|
||||
'#010610',
|
||||
-6000,
|
||||
'#020816',
|
||||
-4000,
|
||||
'#030c1c',
|
||||
-2000,
|
||||
'#041022',
|
||||
-1000,
|
||||
@ -64,6 +99,17 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
|
||||
'#2097a6',
|
||||
] as const;
|
||||
|
||||
// depth >= -2000 필터: -2000m보다 깊은 등심선은 GPU에 전달하지 않음
|
||||
const shallowFilter = ['>=', depth, -2000] as unknown[];
|
||||
|
||||
// --- Depth tiers for zoom-based LOD ---
|
||||
const DEPTHS_COARSE = [-1000, -2000];
|
||||
const DEPTHS_MEDIUM = [-100, -500, -1000, -2000];
|
||||
const DEPTHS_DETAIL = [-50, -100, -200, -500, -1000, -2000];
|
||||
const depthIn = (depths: number[]) =>
|
||||
['all', shallowFilter, ['in', depth, ['literal', depths]]] as unknown[];
|
||||
|
||||
// === Fill (contour polygons) — 단일 레이어, shallowFilter만 적용 ===
|
||||
const bathyFill: LayerSpecification = {
|
||||
id: 'bathymetry-fill',
|
||||
type: 'fill',
|
||||
@ -71,104 +117,140 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
|
||||
'source-layer': 'contour',
|
||||
minzoom: 3,
|
||||
maxzoom: 24,
|
||||
filter: shallowFilter as unknown as unknown[],
|
||||
paint: {
|
||||
'fill-color': bathyFillColor,
|
||||
'fill-opacity': ['interpolate', ['linear'], ['zoom'], 0, 0.9, 5, 0.88, 8, 0.84, 10, 0.78],
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const bathyBandBorders: LayerSpecification = {
|
||||
// === Borders (contour polygon edges) — 2-tier LOD ===
|
||||
// z3-z7: 1000m, 2000m 경계만
|
||||
const bathyBordersMajor: LayerSpecification = {
|
||||
id: 'bathymetry-borders-major',
|
||||
type: 'line',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour',
|
||||
minzoom: 3,
|
||||
maxzoom: 7,
|
||||
filter: depthIn(DEPTHS_COARSE) as unknown as unknown[],
|
||||
paint: {
|
||||
'line-color': 'rgba(255,255,255,0.14)',
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 3, 0.10, 6, 0.16],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 3, 0.5, 6, 0.35],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 3, 0.25, 6, 0.4],
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
// z7+: 전체 shallow 등심선 경계
|
||||
const bathyBorders: LayerSpecification = {
|
||||
id: 'bathymetry-borders',
|
||||
type: 'line',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour',
|
||||
minzoom: 5, // fill은 3부터, borders는 5부터
|
||||
minzoom: 7,
|
||||
maxzoom: 24,
|
||||
filter: shallowFilter as unknown as unknown[],
|
||||
paint: {
|
||||
'line-color': 'rgba(255,255,255,0.06)',
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 4, 0.12, 8, 0.18, 12, 0.22],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 4, 0.8, 10, 0.2],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 4, 0.2, 8, 0.35, 12, 0.6],
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 7, 0.18, 12, 0.22],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 7, 0.3, 10, 0.2],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 7, 0.35, 12, 0.6],
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const bathyLinesMinor: LayerSpecification = {
|
||||
id: 'bathymetry-lines',
|
||||
// === Contour lines (contour_line) — 3-tier LOD ===
|
||||
// z5-z7: 1000m, 2000m만
|
||||
const bathyLinesCoarse: LayerSpecification = {
|
||||
id: 'bathymetry-lines-coarse',
|
||||
type: 'line',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour_line',
|
||||
minzoom: 7,
|
||||
minzoom: 5,
|
||||
maxzoom: 7,
|
||||
filter: depthIn(DEPTHS_COARSE) as unknown as unknown[],
|
||||
paint: {
|
||||
'line-color': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
depth,
|
||||
-11000,
|
||||
'rgba(255,255,255,0.04)',
|
||||
-6000,
|
||||
'rgba(255,255,255,0.05)',
|
||||
-2000,
|
||||
'rgba(255,255,255,0.07)',
|
||||
0,
|
||||
'rgba(255,255,255,0.10)',
|
||||
],
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 8, 0.18, 10, 0.22, 12, 0.28],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 8, 0.8, 11, 0.3],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.35, 10, 0.55, 12, 0.85],
|
||||
'line-color': 'rgba(255,255,255,0.12)',
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 5, 0.15, 7, 0.22],
|
||||
'line-blur': 0.5,
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 5, 0.4, 7, 0.6],
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const majorDepths = [-50, -100, -200, -500, -1000, -2000, -4000, -6000, -8000, -9500];
|
||||
const bathyMajorDepthFilter: unknown[] = [
|
||||
'in',
|
||||
['to-number', ['get', 'depth']],
|
||||
['literal', majorDepths],
|
||||
] as unknown[];
|
||||
|
||||
// z7-z9: 100, 500, 1000, 2000m
|
||||
const bathyLinesMajor: LayerSpecification = {
|
||||
id: 'bathymetry-lines-major',
|
||||
type: 'line',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour_line',
|
||||
minzoom: 7,
|
||||
maxzoom: 24,
|
||||
filter: bathyMajorDepthFilter as unknown as unknown[],
|
||||
maxzoom: 9,
|
||||
filter: depthIn(DEPTHS_MEDIUM) as unknown as unknown[],
|
||||
paint: {
|
||||
'line-color': 'rgba(255,255,255,0.16)',
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 8, 0.22, 10, 0.28, 12, 0.34],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 8, 0.4, 11, 0.2],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.6, 10, 0.95, 12, 1.3],
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 7, 0.22, 9, 0.28],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 7, 0.4, 9, 0.2],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 7, 0.6, 9, 0.95],
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
const bathyBandBordersMajor: LayerSpecification = {
|
||||
id: 'bathymetry-borders-major',
|
||||
// z9+: 50, 100, 200, 500, 1000, 2000m (풀 디테일)
|
||||
const bathyLinesDetail: LayerSpecification = {
|
||||
id: 'bathymetry-lines-detail',
|
||||
type: 'line',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour',
|
||||
minzoom: 3,
|
||||
'source-layer': 'contour_line',
|
||||
minzoom: 9,
|
||||
maxzoom: 24,
|
||||
filter: bathyMajorDepthFilter as unknown as unknown[],
|
||||
filter: depthIn(DEPTHS_DETAIL) as unknown as unknown[],
|
||||
paint: {
|
||||
'line-color': 'rgba(255,255,255,0.14)',
|
||||
'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],
|
||||
'line-color': 'rgba(255,255,255,0.16)',
|
||||
'line-opacity': ['interpolate', ['linear'], ['zoom'], 9, 0.28, 12, 0.34],
|
||||
'line-blur': ['interpolate', ['linear'], ['zoom'], 9, 0.2, 11, 0.15],
|
||||
'line-width': ['interpolate', ['linear'], ['zoom'], 9, 0.95, 12, 1.3],
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
// === Labels — 2-tier LOD ===
|
||||
// z6-z9: 1000m, 2000m 라벨만
|
||||
const bathyLabelsCoarse: LayerSpecification = {
|
||||
id: 'bathymetry-labels-coarse',
|
||||
type: 'symbol',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour_line',
|
||||
minzoom: 6,
|
||||
maxzoom: 9,
|
||||
filter: depthIn(DEPTHS_COARSE) as unknown as unknown[],
|
||||
layout: {
|
||||
'symbol-placement': 'line',
|
||||
'text-field': depthLabel,
|
||||
'text-font': ['Noto Sans Regular', 'Open Sans Regular'],
|
||||
'text-size': ['interpolate', ['linear'], ['zoom'], 6, 10, 9, 12],
|
||||
'text-allow-overlap': false,
|
||||
'text-padding': 4,
|
||||
'text-rotation-alignment': 'map',
|
||||
},
|
||||
paint: {
|
||||
'text-color': 'rgba(226,232,240,0.78)',
|
||||
'text-halo-color': 'rgba(2,6,23,0.88)',
|
||||
'text-halo-width': 1.2,
|
||||
'text-halo-blur': 0.5,
|
||||
},
|
||||
} as unknown as LayerSpecification;
|
||||
|
||||
// z9+: 100, 500, 1000, 2000m 라벨
|
||||
const bathyLabels: LayerSpecification = {
|
||||
id: 'bathymetry-labels',
|
||||
type: 'symbol',
|
||||
source: oceanSourceId,
|
||||
'source-layer': 'contour_line',
|
||||
minzoom: 7,
|
||||
filter: bathyMajorDepthFilter as unknown as unknown[],
|
||||
minzoom: 9,
|
||||
filter: depthIn(DEPTHS_MEDIUM) as unknown as unknown[],
|
||||
layout: {
|
||||
'symbol-placement': 'line',
|
||||
'text-field': depthLabel,
|
||||
'text-font': ['Noto Sans Regular', 'Open Sans Regular'],
|
||||
'text-size': ['interpolate', ['linear'], ['zoom'], 7, 10, 9, 12, 11, 14, 13, 16],
|
||||
'text-size': ['interpolate', ['linear'], ['zoom'], 9, 12, 11, 14, 13, 16],
|
||||
'text-allow-overlap': false,
|
||||
'text-padding': 4,
|
||||
'text-rotation-alignment': 'map',
|
||||
@ -244,10 +326,12 @@ export function injectOceanBathymetryLayers(style: StyleSpecification, maptilerK
|
||||
|
||||
const toInsert = [
|
||||
bathyFill,
|
||||
bathyBandBorders,
|
||||
bathyBandBordersMajor,
|
||||
bathyLinesMinor,
|
||||
bathyBordersMajor,
|
||||
bathyBorders,
|
||||
bathyLinesCoarse,
|
||||
bathyLinesMajor,
|
||||
bathyLinesDetail,
|
||||
bathyLabelsCoarse,
|
||||
bathyLabels,
|
||||
landformLabels,
|
||||
].filter((l) => !existingIds.has(l.id));
|
||||
@ -298,6 +382,7 @@ export async function resolveInitialMapStyle(signal: AbortSignal): Promise<strin
|
||||
const res = await fetch(styleUrl, { signal, headers: { accept: 'application/json' } });
|
||||
if (!res.ok) throw new Error(`MapTiler style fetch failed: ${res.status} ${res.statusText}`);
|
||||
const json = (await res.json()) as StyleSpecification;
|
||||
applyLandLayerLOD(json);
|
||||
applyKoreanLabels(json);
|
||||
injectOceanBathymetryLayers(json, key);
|
||||
return json;
|
||||
|
||||
불러오는 중...
Reference in New Issue
Block a user