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