/* ============================================================================= * D03_PlanAnalysis_UI_LayerPaint.ts * D03 바탕 겹 옛 캔버스 그리기(PLAN 569-2 · 581 · 584 · 586-2) — 그리기 차례 · 색 · 굵기 · 투명도. * GL 바탕(586-2)이 같은 꼴을 MapLibre 로 그림(`D03_PlanAnalysis_UI_MapGl`) · 이것은 GL 을 못 쓰거나 끈 때. * 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 켬끔 · GL 이면 쉼). * ========================================================================== */ import { themeColor } from "@ui/ui_template_palette"; import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { createNormalizer, drawPreparedLabels, lonLatToScreen, pickLevelStep, } from "../B04_PreProcess/B04_PreProcess_UI_MapRender"; import { LINE_KEYS, LINE_LAYERS, ZONE_COLORS, drawRaster, fillLayer, stroke, type PlanLayerKey, } from "./D03_PlanAnalysis_UI_LayerDefs"; import type { Prepared } from "./D03_PlanAnalysis_UI_LayerLegend"; import type { PlanMapLayer } from "./D03_PlanAnalysis_UI_Map"; /** 바탕 겹(아래 → 위) — 켬끔 · 받은 자료 · 위성 틀 · 위성 그림(음영 곱하기)을 그때그때 읽음. */ export function createCanvasLayers(opts: { on: Record; data: Partial; meta: () => VWorldMeta | null; satellite: () => HTMLImageElement | null; }): PlanMapLayer[] { const { on, data } = opts; /** 등고 글 폭 기억(글꼴 하나 · 582). */ const labelWidths = new Map(); return [ { base: true, draw: (context, map) => { if (!on.hillshade || !data.hillshade) return; // 위성 위 = 곱하기(밝은 곳은 그대로 · 그늘만 짙게) · 위성 없으면 그대로 if (on.satellite && opts.satellite()) { context.globalCompositeOperation = "multiply"; context.globalAlpha = 0.75; } drawRaster(context, map, data.hillshade); }, }, { base: true, draw: (context, map) => { if (!on.slope || !data.slope) return; context.globalAlpha = 0.55; drawRaster(context, map, data.slope); }, }, { base: true, draw: (context, map) => { if (!on.landslide || !data.landslide) return; context.globalAlpha = 0.6; drawRaster(context, map, data.landslide); }, }, { base: true, draw: (context, map) => { if (!on.soils || !data.soils) return; for (const { layer, color } of data.soils.kinds) { const fill = themeColor(color, "#facc15"); fillLayer(context, layer, map.view, fill, 0.35, { color: fill, width: 1.4 }); } }, }, { base: true, draw: (context, map) => { if (!on.zones || !data.zones) return; for (const [kind, layer] of data.zones.kinds) { const color = themeColor(ZONE_COLORS[kind] ?? "--draw-warning", "#eab308"); fillLayer(context, layer, map.view, color, 0.22, { color, width: 1.6 }); } }, }, { base: true, draw: (context, map) => { if (!on.bands || !data.bands) return; const road = themeColor("--draw-map-band-road", "#facc15"); const stream = themeColor("--draw-map-band-stream", "#22d3ee"); fillLayer(context, data.bands.road, map.view, road, 0.16, { color: road, width: 1.2, dash: [6, 4], }); fillLayer(context, data.bands.stream, map.view, stream, 0.18, { color: stream, width: 1.2, dash: [6, 4], }); }, }, { base: true, draw: (context, map) => { if (!on.cadastral || !data.cadastral) return; context.globalAlpha = 0.8; stroke(context, map, data.cadastral, themeColor("--draw-map-parcel", "#f0abfc"), 0.8); }, }, { base: true, draw: (context, map) => { if (!on.water || !data.water) return; const color = themeColor("--draw-map-water", "#0ea5e9"); fillLayer(context, data.water, map.view, color, 0.45, { color, width: 1 }); }, }, { base: true, draw: (context, map) => { if (!on.building || !data.building) return; const color = themeColor("--draw-map-building", "#fb7185"); fillLayer(context, data.building, map.view, color, 0.7, { color: "rgba(0, 0, 0, 0.55)", width: 0.6, }); }, }, { base: true, draw: (context, map) => { const contour = on.contour && data.contour; if (!contour) return; const color = themeColor("--draw-map-contour", "#ffd68c"); const step = pickLevelStep(contour, map.view, 5); context.globalAlpha = 0.7; stroke(context, map, contour, color, 0.8, step); stroke(context, map, contour, color, 1.6, 25); // 계곡선 context.globalAlpha = 1; context.font = "10px system-ui, sans-serif"; context.textAlign = "center"; context.textBaseline = "middle"; drawPreparedLabels( context, contour, map.view, color, 25, map.uprightRad, "rgba(0, 0, 0, 0.6)", labelWidths, ); }, }, { base: true, draw: (context, map) => { if (!on.road || !data.road) return; stroke(context, map, data.road.all, "rgba(0, 0, 0, 0.55)", 3.2); stroke(context, map, data.road.all, themeColor("--draw-map-road", "#f8fafc"), 1.6); }, }, { base: true, draw: (context, map) => { if (!on.forestRoad || !data.road) return; context.setLineDash([7, 4]); stroke(context, map, data.road.small, "rgba(0, 0, 0, 0.55)", 3.6); stroke(context, map, data.road.small, themeColor("--draw-map-forest-road", "#f59e0b"), 2); context.setLineDash([]); }, }, { base: true, draw: (context, map) => { const got = on.forestRoadFile && data.forestRoadFile; if (!got) return; stroke(context, map, got.layer, "rgba(0, 0, 0, 0.55)", 4); stroke(context, map, got.layer, themeColor("--draw-map-forest-road-file", "#ea580c"), 2.4); }, }, { base: true, draw: (context, map) => { if (!on.stream || !data.stream) return; const color = themeColor("--draw-map-stream", "#38bdf8"); stroke(context, map, data.stream[0], color, 1); stroke(context, map, data.stream[1], color, 2.4); }, }, ...LINE_KEYS.map((key): PlanMapLayer => ({ base: true, draw: (context, map) => { const got = on[key] && data[key]; if (!got) return; const { color, width, dash } = LINE_LAYERS[key]; context.setLineDash(dash); stroke(context, map, got.layer, "rgba(0, 0, 0, 0.55)", width + 1.6); stroke(context, map, got.layer, themeColor(color, "#f43f5e"), width); context.setLineDash([]); }, })), { base: true, draw: (context, map) => { const got = on.landslideZones && data.landslideZones; const meta = opts.meta(); if (!got || !meta) return; const color = themeColor("--draw-map-landslide-zone", "#e11d48"); // 필지 = 「이 필지 안 어딘가」 테두리만(지정 넓이 ≪ 필지 · 칠하지 않음) context.setLineDash([6, 3]); stroke(context, map, got.parcels, "rgba(255, 255, 255, 0.7)", 3.2); stroke(context, map, got.parcels, color, 1.8); context.setLineDash([]); const normalizer = createNormalizer(meta); context.fillStyle = color; context.strokeStyle = "#ffffff"; context.lineWidth = 1.5; for (const point of got.info.points) { const [x, y] = lonLatToScreen(normalizer, map.view, point.lon, point.lat); if (x < -10 || y < -10 || x > map.view.width + 10 || y > map.view.height + 10) continue; context.beginPath(); context.arc(x, y, 5, 0, Math.PI * 2); context.fill(); context.stroke(); } }, }, { base: true, draw: (context, map) => { const meta = opts.meta(); if (!on.place || !data.place || !meta) return; const normalizer = createNormalizer(meta); context.font = "600 11px system-ui, sans-serif"; context.textAlign = "center"; context.textBaseline = "bottom"; context.lineWidth = 3; context.strokeStyle = "rgba(0, 0, 0, 0.7)"; context.fillStyle = themeColor("--draw-map-place", "#f8fafc"); for (const place of data.place) { const [x, y] = lonLatToScreen(normalizer, map.view, place.lon, place.lat); if (x < -60 || y < -20 || x > map.view.width + 60 || y > map.view.height + 20) continue; map.upright(context, x, y, () => { context.beginPath(); context.arc(x, y, 2.5, 0, Math.PI * 2); context.fill(); context.strokeText(place.name, x, y - 4); context.fillText(place.name, x, y - 4); }); } }, }, ]; }