fix(map): 지도 설정 패널 개선
- 육지색 적용 범위 확대 (background + 전체 fill 레이어) - UI 가독성 개선: 라벨 10px, 색상 대비 강화 - 수심 구간 '자동채우기' 토글 추가 (최소/최대 기준 보간) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
부모
650888adb7
커밋
1a3dd82eb4
@ -971,7 +971,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-settings-panel .ms-title {
|
.map-settings-panel .ms-title {
|
||||||
font-size: 10px;
|
font-size: 11px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
@ -983,11 +983,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-settings-panel .ms-label {
|
.map-settings-panel .ms-label {
|
||||||
font-size: 8px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
color: var(--muted);
|
color: var(--text);
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-settings-panel .ms-row {
|
.map-settings-panel .ms-row {
|
||||||
@ -1019,12 +1019,12 @@ body {
|
|||||||
|
|
||||||
.map-settings-panel .ms-hex {
|
.map-settings-panel .ms-hex {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: var(--muted);
|
color: #94a3b8;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-settings-panel .ms-depth-label {
|
.map-settings-panel .ms-depth-label {
|
||||||
font-size: 9px;
|
font-size: 10px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import type { MapStyleSettings, MapLabelLanguage, DepthFontSize } from './types';
|
import type { MapStyleSettings, MapLabelLanguage, DepthFontSize, DepthColorStop } from './types';
|
||||||
import { DEFAULT_MAP_STYLE_SETTINGS } from './types';
|
import { DEFAULT_MAP_STYLE_SETTINGS } from './types';
|
||||||
|
|
||||||
interface MapSettingsPanelProps {
|
interface MapSettingsPanelProps {
|
||||||
@ -25,8 +25,42 @@ function depthLabel(depth: number): string {
|
|||||||
return `${Math.abs(depth).toLocaleString()}m`;
|
return `${Math.abs(depth).toLocaleString()}m`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hexToRgb(hex: string): [number, number, number] {
|
||||||
|
return [
|
||||||
|
parseInt(hex.slice(1, 3), 16),
|
||||||
|
parseInt(hex.slice(3, 5), 16),
|
||||||
|
parseInt(hex.slice(5, 7), 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function rgbToHex(r: number, g: number, b: number): string {
|
||||||
|
return `#${[r, g, b].map((c) => Math.round(Math.max(0, Math.min(255, c))).toString(16).padStart(2, '0')).join('')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpolateGradient(stops: DepthColorStop[]): DepthColorStop[] {
|
||||||
|
if (stops.length < 2) return stops;
|
||||||
|
const sorted = [...stops].sort((a, b) => a.depth - b.depth);
|
||||||
|
const first = sorted[0];
|
||||||
|
const last = sorted[sorted.length - 1];
|
||||||
|
const [r1, g1, b1] = hexToRgb(first.color);
|
||||||
|
const [r2, g2, b2] = hexToRgb(last.color);
|
||||||
|
return sorted.map((stop, i) => {
|
||||||
|
if (i === 0 || i === sorted.length - 1) return stop;
|
||||||
|
const t = i / (sorted.length - 1);
|
||||||
|
return {
|
||||||
|
depth: stop.depth,
|
||||||
|
color: rgbToHex(
|
||||||
|
r1 + (r2 - r1) * t,
|
||||||
|
g1 + (g2 - g1) * t,
|
||||||
|
b1 + (b2 - b1) * t,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function MapSettingsPanel({ value, onChange }: MapSettingsPanelProps) {
|
export function MapSettingsPanel({ value, onChange }: MapSettingsPanelProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [autoGradient, setAutoGradient] = useState(false);
|
||||||
|
|
||||||
const update = <K extends keyof MapStyleSettings>(key: K, val: MapStyleSettings[K]) => {
|
const update = <K extends keyof MapStyleSettings>(key: K, val: MapStyleSettings[K]) => {
|
||||||
onChange({ ...value, [key]: val });
|
onChange({ ...value, [key]: val });
|
||||||
@ -34,7 +68,19 @@ export function MapSettingsPanel({ value, onChange }: MapSettingsPanelProps) {
|
|||||||
|
|
||||||
const updateDepthStop = (index: number, color: string) => {
|
const updateDepthStop = (index: number, color: string) => {
|
||||||
const next = value.depthStops.map((s, i) => (i === index ? { ...s, color } : s));
|
const next = value.depthStops.map((s, i) => (i === index ? { ...s, color } : s));
|
||||||
|
if (autoGradient && (index === 0 || index === next.length - 1)) {
|
||||||
|
update('depthStops', interpolateGradient(next));
|
||||||
|
} else {
|
||||||
update('depthStops', next);
|
update('depthStops', next);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleAutoGradient = () => {
|
||||||
|
const next = !autoGradient;
|
||||||
|
setAutoGradient(next);
|
||||||
|
if (next) {
|
||||||
|
update('depthStops', interpolateGradient(value.depthStops));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -97,19 +143,34 @@ export function MapSettingsPanel({ value, onChange }: MapSettingsPanelProps) {
|
|||||||
|
|
||||||
{/* ── Depth gradient ────────────────────────────── */}
|
{/* ── Depth gradient ────────────────────────────── */}
|
||||||
<div className="ms-section">
|
<div className="ms-section">
|
||||||
<div className="ms-label">수심 구간 색상</div>
|
<div className="ms-label" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
{value.depthStops.map((stop, i) => (
|
수심 구간 색상
|
||||||
<div className="ms-row" key={stop.depth}>
|
<span
|
||||||
|
className={`tog-btn${autoGradient ? ' on' : ''}`}
|
||||||
|
style={{ fontSize: 8, padding: '1px 5px', marginLeft: 8 }}
|
||||||
|
onClick={toggleAutoGradient}
|
||||||
|
title="최소/최대 색상 기준으로 중간 구간을 자동 보간합니다"
|
||||||
|
>
|
||||||
|
자동채우기
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{value.depthStops.map((stop, i) => {
|
||||||
|
const isEdge = i === 0 || i === value.depthStops.length - 1;
|
||||||
|
const dimmed = autoGradient && !isEdge;
|
||||||
|
return (
|
||||||
|
<div className="ms-row" key={stop.depth} style={dimmed ? { opacity: 0.5 } : undefined}>
|
||||||
<span className="ms-depth-label">{depthLabel(stop.depth)}</span>
|
<span className="ms-depth-label">{depthLabel(stop.depth)}</span>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
className="ms-color-input"
|
className="ms-color-input"
|
||||||
value={stop.color}
|
value={stop.color}
|
||||||
onChange={(e) => updateDepthStop(i, e.target.value)}
|
onChange={(e) => updateDepthStop(i, e.target.value)}
|
||||||
|
disabled={dimmed}
|
||||||
/>
|
/>
|
||||||
<span className="ms-hex">{stop.color}</span>
|
<span className="ms-hex">{stop.color}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Depth font size ───────────────────────────── */}
|
{/* ── Depth font size ───────────────────────────── */}
|
||||||
@ -146,7 +207,10 @@ export function MapSettingsPanel({ value, onChange }: MapSettingsPanelProps) {
|
|||||||
<button
|
<button
|
||||||
className="ms-reset"
|
className="ms-reset"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onChange(DEFAULT_MAP_STYLE_SETTINGS)}
|
onClick={() => {
|
||||||
|
onChange(DEFAULT_MAP_STYLE_SETTINGS);
|
||||||
|
setAutoGradient(false);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
초기화
|
초기화
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -51,14 +51,21 @@ function applyLabelLanguage(map: maplibregl.Map, lang: MapLabelLanguage) {
|
|||||||
function applyLandColor(map: maplibregl.Map, color: string) {
|
function applyLandColor(map: maplibregl.Map, color: string) {
|
||||||
const style = map.getStyle();
|
const style = map.getStyle();
|
||||||
if (!style?.layers) return;
|
if (!style?.layers) return;
|
||||||
const landRegex = /(land|landcover|landuse|earth|continent|terrain|park)/i;
|
const waterRegex = /(water|sea|ocean|river|lake|coast|bay)/i;
|
||||||
|
const darkVariant = darkenHex(color, 0.8);
|
||||||
for (const layer of style.layers) {
|
for (const layer of style.layers) {
|
||||||
if (layer.type !== 'fill') continue;
|
|
||||||
const id = layer.id;
|
const id = layer.id;
|
||||||
|
if (id.startsWith('bathymetry-')) continue;
|
||||||
|
if (id.startsWith('subcables-')) continue;
|
||||||
const sourceLayer = String((layer as Record<string, unknown>)['source-layer'] ?? '');
|
const sourceLayer = String((layer as Record<string, unknown>)['source-layer'] ?? '');
|
||||||
if (!landRegex.test(id) && !landRegex.test(sourceLayer)) continue;
|
const isWater = waterRegex.test(id) || waterRegex.test(sourceLayer);
|
||||||
|
if (isWater) continue;
|
||||||
try {
|
try {
|
||||||
map.setPaintProperty(id, 'fill-color', color);
|
if (layer.type === 'background') {
|
||||||
|
map.setPaintProperty(id, 'background-color', color);
|
||||||
|
} else if (layer.type === 'fill') {
|
||||||
|
map.setPaintProperty(id, 'fill-color', darkVariant);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
불러오는 중...
Reference in New Issue
Block a user