/* ============================================================================= * D03_PlanAnalysis_UI_Measure.ts * D03 거리재기 겹(PLAN 562-3) — 두 점을 누르면 그 사이 길이(m) · 원지반 기울기(%) · 높이 차(m). * 셋째 누름 = 새로 잼 · Esc = 지움. 지반고 = 받은 지형 `heightAt` · 셈 = 562-1 짝(`logic/D03_PlanAnalysis_Draw`). * 600-2 — 첫 누름이 노선 위면 「노선 따라」: 둘째 점 = 이어진 노선 위 가장 가까운 자리 · 길이 = 노선 따라 * (`logic/D03_PlanAnalysis_Measure`) · 첫 누름이 선 밖이면 곧은 거리 그대로. * ========================================================================== */ import { groundSlopePct, segmentLength } from "./logic/D03_PlanAnalysis_Draw"; import { nearestOnRoute, routeSpan, type RoutePos, type RouteSpan, } from "./logic/D03_PlanAnalysis_Measure"; import type { PlanNetwork, Vertex } from "./logic/D03_PlanAnalysis_Plan"; import { planColors, type PlanMapLayer, type ScreenMap } from "./D03_PlanAnalysis_UI_Map"; import { lineAt, strokeCased } from "./D03_PlanAnalysis_UI_DrawPaint"; export interface PlanMeasure { layer: PlanMapLayer; /** 지움(Esc · 끔). */ clear: () => void; /** 검증 창구 — 잰 값(두 점이 다 있으면) · `alongRoute` = 노선 따라 쟀나. */ result: () => { lengthM: number; slopePct: number | null; riseM: number | null; alongRoute: boolean; } | null; } const fmt = (value: number, digits = 1): string => value.toFixed(digits); const signed = (value: number, digits = 1): string => `${value >= 0 ? "+" : ""}${fmt(value, digits)}`; export function createPlanMeasure( heightAt: () => (x: number, y: number) => number, network: () => PlanNetwork, ): PlanMeasure { let from: Vertex | null = null; let to: Vertex | null = null; let fixed = false; /** 첫 누름이 노선 위였던 자리 · 둘째 점이 노선 위에 놓인 길. */ let fromPos: RoutePos | null = null; let span: RouteSpan | null = null; const measure = (a: Vertex, b: Vertex) => { const z = heightAt(); const rise = z(b[0], b[1]) - z(a[0], a[1]); const lengthM = span ? span.lengthM : segmentLength(a, b); return { lengthM, slopePct: span ? Number.isFinite(rise) && lengthM > 0 ? (rise / lengthM) * 100 : null : groundSlopePct(a, b, z), riseM: Number.isFinite(rise) ? rise : null, alongRoute: !!span, }; }; /** 둘째 점 놓기 — 노선 위 첫 점이면 이어진 노선 위 가까운 자리 · 아니면 누른 자리. */ const place = (point: Vertex): void => { const near = fromPos ? nearestOnRoute(network(), point, fromPos) : null; span = near && fromPos ? routeSpan(network(), fromPos, near) : null; to = near && span ? near.point : point; }; const layer: PlanMapLayer = { draw(context, map: ScreenMap) { if (!from) return; const colors = planColors(); const [ax, ay] = map.toScreen(from); const dot = (x: number, y: number): void => { context.beginPath(); context.arc(x, y, 4.5, 0, Math.PI * 2); context.fillStyle = colors.measure; context.fill(); context.lineWidth = 1.5; context.strokeStyle = colors.casing; context.stroke(); }; context.lineCap = "round"; context.lineJoin = "round"; if (to) { const [bx, by] = map.toScreen(to); context.beginPath(); if (span) { span.path.forEach((point, i) => { const [sx, sy] = map.toScreen(point); if (i) context.lineTo(sx, sy); else context.moveTo(sx, sy); }); strokeCased(context, colors.measure, 4); } else { context.moveTo(ax, ay); context.lineTo(bx, by); strokeCased(context, colors.measure, 2.5, [6, 4]); } dot(ax, ay); dot(bx, by); const { lengthM, slopePct, riseM } = measure(from, to); const rise = riseM === null ? "지형 밖" : `Δh ${signed(riseM)} m`; const text = span ? `노선 따라 ${fmt(lengthM)} m · ` + (span.stations ? `측점 ${fmt(span.stations[0])} ~ ${fmt(span.stations[1])} m · ` : "") + rise : `${fmt(lengthM)} m · ` + (slopePct === null ? "지형 밖" : `${signed(slopePct)} % · ${rise}`); const [mx, my] = [(ax + bx) / 2, (ay + by) / 2 - 14]; map.upright(context, mx, my, () => { context.font = "12px system-ui, sans-serif"; context.textAlign = "center"; context.textBaseline = "middle"; context.lineWidth = 3; context.strokeStyle = colors.halo; context.strokeText(text, mx, my); context.fillStyle = "#ffffff"; context.fillText(text, mx, my); }); } else { dot(ax, ay); } }, press(px, py, map, button) { if (button !== 0) return false; const point = map.toMetric(px, py); if (!from || fixed) { // 첫 누름 — 노선 위(선 8 px · 꺾임점 12 px)면 노선 따라 · 아니면 곧은 거리 const net = network(); const hit = lineAt(net, px, py, map); fromPos = hit ? { line: net.lines[hit.line].id, atM: hit.atM } : null; [from, to, fixed, span] = [hit ? hit.point : point, null, false, null]; } else { place(point); fixed = true; } return true; }, hover(px, py, map) { if (from && !fixed) place(map.toMetric(px, py)); }, }; return { layer, clear: () => { [from, to, fixed, fromPos, span] = [null, null, false, null, null]; }, result: () => (from && to && fixed ? measure(from, to) : null), }; }