From 6b91474ee774a758418bd6cc4de6a7e07c145418 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 30 Oct 2026 18:00:59 +0900 Subject: [PATCH] =?UTF-8?q?feat(D=20=EC=98=88=EC=A0=95=EB=85=B8=EC=84=A0):?= =?UTF-8?q?=20=EC=9C=84=EC=84=B1=20=EB=B0=94=ED=83=95=20=EC=A1=B0=EA=B8=88?= =?UTF-8?q?=20=EC=96=B4=EB=91=A1=EA=B3=A0=20=EC=97=B0=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?+=20=EB=B2=94=EB=A1=80=20=E3=80=8C=EC=9C=84=EC=84=B1=20?= =?UTF-8?q?=EB=B0=9D=EA=B8=B0=E3=80=8D=20=EC=A1=B0=EC=A0=88=20=EC=B9=B8=20?= =?UTF-8?q?=C2=B7=20=EA=B0=88=EB=9E=98=20=EC=84=A0=20=EA=B5=B5=EA=B8=B0=20?= =?UTF-8?q?3=20(PLAN=20606-1=20=C2=B7=20=EC=9A=94=EA=B5=AC=20473=20=C2=B7?= =?UTF-8?q?=20475)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 5.5 --- .../D03_PlanAnalysis_UI_DrawPaint.ts | 2 +- .../D03_PlanAnalysis_UI_Layers.ts | 13 +++++- D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts | 28 ++++++++++--- .../D03_PlanAnalysis_UI_SatLight.ts | 39 ++++++++++++++++++ resources/tester/test_606_1_sat_light.py | 40 +++++++++++++++++++ 5 files changed, 114 insertions(+), 8 deletions(-) create mode 100644 D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts create mode 100644 resources/tester/test_606_1_sat_light.py diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts index 10cb635cc..462468e09 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts @@ -130,7 +130,7 @@ export function paintNetwork( network.lines.forEach((line, index) => { if (line.vertices.length < 2) return; const color = index === active ? colors.active : routeColor(index); - const width = line.branch ? 2.5 : 3; + const width = 3; const path = (vertices: ReadonlyArray): void => { context.beginPath(); vertices.forEach((v, i) => { diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts index 91d33f7fa..ec5041073 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts @@ -73,6 +73,7 @@ import { import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend"; import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint"; import { createGlBase, GL_FONT, type GlImage } from "./D03_PlanAnalysis_UI_MapGl"; +import { createSatLightRow, readSatLight } from "./D03_PlanAnalysis_UI_SatLight"; import { openSlopeBands, paintSlope, @@ -208,11 +209,19 @@ export function createPlanLayers(opts: { for (const def of LAYERS) gl.setVisible(def.key, on[def.key]); gl.setData("hillshade", hillshade && (on.satellite ? hillshade.shade : hillshade.plain)); const rows = meta ? legendRows(on, data) : []; - legend.replaceChildren(...rows.map((row) => createLegend(row))); + const light = meta && glOn && on.satellite; // 옛 캔버스 = 위성 없음 + legend.replaceChildren(...rows.map((row) => createLegend(row)), ...(light ? [lightRow] : [])); if (meta && on.slope && data.slope) legend.firstElementChild?.append(gear); - legend.hidden = !rows.length; + legend.hidden = !rows.length && !light; } + /** 위성 밝기(요구 475) — 위성 켠 동안 범례 맨 아래 한 줄 · 바로 반영. */ + gl.setSatelliteLight(readSatLight()); + const lightRow = createSatLightRow(readSatLight(), (level) => { + gl.setSatelliteLight(level); + map.redrawBase(); + }); + /** 급경사 범위(603) — 바꾸면 범례 바로 · 그림은 다시 칠함(늦은 칠하기는 버림). */ let slopeBands = readSlopeBands(); let slopeTurn = 0; diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts index ec78b180c..1d9ae8e04 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts @@ -24,6 +24,7 @@ import { import { themeColor } from "@ui/ui_template_palette"; import type { GeoJsonCollection } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import type { PlanLayerKey } from "./D03_PlanAnalysis_UI_LayerDefs"; +import { SAT_LIGHT_DEFAULT } from "./D03_PlanAnalysis_UI_SatLight"; export { GL_FONT, type GlImage }; @@ -38,6 +39,8 @@ export interface GlBase { setVisible: (key: PlanLayerKey, on: boolean) => void; /** 등고선 주곡선 간격(m) — 캔버스와 같은 「화면 안 가닥 수」 규칙. */ setContourStep: (step: number) => void; + /** 위성 밝기 상한(0~1) — 바로 반영 · 지도를 다시 세워도 유지. */ + setSatelliteLight: (level: number) => void; /** 켬 = 지도를 `parent` 맨 앞에 만듦(참 = 섬 · 거짓 = WebGL 못 씀) · null = 버림. */ enable: (parent: HTMLElement | null) => boolean; map: () => MapLibreMap | null; @@ -122,22 +125,27 @@ function fillEdge( } /** 겹 차례(아래 → 위)와 꼴 — 캔버스 `D03_PlanAnalysis_UI_Layers` 의 색 · 굵기 · 투명도 · 차례 그대로. */ -function layerSpecs(step: number): GlSpec[] { +function layerSpecs(step: number, light: number): GlSpec[] { const contour = themeColor("--draw-map-contour", "#ffd68c"); const place = themeColor("--draw-map-place", "#f8fafc"); const landslideZone = themeColor("--draw-map-landslide-zone", "#e11d48"); - const raster = (key: GlSource | "satellite", opacity: number): GlSpec => + const raster = ( + key: GlSource | "satellite", + opacity: number, + extra: Record = {}, + ): GlSpec => ({ key, id: key, type: "raster", source: key, - paint: { "raster-opacity": opacity, "raster-fade-duration": 0 }, + paint: { "raster-opacity": opacity, "raster-fade-duration": 0, ...extra }, }) as GlSpec; const lineKind = (key: "faults" | "lineaments" | "zeroOrder", width: number, pattern: number[]) => haloLine(key, key, themeColor(LINE_COLOR[key], "#f43f5e"), width, pattern); return [ - raster("satellite", 1), + // 위성 = 조금 어둡고 연하게(요구 475 · 노선 선 대비) — 밝기 상한은 사용자 조절 + raster("satellite", 1, { "raster-brightness-max": light, "raster-saturation": SAT_SATURATION }), raster("hillshade", 1), // 위성 위 = 「곱하기」와 같은 검은 그늘 그림(받는 쪽이 바꿈) raster("slope", 0.55), raster("landslide", 0.6), @@ -313,10 +321,14 @@ const THEME_NAMES = [ "--draw-map-parcel", ] as const; +/** 위성 채도(−1~1) — 조금 연하게. */ +const SAT_SATURATION = -0.3; + export function createGlBase(projectId: string, onFail: (why: string) => void): GlBase { let step = 5; + let light = SAT_LIGHT_DEFAULT; const base = createGlMap({ - specs: () => layerSpecs(step), + specs: () => layerSpecs(step, light), fixed: { satellite: satelliteTileSource(projectId) }, themeNames: THEME_NAMES, className: "d03-plan__gl", @@ -330,5 +342,11 @@ export function createGlBase(projectId: string, onFail: (why: string) => void): const map = base.map(); if (map?.getLayer("contour-minor")) map.setFilter("contour-minor", minorFilter(step)); }, + setSatelliteLight(next) { + light = next; + const map = base.map(); + if (map?.getLayer("satellite")) + map.setPaintProperty("satellite", "raster-brightness-max", next); + }, }; } diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts new file mode 100644 index 000000000..c5dab6be4 --- /dev/null +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts @@ -0,0 +1,39 @@ +/* ============================================================================= + * D03_PlanAnalysis_UI_SatLight.ts + * D03 위성 바탕 밝기(요구 475) — 노선 선 대비를 위해 위성을 조금 어둡게 · 사용자가 범례에서 조절. + * 값 = 밝기 상한(0.4~1) · 계정 화면 취향으로 기억. 칠하기 = `D03_PlanAnalysis_UI_MapGl` `setSatelliteLight`. + * ========================================================================== */ + +import { createSlider } from "@ui/ui_template_input"; +import { readByKey, writeByKey } from "../A00_Common/b_page_state"; + +const PREF_KEY = "aislo:pref:d03.satellite-light"; +export const SAT_LIGHT_MIN = 0.4; +export const SAT_LIGHT_MAX = 1; +export const SAT_LIGHT_DEFAULT = 0.8; + +/** 기억한 밝기 — 없거나 깨지면 처음 값. */ +export function readSatLight(): number { + const value = Number(readByKey(PREF_KEY)); + return readByKey(PREF_KEY) !== null && value >= SAT_LIGHT_MIN && value <= SAT_LIGHT_MAX + ? value + : SAT_LIGHT_DEFAULT; +} + +/** 범례 「위성 밝기」 칸 — 움직이면 바로 `onChange` · 기억. */ +export function createSatLightRow(value: number, onChange: (value: number) => void): HTMLElement { + const { root, input } = createSlider({ + label: "위성 밝기", + min: SAT_LIGHT_MIN, + max: SAT_LIGHT_MAX, + step: 0.05, + value, + format: (v) => `${Math.round(v * 100)} %`, + onInput: (v) => { + writeByKey(PREF_KEY, String(v)); + onChange(v); + }, + }); + input.dataset.role = "satellite-light"; + return root; +} diff --git a/resources/tester/test_606_1_sat_light.py b/resources/tester/test_606_1_sat_light.py new file mode 100644 index 000000000..6263c3cc9 --- /dev/null +++ b/resources/tester/test_606_1_sat_light.py @@ -0,0 +1,40 @@ +"""606-1 위성 바탕 밝기 지킴 — D03 위성 칠에 밝기 상한 · 채도 · 사용자 조절 칸 · 갈래 굵기 3 (B04 · B05 위성은 그대로). + +./venv/Scripts/python.exe -m pytest resources/tester/test_606_1_sat_light.py -q +""" + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +read = lambda rel: (ROOT / rel).read_text(encoding="utf-8") # noqa: E731 + +MAPGL = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_MapGl.ts") +LIGHT = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_SatLight.ts") +LAYERS = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_Layers.ts") + + +def test_위성_칠에_밝기_채도(): + assert re.search(r'raster\("satellite", 1, \{[^}]*"raster-brightness-max": light', MAPGL) + assert '"raster-saturation": SAT_SATURATION' in MAPGL + + +def test_밝기_처음값은_어둡게_범위_안(): + m = {k: float(v) for k, v in re.findall(r"SAT_LIGHT_(MIN|MAX|DEFAULT) = ([\d.]+)", LIGHT)} + assert m["MIN"] < m["DEFAULT"] < m["MAX"] == 1 + + +def test_조절_칸은_GL_위성_켠_때만_기억(): + assert "createSatLightRow(" in LAYERS + assert "glOn && on.satellite" in LAYERS + assert "aislo:pref:d03.satellite-light" in LIGHT + + +def test_갈래_굵기_3(): + paint = read("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts") + assert "const width = 3;" in paint + assert "branch ? 2.5" not in paint + + +def test_공용_GL_바탕은_안_바뀜(): + assert "brightness" not in read("ui_template/ui_template_gl_map.ts")