diff --git a/A00_Common/design/design_profile.ts b/A00_Common/design/design_profile.ts index 8b2b0f640..ca82056cc 100644 --- a/A00_Common/design/design_profile.ts +++ b/A00_Common/design/design_profile.ts @@ -86,6 +86,7 @@ import type { RouteProfilePanelCallbacks } from "../../B05_Profile/B05_Profile_U import { attachWheelHorizontalScroll } from "../../B05_Profile/B05_Profile_UI_Profile_Wheel"; import { needsFullRedraw, type ElevationWindowResult } from "@util/common_util_chart_ywindow"; import type { DesignSideBoard, DesignSideFactory, DesignView } from "./design_profile_slots"; +import { noticeDroppedEdits } from "./design_save"; import "../../B06_Section/B06_Section_UI_Style.css"; // SVG 차트 색상(.b06-chart__*)의 정의처는 _Style_Cross.css다. 이걸 빼면 B05로 바로 진입했을 때 // 배경 rect가 브라우저 기본 fill(검정)로 그려진다 — B06을 먼저 방문해야 정상으로 보이던 원인. @@ -650,6 +651,7 @@ export function createDesignProfile(ctx: DesignProfileContext) { async save(): Promise { if (!routeId || !store.dirty()) return; const saved = await saveProfileAlignment(projectId, routeId, store.edits()); + noticeDroppedEdits(saved); const next = saved.profile_alignment as ProfileAlignment | undefined; if (next?.base_pvi?.length) { base = toAlignmentBase(next); diff --git a/A00_Common/design/design_save.ts b/A00_Common/design/design_save.ts index 8c8bad1fa..827ffbfe9 100644 --- a/A00_Common/design/design_save.ts +++ b/A00_Common/design/design_save.ts @@ -65,11 +65,26 @@ function reportOnly(label: string): (error: unknown) => void { }; } +/** 서버가 버린 계획고 편집 자리(측점 · 관 · 구조물이 없는 자리 · 213-2)를 알린다. */ +export function noticeDroppedEdits(saved: { dropped_edit_chainages?: number[] }): void { + const dropped = saved.dropped_edit_chainages ?? []; + if (!dropped.length) return; + const where = dropped + .slice(0, 5) + .map((value) => `${value.toFixed(2)}m`) + .join(" · "); + showToast( + `계획고 편집 ${dropped.length}곳을 지웠습니다 — 측점 · 관 · 구조물이 없는 자리: ${where}` + + (dropped.length > 5 ? ` 외 ${dropped.length - 5}곳` : ""), + "info", + ); +} + /** 계획고 초안(가게 하나)을 종단 정본에 쓴다. 없으면 아무 일도 하지 않는다. */ async function flushAlignmentDraft(projectId: string, routeId: number): Promise { const draft = pendingAlignment(routeId); if (!draft) return; - await saveProfileAlignment(projectId, routeId, draft); + noticeDroppedEdits(await saveProfileAlignment(projectId, routeId, draft)); markAlignmentSaved(routeId); } diff --git a/B05_Profile/B05_Profile_Api_Fetch.ts b/B05_Profile/B05_Profile_Api_Fetch.ts index bf89201a6..05e7e1310 100644 --- a/B05_Profile/B05_Profile_Api_Fetch.ts +++ b/B05_Profile/B05_Profile_Api_Fetch.ts @@ -219,6 +219,8 @@ export interface ProfileAlignmentSaveResponse { route_id: number; profile_alignment: unknown; grade_summary: RouteGradeSummary | null; + /** 측점 · 관 · 구조물 자리가 없어 서버가 버린 계획고 편집 자리(m · 213-2). */ + dropped_edit_chainages?: number[]; } /** diff --git a/B05_Profile/B05_Profile_Engine_Grade_Alignment.py b/B05_Profile/B05_Profile_Engine_Grade_Alignment.py index 82e8e274f..d5e0f1223 100644 --- a/B05_Profile/B05_Profile_Engine_Grade_Alignment.py +++ b/B05_Profile/B05_Profile_Engine_Grade_Alignment.py @@ -214,6 +214,30 @@ def snap_to_stations( ) +def drop_unplaced_edits( + edits: dict[str, dict[str, float]], places: np.ndarray +) -> tuple[dict[str, dict[str, float]], list[float]]: + """측점 · 관 · 구조물 자리(`places`) 0.006m 안에 없는 편집 키를 버린다(213-2). + + 옮긴 관의 옛 자리 · 지운 시설 자리에 남은 키는 측점선 없는 꼭짓점(유령)이 된다 — 세션 초안이 + 옛 키를 다시 보내도 [저장]에서 걸러진다. (남은 편집, 버린 자리 누가거리). + """ + kept: dict[str, dict[str, float]] = {} + dropped: set[float] = set() + for name, values in edits.items(): + kept[name] = {} + for key, value in values.items(): + try: + chainage = float(key) + except (TypeError, ValueError): + continue + if places.size and float(np.min(np.abs(places - chainage))) < SAME_STATION_SNAP_M: + kept[name][key] = value + else: + dropped.add(round(chainage, _KEY_DECIMALS)) + return kept, sorted(dropped) + + def resolve_pvi( base_s: np.ndarray, base_z: np.ndarray, diff --git a/B05_Profile/B05_Profile_Router.py b/B05_Profile/B05_Profile_Router.py index 3f0a47c96..1f0576dca 100644 --- a/B05_Profile/B05_Profile_Router.py +++ b/B05_Profile/B05_Profile_Router.py @@ -8,6 +8,7 @@ from typing import Any from uuid import UUID import aiomysql +import numpy as np from fastapi import APIRouter from fastapi.responses import JSONResponse @@ -15,6 +16,7 @@ from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_ from B05_Profile.B05_Profile_Debug import log_b05_debug from B05_Profile.B05_Profile_Engine import run_route_design from B05_Profile.B05_Profile_Engine_Grade import GradeDesignOptions, resolve_grade_options +from B05_Profile.B05_Profile_Engine_Grade_Alignment import drop_unplaced_edits from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile from B05_Profile.B05_Profile_Engine_Sections import run_section_generation from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions @@ -384,13 +386,30 @@ async def update_contour_interval( ) +def _structure_marks(project_root: Path, route_data_path: str) -> list[float]: + """관 · 구조물 자리(m) — 관 지점 전부 + 구조물 측점 자리(시작 · 기준 · 종료).""" + from B05_Profile.B05_Profile_Engine_Sections import ( + _load_pipe_points, + _load_route_polyline, + resolve_extra_stations, + ) + + pipes = _load_pipe_points(project_root, _load_route_polyline(project_root, route_data_path)) + extras = resolve_extra_stations(project_root, pipes) + return [float(pipe.chainage_m) for pipe in pipes] + [float(mark) for mark, _ in extras] + + def _apply_alignment_edits( - project_root: Path, longitudinal_file_path: str, edits: dict[str, Any] -) -> tuple[dict[str, Any], dict[str, Any]]: - """저장된 종단 JSON에 계획선 편집을 반영해 정본을 다시 쓰고 (선형, 요약)을 반환한다. + project_root: Path, + longitudinal_file_path: str, + edits: dict[str, Any], + marks: list[float] | None = None, +) -> tuple[dict[str, Any], dict[str, Any], list[float]]: + """저장된 종단 JSON에 계획선 편집을 반영해 정본을 다시 쓰고 (선형, 요약, 버린 자리)를 반환한다. 화면이 즉시 계산해 보여준 값과 같은 기하식을 서버에서도 다시 적용해, 파일에 - 남는 정본이 항상 한 곳(백엔드)에서 만들어지게 한다. + 남는 정본이 항상 한 곳(백엔드)에서 만들어지게 한다. `marks`(관 · 구조물 자리)를 주면 + 측점 · 관 · 구조물 어디에도 없는 편집 키를 버린다(213-2 — 저장 차례가 관 · 구조물 뒤라 최신). """ root = project_root.resolve() path = (root / longitudinal_file_path).resolve() @@ -399,6 +418,16 @@ def _apply_alignment_edits( if not path.is_file(): raise FileNotFoundError("종단 데이터 파일을 찾을 수 없습니다.") longitudinal = json.loads(path.read_text(encoding="utf-8")) + dropped: list[float] = [] + if marks is not None: + stations = [float(s.get("chainage_m", 0.0)) for s in longitudinal.get("stations") or []] + edits, dropped = drop_unplaced_edits( + { + "station_offsets": dict(edits.get("station_offsets") or {}), + "curve_radii": dict(edits.get("curve_radii") or {}), + }, + np.array(stations + marks, dtype=np.float64), + ) alignment, profile = rebuild_alignment_profile(longitudinal, edits) longitudinal["profile_alignment"] = alignment profiles = longitudinal.get("design_profiles") @@ -407,7 +436,7 @@ def _apply_alignment_edits( else: longitudinal["design_profiles"] = [profile] atomic_write_json(path, longitudinal) - return alignment, {"id": profile["id"], **profile["summary"]} + return alignment, {"id": profile["id"], **profile["summary"]}, dropped @router.put("/{project_id}/route/profile-alignment", response_model=ProfileAlignmentSaveResponse) @@ -426,11 +455,18 @@ async def save_profile_alignment( ) stored_path = await get_project_storage_relative_path(connection, project_id) project_root = Path(resolve_stored_project_path(stored_path)) - alignment, grade_summary = await asyncio.to_thread( + route = await get_latest_route(connection, project_id) + marks = None + if route and route["id"] == request.route_id and route.get("route_data_path"): + marks = await asyncio.to_thread( + _structure_marks, project_root, str(route["route_data_path"]) + ) + alignment, grade_summary, dropped = await asyncio.to_thread( _apply_alignment_edits, project_root, str(longitudinal["longitudinal_file_path"]), request.edits(), + marks, ) await connection.begin() try: @@ -446,6 +482,7 @@ async def save_profile_alignment( route_id=request.route_id, profile_alignment=alignment, grade_summary=grade_summary, + dropped_edit_chainages=dropped, ) except FileNotFoundError as exc: return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)}) diff --git a/B05_Profile/B05_Profile_Router_RouteInitial.py b/B05_Profile/B05_Profile_Router_RouteInitial.py index 9540d6bfc..88e90e6b1 100644 --- a/B05_Profile/B05_Profile_Router_RouteInitial.py +++ b/B05_Profile/B05_Profile_Router_RouteInitial.py @@ -173,7 +173,9 @@ async def apply_profile_target( root = Path(resolve_stored_project_path(stored_path)) relative = str(row["longitudinal_file_path"]) count = await asyncio.to_thread(_set_base_pvi, root / relative, *target) - _alignment, summary = await asyncio.to_thread(_apply_alignment_edits, root, relative, {}) + _alignment, summary, _dropped = await asyncio.to_thread( + _apply_alignment_edits, root, relative, {} + ) async with pool.acquire() as connection: await connection.begin() await update_longitudinal_grade_summary( diff --git a/B05_Profile/B05_Profile_Schema.py b/B05_Profile/B05_Profile_Schema.py index e6407c988..804107d7f 100644 --- a/B05_Profile/B05_Profile_Schema.py +++ b/B05_Profile/B05_Profile_Schema.py @@ -225,6 +225,8 @@ class ProfileAlignmentSaveResponse(BaseModel): route_id: int profile_alignment: dict[str, Any] grade_summary: dict[str, Any] | None = None + #: 측점 · 관 · 구조물 자리가 없어 버린 계획고 편집 자리(m) — 화면이 알림으로 보임(213-2) + dropped_edit_chainages: list[float] = Field(default_factory=list) class RouteSolveResponse(BaseModel): diff --git a/B05_Profile/B05_Profile_UI_Page_Structures.ts b/B05_Profile/B05_Profile_UI_Page_Structures.ts index 296298433..8518459e2 100644 --- a/B05_Profile/B05_Profile_UI_Page_Structures.ts +++ b/B05_Profile/B05_Profile_UI_Page_Structures.ts @@ -32,6 +32,7 @@ import { import type { createRoutePanel } from "./B05_Profile_UI_Panel"; import type { createRouteProfilePanel } from "./B05_Profile_UI_Profile_Panel"; import { FACILITY_NAMES } from "./B05_Profile_UI_Page_Helpers"; +import { followedStation } from "./B05_Profile_UI_Profile_Alignment"; /** 관 지점 한 건 — 유효직경·시설 종류·부속 옵션까지 그대로 받는다. */ export interface PipeProjection { @@ -103,20 +104,19 @@ export function createStructuresBridge(deps: StructuresBridgeDeps) { /** 비정규 측점 목록 변경 → 3D·그래프·테이블에 반영(프론트 프리뷰, 백엔드 미전송). */ function applyIrregularStations(stations: IrregularStation[]): void { - // 위치가 바뀌거나 삭제된 비정규 측점의 옛 chainage에 남은 계획고 편집(유령 변화점)을 지운다. - // 서버가 관 측점을 0.01m 로 반올림해 돌려주므로 0.005m 안의 새 측점은 같은 측점 — 편집을 옮김. + // 위치가 바뀌거나 삭제된 비정규 측점의 옛 chainage에 남은 계획고 편집(유령 변화점)을 옮기거나 지운다. + // 옮김 = 0.006m 안 새 측점(서버 0.01m 반올림) · 같은 id · 사라진 것 하나와 새것 하나(옮긴 관 · 213-2) + // — 세션 초안도 새 키로 바뀌어 옛 키가 다시 안 나감. 나머지는 지움. const nextKeys = new Set(stations.map((station) => station.chainage_m.toFixed(3))); const oldKeys = new Set(irregularStations.map((station) => station.chainage_m.toFixed(3))); - irregularStations - .filter((station) => !nextKeys.has(station.chainage_m.toFixed(3))) - .forEach((station) => { - const same = stations.find( - (next) => - Math.abs(next.chainage_m - station.chainage_m) < 0.006 && - !oldKeys.has(next.chainage_m.toFixed(3)), - ); - deps.profilePanel().resetStationEdit(station.chainage_m, same?.chainage_m); - }); + const gone = irregularStations.filter( + (station) => !nextKeys.has(station.chainage_m.toFixed(3)), + ); + const born = stations.filter((station) => !oldKeys.has(station.chainage_m.toFixed(3))); + gone.forEach((station) => { + const same = followedStation(station, gone, born); + deps.profilePanel().resetStationEdit(station.chainage_m, same?.chainage_m); + }); irregularStations = stations; deps.renderStationLines(); deps.profilePanel().setIrregularStations(withoutFordPavement(stations)); diff --git a/B05_Profile/B05_Profile_UI_Profile_Alignment.ts b/B05_Profile/B05_Profile_UI_Profile_Alignment.ts index c30957a82..1346067ba 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Alignment.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Alignment.ts @@ -182,6 +182,22 @@ export function toAlignmentBase(alignment: ProfileAlignment): AlignmentBase { }; } +/** + * 사라진 비정규 측점의 편집이 따라갈 새 측점(213-2) — 0.006m 안(반올림) · 같은 id · 사라진 것 하나와 + * 새것 하나(옮긴 관)면 그것. 없으면 null(편집을 지움). + */ +export function followedStation( + station: T, + gone: ReadonlyArray, + born: ReadonlyArray, +): T | null { + return ( + born.find((next) => Math.abs(next.chainage_m - station.chainage_m) < 0.006) ?? + born.find((next) => next.id === station.id) ?? + (gone.length === 1 && born.length === 1 ? born[0] : null) + ); +} + /** 반올림만 다른 같은 측점(m) — 서버 `SAME_STATION_SNAP_M`(B05_Profile_Engine_Grade_Alignment.py) 와 짝. */ const SAME_STATION_SNAP_M = 0.006; diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index d3dbdd338..e3c76208c 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -476,7 +476,11 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc const hit = pipeFacilities.find( (entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M, ); - if (hit) hit.chainage_m = to; + if (hit) { + // 그 관 측점의 계획고 편집도 새 자리로 — 옛 키가 유령 꼭짓점으로 남지 않게(213-2) + deps.shared.profile?.resetStationEdit(hit.chainage_m, to); + hit.chainage_m = to; + } if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M) currentChainageM = to; pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m); diff --git a/resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json b/resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json new file mode 100644 index 000000000..35b41b64f --- /dev/null +++ b/resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json @@ -0,0 +1,2592 @@ +{ + "_note": "192-1 · 213 시험 고정 자료 — 검증_랩탑메인 226fc805 종단 460~840m 사본, 측점 769.38 → 769.28(192 이전 상태: 관 769.28 → 769.38 옮김 뒤 측점은 옛 자리)", + "length_m": 1153.190099, + "samples": [ + { + "chainage_m": 460.0, + "elevation_m": 854.399565, + "z": 854.399565, + "valid": true + }, + { + "chainage_m": 461.0, + "elevation_m": 854.323496, + "z": 854.323496, + "valid": true + }, + { + "chainage_m": 462.0, + "elevation_m": 854.357254, + "z": 854.357254, + "valid": true + }, + { + "chainage_m": 463.0, + "elevation_m": 854.688311, + "z": 854.688311, + "valid": true + }, + { + "chainage_m": 464.0, + "elevation_m": 854.65229, + "z": 854.65229, + "valid": true + }, + { + "chainage_m": 465.0, + "elevation_m": 854.144385, + "z": 854.144385, + "valid": true + }, + { + "chainage_m": 466.0, + "elevation_m": 854.23999, + "z": 854.23999, + "valid": true + }, + { + "chainage_m": 467.0, + "elevation_m": 854.150669, + "z": 854.150669, + "valid": true + }, + { + "chainage_m": 468.0, + "elevation_m": 854.010948, + "z": 854.010948, + "valid": true + }, + { + "chainage_m": 469.0, + "elevation_m": 854.011239, + "z": 854.011239, + "valid": true + }, + { + "chainage_m": 470.0, + "elevation_m": 853.905926, + "z": 853.905926, + "valid": true + }, + { + "chainage_m": 471.0, + "elevation_m": 854.180391, + "z": 854.180391, + "valid": true + }, + { + "chainage_m": 472.0, + "elevation_m": 854.284507, + "z": 854.284507, + "valid": true + }, + { + "chainage_m": 473.0, + "elevation_m": 854.276427, + "z": 854.276427, + "valid": true + }, + { + "chainage_m": 474.0, + "elevation_m": 854.545725, + "z": 854.545725, + "valid": true + }, + { + "chainage_m": 475.0, + "elevation_m": 855.185317, + "z": 855.185317, + "valid": true + }, + { + "chainage_m": 476.0, + "elevation_m": 855.976081, + "z": 855.976081, + "valid": true + }, + { + "chainage_m": 477.0, + "elevation_m": 855.928803, + "z": 855.928803, + "valid": true + }, + { + "chainage_m": 478.0, + "elevation_m": 855.427162, + "z": 855.427162, + "valid": true + }, + { + "chainage_m": 479.0, + "elevation_m": 855.599991, + "z": 855.599991, + "valid": true + }, + { + "chainage_m": 480.0, + "elevation_m": 856.590036, + "z": 856.590036, + "valid": true + }, + { + "chainage_m": 481.0, + "elevation_m": 857.426487, + "z": 857.426487, + "valid": true + }, + { + "chainage_m": 482.0, + "elevation_m": 857.39277, + "z": 857.39277, + "valid": true + }, + { + "chainage_m": 483.0, + "elevation_m": 857.479369, + "z": 857.479369, + "valid": true + }, + { + "chainage_m": 484.0, + "elevation_m": 855.607648, + "z": 855.607648, + "valid": true + }, + { + "chainage_m": 485.0, + "elevation_m": 854.222329, + "z": 854.222329, + "valid": true + }, + { + "chainage_m": 486.0, + "elevation_m": 853.720087, + "z": 853.720087, + "valid": true + }, + { + "chainage_m": 487.0, + "elevation_m": 853.627091, + "z": 853.627091, + "valid": true + }, + { + "chainage_m": 488.0, + "elevation_m": 853.70957, + "z": 853.70957, + "valid": true + }, + { + "chainage_m": 489.0, + "elevation_m": 853.516046, + "z": 853.516046, + "valid": true + }, + { + "chainage_m": 490.0, + "elevation_m": 853.192759, + "z": 853.192759, + "valid": true + }, + { + "chainage_m": 491.0, + "elevation_m": 853.280064, + "z": 853.280064, + "valid": true + }, + { + "chainage_m": 492.0, + "elevation_m": 853.367633, + "z": 853.367633, + "valid": true + }, + { + "chainage_m": 493.0, + "elevation_m": 853.434346, + "z": 853.434346, + "valid": true + }, + { + "chainage_m": 494.0, + "elevation_m": 853.337047, + "z": 853.337047, + "valid": true + }, + { + "chainage_m": 495.0, + "elevation_m": 852.855864, + "z": 852.855864, + "valid": true + }, + { + "chainage_m": 496.0, + "elevation_m": 853.017523, + "z": 853.017523, + "valid": true + }, + { + "chainage_m": 497.0, + "elevation_m": 853.224828, + "z": 853.224828, + "valid": true + }, + { + "chainage_m": 498.0, + "elevation_m": 853.151205, + "z": 853.151205, + "valid": true + }, + { + "chainage_m": 499.0, + "elevation_m": 852.744451, + "z": 852.744451, + "valid": true + }, + { + "chainage_m": 500.0, + "elevation_m": 852.29905, + "z": 852.29905, + "valid": true + }, + { + "chainage_m": 501.0, + "elevation_m": 852.33224, + "z": 852.33224, + "valid": true + }, + { + "chainage_m": 502.0, + "elevation_m": 852.356165, + "z": 852.356165, + "valid": true + }, + { + "chainage_m": 503.0, + "elevation_m": 852.283422, + "z": 852.283422, + "valid": true + }, + { + "chainage_m": 504.0, + "elevation_m": 852.09869, + "z": 852.09869, + "valid": true + }, + { + "chainage_m": 505.0, + "elevation_m": 851.991606, + "z": 851.991606, + "valid": true + }, + { + "chainage_m": 506.0, + "elevation_m": 851.942009, + "z": 851.942009, + "valid": true + }, + { + "chainage_m": 507.0, + "elevation_m": 851.261328, + "z": 851.261328, + "valid": true + }, + { + "chainage_m": 508.0, + "elevation_m": 851.424119, + "z": 851.424119, + "valid": true + }, + { + "chainage_m": 509.0, + "elevation_m": 851.353731, + "z": 851.353731, + "valid": true + }, + { + "chainage_m": 510.0, + "elevation_m": 851.190865, + "z": 851.190865, + "valid": true + }, + { + "chainage_m": 511.0, + "elevation_m": 851.012716, + "z": 851.012716, + "valid": true + }, + { + "chainage_m": 512.0, + "elevation_m": 850.742907, + "z": 850.742907, + "valid": true + }, + { + "chainage_m": 513.0, + "elevation_m": 850.597659, + "z": 850.597659, + "valid": true + }, + { + "chainage_m": 514.0, + "elevation_m": 850.698391, + "z": 850.698391, + "valid": true + }, + { + "chainage_m": 515.0, + "elevation_m": 850.745868, + "z": 850.745868, + "valid": true + }, + { + "chainage_m": 516.0, + "elevation_m": 850.773431, + "z": 850.773431, + "valid": true + }, + { + "chainage_m": 517.0, + "elevation_m": 850.970436, + "z": 850.970436, + "valid": true + }, + { + "chainage_m": 518.0, + "elevation_m": 850.928337, + "z": 850.928337, + "valid": true + }, + { + "chainage_m": 519.0, + "elevation_m": 850.859021, + "z": 850.859021, + "valid": true + }, + { + "chainage_m": 520.0, + "elevation_m": 851.187557, + "z": 851.187557, + "valid": true + }, + { + "chainage_m": 521.0, + "elevation_m": 851.375589, + "z": 851.375589, + "valid": true + }, + { + "chainage_m": 522.0, + "elevation_m": 851.534681, + "z": 851.534681, + "valid": true + }, + { + "chainage_m": 523.0, + "elevation_m": 851.68768, + "z": 851.68768, + "valid": true + }, + { + "chainage_m": 524.0, + "elevation_m": 851.930171, + "z": 851.930171, + "valid": true + }, + { + "chainage_m": 525.0, + "elevation_m": 851.97661, + "z": 851.97661, + "valid": true + }, + { + "chainage_m": 526.0, + "elevation_m": 852.094679, + "z": 852.094679, + "valid": true + }, + { + "chainage_m": 527.0, + "elevation_m": 852.211098, + "z": 852.211098, + "valid": true + }, + { + "chainage_m": 528.0, + "elevation_m": 852.230928, + "z": 852.230928, + "valid": true + }, + { + "chainage_m": 529.0, + "elevation_m": 852.394254, + "z": 852.394254, + "valid": true + }, + { + "chainage_m": 530.0, + "elevation_m": 852.636398, + "z": 852.636398, + "valid": true + }, + { + "chainage_m": 531.0, + "elevation_m": 852.892311, + "z": 852.892311, + "valid": true + }, + { + "chainage_m": 532.0, + "elevation_m": 853.103535, + "z": 853.103535, + "valid": true + }, + { + "chainage_m": 533.0, + "elevation_m": 853.244656, + "z": 853.244656, + "valid": true + }, + { + "chainage_m": 534.0, + "elevation_m": 853.379518, + "z": 853.379518, + "valid": true + }, + { + "chainage_m": 535.0, + "elevation_m": 853.419451, + "z": 853.419451, + "valid": true + }, + { + "chainage_m": 536.0, + "elevation_m": 853.478335, + "z": 853.478335, + "valid": true + }, + { + "chainage_m": 537.0, + "elevation_m": 853.812764, + "z": 853.812764, + "valid": true + }, + { + "chainage_m": 538.0, + "elevation_m": 854.099244, + "z": 854.099244, + "valid": true + }, + { + "chainage_m": 539.0, + "elevation_m": 854.189474, + "z": 854.189474, + "valid": true + }, + { + "chainage_m": 540.0, + "elevation_m": 854.273041, + "z": 854.273041, + "valid": true + }, + { + "chainage_m": 541.0, + "elevation_m": 854.25719, + "z": 854.25719, + "valid": true + }, + { + "chainage_m": 542.0, + "elevation_m": 854.3265, + "z": 854.3265, + "valid": true + }, + { + "chainage_m": 543.0, + "elevation_m": 854.394984, + "z": 854.394984, + "valid": true + }, + { + "chainage_m": 544.0, + "elevation_m": 854.628286, + "z": 854.628286, + "valid": true + }, + { + "chainage_m": 545.0, + "elevation_m": 854.699286, + "z": 854.699286, + "valid": true + }, + { + "chainage_m": 546.0, + "elevation_m": 854.36337, + "z": 854.36337, + "valid": true + }, + { + "chainage_m": 547.0, + "elevation_m": 854.071547, + "z": 854.071547, + "valid": true + }, + { + "chainage_m": 548.0, + "elevation_m": 853.961326, + "z": 853.961326, + "valid": true + }, + { + "chainage_m": 549.0, + "elevation_m": 854.118069, + "z": 854.118069, + "valid": true + }, + { + "chainage_m": 550.0, + "elevation_m": 854.1746, + "z": 854.1746, + "valid": true + }, + { + "chainage_m": 551.0, + "elevation_m": 854.560974, + "z": 854.560974, + "valid": true + }, + { + "chainage_m": 552.0, + "elevation_m": 854.536009, + "z": 854.536009, + "valid": true + }, + { + "chainage_m": 553.0, + "elevation_m": 854.337467, + "z": 854.337467, + "valid": true + }, + { + "chainage_m": 554.0, + "elevation_m": 854.176247, + "z": 854.176247, + "valid": true + }, + { + "chainage_m": 555.0, + "elevation_m": 854.664027, + "z": 854.664027, + "valid": true + }, + { + "chainage_m": 556.0, + "elevation_m": 854.846734, + "z": 854.846734, + "valid": true + }, + { + "chainage_m": 557.0, + "elevation_m": 854.978906, + "z": 854.978906, + "valid": true + }, + { + "chainage_m": 558.0, + "elevation_m": 855.226901, + "z": 855.226901, + "valid": true + }, + { + "chainage_m": 559.0, + "elevation_m": 855.353289, + "z": 855.353289, + "valid": true + }, + { + "chainage_m": 560.0, + "elevation_m": 855.526111, + "z": 855.526111, + "valid": true + }, + { + "chainage_m": 561.0, + "elevation_m": 855.833343, + "z": 855.833343, + "valid": true + }, + { + "chainage_m": 562.0, + "elevation_m": 856.063139, + "z": 856.063139, + "valid": true + }, + { + "chainage_m": 563.0, + "elevation_m": 856.044308, + "z": 856.044308, + "valid": true + }, + { + "chainage_m": 564.0, + "elevation_m": 856.245881, + "z": 856.245881, + "valid": true + }, + { + "chainage_m": 565.0, + "elevation_m": 856.400703, + "z": 856.400703, + "valid": true + }, + { + "chainage_m": 566.0, + "elevation_m": 856.475154, + "z": 856.475154, + "valid": true + }, + { + "chainage_m": 567.0, + "elevation_m": 856.863479, + "z": 856.863479, + "valid": true + }, + { + "chainage_m": 568.0, + "elevation_m": 857.152322, + "z": 857.152322, + "valid": true + }, + { + "chainage_m": 569.0, + "elevation_m": 857.331392, + "z": 857.331392, + "valid": true + }, + { + "chainage_m": 570.0, + "elevation_m": 857.459269, + "z": 857.459269, + "valid": true + }, + { + "chainage_m": 571.0, + "elevation_m": 857.496881, + "z": 857.496881, + "valid": true + }, + { + "chainage_m": 572.0, + "elevation_m": 857.429968, + "z": 857.429968, + "valid": true + }, + { + "chainage_m": 573.0, + "elevation_m": 857.332459, + "z": 857.332459, + "valid": true + }, + { + "chainage_m": 574.0, + "elevation_m": 857.423876, + "z": 857.423876, + "valid": true + }, + { + "chainage_m": 575.0, + "elevation_m": 857.178176, + "z": 857.178176, + "valid": true + }, + { + "chainage_m": 576.0, + "elevation_m": 857.119821, + "z": 857.119821, + "valid": true + }, + { + "chainage_m": 577.0, + "elevation_m": 857.054032, + "z": 857.054032, + "valid": true + }, + { + "chainage_m": 578.0, + "elevation_m": 856.883656, + "z": 856.883656, + "valid": true + }, + { + "chainage_m": 579.0, + "elevation_m": 856.559821, + "z": 856.559821, + "valid": true + }, + { + "chainage_m": 580.0, + "elevation_m": 856.057531, + "z": 856.057531, + "valid": true + }, + { + "chainage_m": 581.0, + "elevation_m": 855.644475, + "z": 855.644475, + "valid": true + }, + { + "chainage_m": 582.0, + "elevation_m": 855.50838, + "z": 855.50838, + "valid": true + }, + { + "chainage_m": 583.0, + "elevation_m": 855.369504, + "z": 855.369504, + "valid": true + }, + { + "chainage_m": 584.0, + "elevation_m": 855.077738, + "z": 855.077738, + "valid": true + }, + { + "chainage_m": 585.0, + "elevation_m": 854.991337, + "z": 854.991337, + "valid": true + }, + { + "chainage_m": 586.0, + "elevation_m": 854.641958, + "z": 854.641958, + "valid": true + }, + { + "chainage_m": 587.0, + "elevation_m": 854.534526, + "z": 854.534526, + "valid": true + }, + { + "chainage_m": 588.0, + "elevation_m": 854.251449, + "z": 854.251449, + "valid": true + }, + { + "chainage_m": 589.0, + "elevation_m": 854.120608, + "z": 854.120608, + "valid": true + }, + { + "chainage_m": 590.0, + "elevation_m": 853.986049, + "z": 853.986049, + "valid": true + }, + { + "chainage_m": 591.0, + "elevation_m": 854.147835, + "z": 854.147835, + "valid": true + }, + { + "chainage_m": 592.0, + "elevation_m": 854.30359, + "z": 854.30359, + "valid": true + }, + { + "chainage_m": 593.0, + "elevation_m": 854.168616, + "z": 854.168616, + "valid": true + }, + { + "chainage_m": 594.0, + "elevation_m": 853.891351, + "z": 853.891351, + "valid": true + }, + { + "chainage_m": 595.0, + "elevation_m": 853.670235, + "z": 853.670235, + "valid": true + }, + { + "chainage_m": 596.0, + "elevation_m": 853.423332, + "z": 853.423332, + "valid": true + }, + { + "chainage_m": 597.0, + "elevation_m": 853.236596, + "z": 853.236596, + "valid": true + }, + { + "chainage_m": 598.0, + "elevation_m": 853.170732, + "z": 853.170732, + "valid": true + }, + { + "chainage_m": 599.0, + "elevation_m": 853.117057, + "z": 853.117057, + "valid": true + }, + { + "chainage_m": 600.0, + "elevation_m": 852.920634, + "z": 852.920634, + "valid": true + }, + { + "chainage_m": 601.0, + "elevation_m": 852.692771, + "z": 852.692771, + "valid": true + }, + { + "chainage_m": 602.0, + "elevation_m": 852.711229, + "z": 852.711229, + "valid": true + }, + { + "chainage_m": 603.0, + "elevation_m": 852.929115, + "z": 852.929115, + "valid": true + }, + { + "chainage_m": 604.0, + "elevation_m": 853.068653, + "z": 853.068653, + "valid": true + }, + { + "chainage_m": 605.0, + "elevation_m": 852.587642, + "z": 852.587642, + "valid": true + }, + { + "chainage_m": 606.0, + "elevation_m": 851.858333, + "z": 851.858333, + "valid": true + }, + { + "chainage_m": 607.0, + "elevation_m": 851.933693, + "z": 851.933693, + "valid": true + }, + { + "chainage_m": 608.0, + "elevation_m": 852.13358, + "z": 852.13358, + "valid": true + }, + { + "chainage_m": 609.0, + "elevation_m": 852.10442, + "z": 852.10442, + "valid": true + }, + { + "chainage_m": 610.0, + "elevation_m": 851.989072, + "z": 851.989072, + "valid": true + }, + { + "chainage_m": 611.0, + "elevation_m": 852.048089, + "z": 852.048089, + "valid": true + }, + { + "chainage_m": 612.0, + "elevation_m": 852.201363, + "z": 852.201363, + "valid": true + }, + { + "chainage_m": 613.0, + "elevation_m": 852.280375, + "z": 852.280375, + "valid": true + }, + { + "chainage_m": 614.0, + "elevation_m": 852.336821, + "z": 852.336821, + "valid": true + }, + { + "chainage_m": 615.0, + "elevation_m": 852.433908, + "z": 852.433908, + "valid": true + }, + { + "chainage_m": 616.0, + "elevation_m": 852.283746, + "z": 852.283746, + "valid": true + }, + { + "chainage_m": 617.0, + "elevation_m": 851.996817, + "z": 851.996817, + "valid": true + }, + { + "chainage_m": 618.0, + "elevation_m": 851.459704, + "z": 851.459704, + "valid": true + }, + { + "chainage_m": 619.0, + "elevation_m": 851.163413, + "z": 851.163413, + "valid": true + }, + { + "chainage_m": 620.0, + "elevation_m": 850.964366, + "z": 850.964366, + "valid": true + }, + { + "chainage_m": 621.0, + "elevation_m": 850.778727, + "z": 850.778727, + "valid": true + }, + { + "chainage_m": 622.0, + "elevation_m": 850.8563, + "z": 850.8563, + "valid": true + }, + { + "chainage_m": 623.0, + "elevation_m": 850.974656, + "z": 850.974656, + "valid": true + }, + { + "chainage_m": 624.0, + "elevation_m": 850.894949, + "z": 850.894949, + "valid": true + }, + { + "chainage_m": 625.0, + "elevation_m": 850.610577, + "z": 850.610577, + "valid": true + }, + { + "chainage_m": 626.0, + "elevation_m": 850.293723, + "z": 850.293723, + "valid": true + }, + { + "chainage_m": 627.0, + "elevation_m": 850.155834, + "z": 850.155834, + "valid": true + }, + { + "chainage_m": 628.0, + "elevation_m": 850.12646, + "z": 850.12646, + "valid": true + }, + { + "chainage_m": 629.0, + "elevation_m": 850.478046, + "z": 850.478046, + "valid": true + }, + { + "chainage_m": 630.0, + "elevation_m": 850.244623, + "z": 850.244623, + "valid": true + }, + { + "chainage_m": 631.0, + "elevation_m": 850.112925, + "z": 850.112925, + "valid": true + }, + { + "chainage_m": 632.0, + "elevation_m": 850.191545, + "z": 850.191545, + "valid": true + }, + { + "chainage_m": 633.0, + "elevation_m": 850.249846, + "z": 850.249846, + "valid": true + }, + { + "chainage_m": 634.0, + "elevation_m": 850.483782, + "z": 850.483782, + "valid": true + }, + { + "chainage_m": 635.0, + "elevation_m": 850.528137, + "z": 850.528137, + "valid": true + }, + { + "chainage_m": 636.0, + "elevation_m": 850.570394, + "z": 850.570394, + "valid": true + }, + { + "chainage_m": 637.0, + "elevation_m": 850.427413, + "z": 850.427413, + "valid": true + }, + { + "chainage_m": 638.0, + "elevation_m": 850.316345, + "z": 850.316345, + "valid": true + }, + { + "chainage_m": 639.0, + "elevation_m": 850.192353, + "z": 850.192353, + "valid": true + }, + { + "chainage_m": 640.0, + "elevation_m": 849.970857, + "z": 849.970857, + "valid": true + }, + { + "chainage_m": 641.0, + "elevation_m": 849.630379, + "z": 849.630379, + "valid": true + }, + { + "chainage_m": 642.0, + "elevation_m": 849.659423, + "z": 849.659423, + "valid": true + }, + { + "chainage_m": 643.0, + "elevation_m": 849.718725, + "z": 849.718725, + "valid": true + }, + { + "chainage_m": 644.0, + "elevation_m": 849.829556, + "z": 849.829556, + "valid": true + }, + { + "chainage_m": 645.0, + "elevation_m": 849.677023, + "z": 849.677023, + "valid": true + }, + { + "chainage_m": 646.0, + "elevation_m": 849.414297, + "z": 849.414297, + "valid": true + }, + { + "chainage_m": 647.0, + "elevation_m": 849.360012, + "z": 849.360012, + "valid": true + }, + { + "chainage_m": 648.0, + "elevation_m": 849.082445, + "z": 849.082445, + "valid": true + }, + { + "chainage_m": 649.0, + "elevation_m": 848.730251, + "z": 848.730251, + "valid": true + }, + { + "chainage_m": 650.0, + "elevation_m": 848.521945, + "z": 848.521945, + "valid": true + }, + { + "chainage_m": 651.0, + "elevation_m": 848.638333, + "z": 848.638333, + "valid": true + }, + { + "chainage_m": 652.0, + "elevation_m": 848.802743, + "z": 848.802743, + "valid": true + }, + { + "chainage_m": 653.0, + "elevation_m": 849.003836, + "z": 849.003836, + "valid": true + }, + { + "chainage_m": 654.0, + "elevation_m": 848.537845, + "z": 848.537845, + "valid": true + }, + { + "chainage_m": 655.0, + "elevation_m": 848.337755, + "z": 848.337755, + "valid": true + }, + { + "chainage_m": 656.0, + "elevation_m": 848.304842, + "z": 848.304842, + "valid": true + }, + { + "chainage_m": 657.0, + "elevation_m": 848.390895, + "z": 848.390895, + "valid": true + }, + { + "chainage_m": 658.0, + "elevation_m": 848.38951, + "z": 848.38951, + "valid": true + }, + { + "chainage_m": 659.0, + "elevation_m": 848.399387, + "z": 848.399387, + "valid": true + }, + { + "chainage_m": 660.0, + "elevation_m": 848.360094, + "z": 848.360094, + "valid": true + }, + { + "chainage_m": 661.0, + "elevation_m": 848.389533, + "z": 848.389533, + "valid": true + }, + { + "chainage_m": 662.0, + "elevation_m": 848.308194, + "z": 848.308194, + "valid": true + }, + { + "chainage_m": 663.0, + "elevation_m": 848.212779, + "z": 848.212779, + "valid": true + }, + { + "chainage_m": 664.0, + "elevation_m": 848.109928, + "z": 848.109928, + "valid": true + }, + { + "chainage_m": 665.0, + "elevation_m": 848.100868, + "z": 848.100868, + "valid": true + }, + { + "chainage_m": 666.0, + "elevation_m": 848.04221, + "z": 848.04221, + "valid": true + }, + { + "chainage_m": 667.0, + "elevation_m": 847.988501, + "z": 847.988501, + "valid": true + }, + { + "chainage_m": 668.0, + "elevation_m": 847.947989, + "z": 847.947989, + "valid": true + }, + { + "chainage_m": 669.0, + "elevation_m": 848.06593, + "z": 848.06593, + "valid": true + }, + { + "chainage_m": 670.0, + "elevation_m": 848.220883, + "z": 848.220883, + "valid": true + }, + { + "chainage_m": 671.0, + "elevation_m": 847.961508, + "z": 847.961508, + "valid": true + }, + { + "chainage_m": 672.0, + "elevation_m": 848.126574, + "z": 848.126574, + "valid": true + }, + { + "chainage_m": 673.0, + "elevation_m": 848.032362, + "z": 848.032362, + "valid": true + }, + { + "chainage_m": 674.0, + "elevation_m": 847.78662, + "z": 847.78662, + "valid": true + }, + { + "chainage_m": 675.0, + "elevation_m": 847.53375, + "z": 847.53375, + "valid": true + }, + { + "chainage_m": 676.0, + "elevation_m": 847.573062, + "z": 847.573062, + "valid": true + }, + { + "chainage_m": 677.0, + "elevation_m": 847.909466, + "z": 847.909466, + "valid": true + }, + { + "chainage_m": 678.0, + "elevation_m": 847.948459, + "z": 847.948459, + "valid": true + }, + { + "chainage_m": 679.0, + "elevation_m": 847.739344, + "z": 847.739344, + "valid": true + }, + { + "chainage_m": 680.0, + "elevation_m": 847.460312, + "z": 847.460312, + "valid": true + }, + { + "chainage_m": 681.0, + "elevation_m": 847.168271, + "z": 847.168271, + "valid": true + }, + { + "chainage_m": 682.0, + "elevation_m": 847.060168, + "z": 847.060168, + "valid": true + }, + { + "chainage_m": 683.0, + "elevation_m": 846.989919, + "z": 846.989919, + "valid": true + }, + { + "chainage_m": 684.0, + "elevation_m": 846.686868, + "z": 846.686868, + "valid": true + }, + { + "chainage_m": 685.0, + "elevation_m": 846.464286, + "z": 846.464286, + "valid": true + }, + { + "chainage_m": 686.0, + "elevation_m": 846.501559, + "z": 846.501559, + "valid": true + }, + { + "chainage_m": 687.0, + "elevation_m": 846.774701, + "z": 846.774701, + "valid": true + }, + { + "chainage_m": 688.0, + "elevation_m": 846.745857, + "z": 846.745857, + "valid": true + }, + { + "chainage_m": 689.0, + "elevation_m": 846.612569, + "z": 846.612569, + "valid": true + }, + { + "chainage_m": 690.0, + "elevation_m": 846.165436, + "z": 846.165436, + "valid": true + }, + { + "chainage_m": 691.0, + "elevation_m": 845.994884, + "z": 845.994884, + "valid": true + }, + { + "chainage_m": 692.0, + "elevation_m": 845.858305, + "z": 845.858305, + "valid": true + }, + { + "chainage_m": 693.0, + "elevation_m": 845.856014, + "z": 845.856014, + "valid": true + }, + { + "chainage_m": 694.0, + "elevation_m": 845.874997, + "z": 845.874997, + "valid": true + }, + { + "chainage_m": 695.0, + "elevation_m": 845.692977, + "z": 845.692977, + "valid": true + }, + { + "chainage_m": 696.0, + "elevation_m": 845.572585, + "z": 845.572585, + "valid": true + }, + { + "chainage_m": 697.0, + "elevation_m": 845.493257, + "z": 845.493257, + "valid": true + }, + { + "chainage_m": 698.0, + "elevation_m": 845.332125, + "z": 845.332125, + "valid": true + }, + { + "chainage_m": 699.0, + "elevation_m": 845.278834, + "z": 845.278834, + "valid": true + }, + { + "chainage_m": 700.0, + "elevation_m": 845.27445, + "z": 845.27445, + "valid": true + }, + { + "chainage_m": 701.0, + "elevation_m": 844.912407, + "z": 844.912407, + "valid": true + }, + { + "chainage_m": 702.0, + "elevation_m": 844.792095, + "z": 844.792095, + "valid": true + }, + { + "chainage_m": 703.0, + "elevation_m": 844.608147, + "z": 844.608147, + "valid": true + }, + { + "chainage_m": 704.0, + "elevation_m": 844.745787, + "z": 844.745787, + "valid": true + }, + { + "chainage_m": 705.0, + "elevation_m": 844.822649, + "z": 844.822649, + "valid": true + }, + { + "chainage_m": 706.0, + "elevation_m": 844.65125, + "z": 844.65125, + "valid": true + }, + { + "chainage_m": 707.0, + "elevation_m": 844.537021, + "z": 844.537021, + "valid": true + }, + { + "chainage_m": 708.0, + "elevation_m": 844.553395, + "z": 844.553395, + "valid": true + }, + { + "chainage_m": 709.0, + "elevation_m": 844.618185, + "z": 844.618185, + "valid": true + }, + { + "chainage_m": 710.0, + "elevation_m": 844.41286, + "z": 844.41286, + "valid": true + }, + { + "chainage_m": 711.0, + "elevation_m": 844.000168, + "z": 844.000168, + "valid": true + }, + { + "chainage_m": 712.0, + "elevation_m": 843.70105, + "z": 843.70105, + "valid": true + }, + { + "chainage_m": 713.0, + "elevation_m": 843.546356, + "z": 843.546356, + "valid": true + }, + { + "chainage_m": 714.0, + "elevation_m": 843.328733, + "z": 843.328733, + "valid": true + }, + { + "chainage_m": 715.0, + "elevation_m": 843.229925, + "z": 843.229925, + "valid": true + }, + { + "chainage_m": 716.0, + "elevation_m": 843.082965, + "z": 843.082965, + "valid": true + }, + { + "chainage_m": 717.0, + "elevation_m": 843.3016, + "z": 843.3016, + "valid": true + }, + { + "chainage_m": 718.0, + "elevation_m": 843.327932, + "z": 843.327932, + "valid": true + }, + { + "chainage_m": 719.0, + "elevation_m": 843.516812, + "z": 843.516812, + "valid": true + }, + { + "chainage_m": 720.0, + "elevation_m": 843.926252, + "z": 843.926252, + "valid": true + }, + { + "chainage_m": 721.0, + "elevation_m": 844.901787, + "z": 844.901787, + "valid": true + }, + { + "chainage_m": 722.0, + "elevation_m": 845.343369, + "z": 845.343369, + "valid": true + }, + { + "chainage_m": 723.0, + "elevation_m": 845.407688, + "z": 845.407688, + "valid": true + }, + { + "chainage_m": 724.0, + "elevation_m": 845.988659, + "z": 845.988659, + "valid": true + }, + { + "chainage_m": 725.0, + "elevation_m": 846.16845, + "z": 846.16845, + "valid": true + }, + { + "chainage_m": 726.0, + "elevation_m": 846.381459, + "z": 846.381459, + "valid": true + }, + { + "chainage_m": 727.0, + "elevation_m": 847.055647, + "z": 847.055647, + "valid": true + }, + { + "chainage_m": 728.0, + "elevation_m": 847.414421, + "z": 847.414421, + "valid": true + }, + { + "chainage_m": 729.0, + "elevation_m": 847.576499, + "z": 847.576499, + "valid": true + }, + { + "chainage_m": 730.0, + "elevation_m": 848.206747, + "z": 848.206747, + "valid": true + }, + { + "chainage_m": 731.0, + "elevation_m": 848.405212, + "z": 848.405212, + "valid": true + }, + { + "chainage_m": 732.0, + "elevation_m": 848.792653, + "z": 848.792653, + "valid": true + }, + { + "chainage_m": 733.0, + "elevation_m": 849.283679, + "z": 849.283679, + "valid": true + }, + { + "chainage_m": 734.0, + "elevation_m": 849.687725, + "z": 849.687725, + "valid": true + }, + { + "chainage_m": 735.0, + "elevation_m": 850.005008, + "z": 850.005008, + "valid": true + }, + { + "chainage_m": 736.0, + "elevation_m": 850.159595, + "z": 850.159595, + "valid": true + }, + { + "chainage_m": 737.0, + "elevation_m": 849.998573, + "z": 849.998573, + "valid": true + }, + { + "chainage_m": 738.0, + "elevation_m": 849.167206, + "z": 849.167206, + "valid": true + }, + { + "chainage_m": 739.0, + "elevation_m": 848.697835, + "z": 848.697835, + "valid": true + }, + { + "chainage_m": 740.0, + "elevation_m": 848.425747, + "z": 848.425747, + "valid": true + }, + { + "chainage_m": 741.0, + "elevation_m": 848.114222, + "z": 848.114222, + "valid": true + }, + { + "chainage_m": 742.0, + "elevation_m": 847.984778, + "z": 847.984778, + "valid": true + }, + { + "chainage_m": 743.0, + "elevation_m": 847.996605, + "z": 847.996605, + "valid": true + }, + { + "chainage_m": 744.0, + "elevation_m": 847.943916, + "z": 847.943916, + "valid": true + }, + { + "chainage_m": 745.0, + "elevation_m": 847.415277, + "z": 847.415277, + "valid": true + }, + { + "chainage_m": 746.0, + "elevation_m": 847.713023, + "z": 847.713023, + "valid": true + }, + { + "chainage_m": 747.0, + "elevation_m": 849.351653, + "z": 849.351653, + "valid": true + }, + { + "chainage_m": 748.0, + "elevation_m": 849.375317, + "z": 849.375317, + "valid": true + }, + { + "chainage_m": 749.0, + "elevation_m": 848.149511, + "z": 848.149511, + "valid": true + }, + { + "chainage_m": 750.0, + "elevation_m": 847.367599, + "z": 847.367599, + "valid": true + }, + { + "chainage_m": 751.0, + "elevation_m": 847.265878, + "z": 847.265878, + "valid": true + }, + { + "chainage_m": 752.0, + "elevation_m": 847.207383, + "z": 847.207383, + "valid": true + }, + { + "chainage_m": 753.0, + "elevation_m": 847.250653, + "z": 847.250653, + "valid": true + }, + { + "chainage_m": 754.0, + "elevation_m": 847.33811, + "z": 847.33811, + "valid": true + }, + { + "chainage_m": 755.0, + "elevation_m": 846.84956, + "z": 846.84956, + "valid": true + }, + { + "chainage_m": 756.0, + "elevation_m": 846.252422, + "z": 846.252422, + "valid": true + }, + { + "chainage_m": 757.0, + "elevation_m": 846.169892, + "z": 846.169892, + "valid": true + }, + { + "chainage_m": 758.0, + "elevation_m": 845.921486, + "z": 845.921486, + "valid": true + }, + { + "chainage_m": 759.0, + "elevation_m": 844.818659, + "z": 844.818659, + "valid": true + }, + { + "chainage_m": 760.0, + "elevation_m": 844.434274, + "z": 844.434274, + "valid": true + }, + { + "chainage_m": 761.0, + "elevation_m": 844.296984, + "z": 844.296984, + "valid": true + }, + { + "chainage_m": 762.0, + "elevation_m": 844.132591, + "z": 844.132591, + "valid": true + }, + { + "chainage_m": 763.0, + "elevation_m": 843.716978, + "z": 843.716978, + "valid": true + }, + { + "chainage_m": 764.0, + "elevation_m": 843.540954, + "z": 843.540954, + "valid": true + }, + { + "chainage_m": 765.0, + "elevation_m": 842.964549, + "z": 842.964549, + "valid": true + }, + { + "chainage_m": 766.0, + "elevation_m": 842.896137, + "z": 842.896137, + "valid": true + }, + { + "chainage_m": 767.0, + "elevation_m": 842.458402, + "z": 842.458402, + "valid": true + }, + { + "chainage_m": 768.0, + "elevation_m": 842.018798, + "z": 842.018798, + "valid": true + }, + { + "chainage_m": 769.0, + "elevation_m": 841.42706, + "z": 841.42706, + "valid": true + }, + { + "chainage_m": 770.0, + "elevation_m": 840.784849, + "z": 840.784849, + "valid": true + }, + { + "chainage_m": 771.0, + "elevation_m": 840.82025, + "z": 840.82025, + "valid": true + }, + { + "chainage_m": 772.0, + "elevation_m": 841.07306, + "z": 841.07306, + "valid": true + }, + { + "chainage_m": 773.0, + "elevation_m": 841.446496, + "z": 841.446496, + "valid": true + }, + { + "chainage_m": 774.0, + "elevation_m": 841.416258, + "z": 841.416258, + "valid": true + }, + { + "chainage_m": 775.0, + "elevation_m": 841.013515, + "z": 841.013515, + "valid": true + }, + { + "chainage_m": 776.0, + "elevation_m": 840.507013, + "z": 840.507013, + "valid": true + }, + { + "chainage_m": 777.0, + "elevation_m": 840.571919, + "z": 840.571919, + "valid": true + }, + { + "chainage_m": 778.0, + "elevation_m": 841.1076, + "z": 841.1076, + "valid": true + }, + { + "chainage_m": 779.0, + "elevation_m": 841.040727, + "z": 841.040727, + "valid": true + }, + { + "chainage_m": 780.0, + "elevation_m": 841.261415, + "z": 841.261415, + "valid": true + }, + { + "chainage_m": 781.0, + "elevation_m": 840.825901, + "z": 840.825901, + "valid": true + }, + { + "chainage_m": 782.0, + "elevation_m": 840.98212, + "z": 840.98212, + "valid": true + }, + { + "chainage_m": 783.0, + "elevation_m": 840.934341, + "z": 840.934341, + "valid": true + }, + { + "chainage_m": 784.0, + "elevation_m": 840.77192, + "z": 840.77192, + "valid": true + }, + { + "chainage_m": 785.0, + "elevation_m": 840.90707, + "z": 840.90707, + "valid": true + }, + { + "chainage_m": 786.0, + "elevation_m": 841.003614, + "z": 841.003614, + "valid": true + }, + { + "chainage_m": 787.0, + "elevation_m": 840.828882, + "z": 840.828882, + "valid": true + }, + { + "chainage_m": 788.0, + "elevation_m": 840.494601, + "z": 840.494601, + "valid": true + }, + { + "chainage_m": 789.0, + "elevation_m": 840.248678, + "z": 840.248678, + "valid": true + }, + { + "chainage_m": 790.0, + "elevation_m": 840.251876, + "z": 840.251876, + "valid": true + }, + { + "chainage_m": 791.0, + "elevation_m": 840.197685, + "z": 840.197685, + "valid": true + }, + { + "chainage_m": 792.0, + "elevation_m": 840.081749, + "z": 840.081749, + "valid": true + }, + { + "chainage_m": 793.0, + "elevation_m": 840.018425, + "z": 840.018425, + "valid": true + }, + { + "chainage_m": 794.0, + "elevation_m": 840.037817, + "z": 840.037817, + "valid": true + }, + { + "chainage_m": 795.0, + "elevation_m": 840.096343, + "z": 840.096343, + "valid": true + }, + { + "chainage_m": 796.0, + "elevation_m": 840.017159, + "z": 840.017159, + "valid": true + }, + { + "chainage_m": 797.0, + "elevation_m": 839.990881, + "z": 839.990881, + "valid": true + }, + { + "chainage_m": 798.0, + "elevation_m": 840.023953, + "z": 840.023953, + "valid": true + }, + { + "chainage_m": 799.0, + "elevation_m": 840.016825, + "z": 840.016825, + "valid": true + }, + { + "chainage_m": 800.0, + "elevation_m": 839.97829, + "z": 839.97829, + "valid": true + }, + { + "chainage_m": 801.0, + "elevation_m": 839.755273, + "z": 839.755273, + "valid": true + }, + { + "chainage_m": 802.0, + "elevation_m": 840.031205, + "z": 840.031205, + "valid": true + }, + { + "chainage_m": 803.0, + "elevation_m": 840.077855, + "z": 840.077855, + "valid": true + }, + { + "chainage_m": 804.0, + "elevation_m": 840.049919, + "z": 840.049919, + "valid": true + }, + { + "chainage_m": 805.0, + "elevation_m": 839.804295, + "z": 839.804295, + "valid": true + }, + { + "chainage_m": 806.0, + "elevation_m": 839.497403, + "z": 839.497403, + "valid": true + }, + { + "chainage_m": 807.0, + "elevation_m": 839.357147, + "z": 839.357147, + "valid": true + }, + { + "chainage_m": 808.0, + "elevation_m": 839.724823, + "z": 839.724823, + "valid": true + }, + { + "chainage_m": 809.0, + "elevation_m": 839.723196, + "z": 839.723196, + "valid": true + }, + { + "chainage_m": 810.0, + "elevation_m": 839.605801, + "z": 839.605801, + "valid": true + }, + { + "chainage_m": 811.0, + "elevation_m": 839.557805, + "z": 839.557805, + "valid": true + }, + { + "chainage_m": 812.0, + "elevation_m": 839.554231, + "z": 839.554231, + "valid": true + }, + { + "chainage_m": 813.0, + "elevation_m": 839.571381, + "z": 839.571381, + "valid": true + }, + { + "chainage_m": 814.0, + "elevation_m": 839.452099, + "z": 839.452099, + "valid": true + }, + { + "chainage_m": 815.0, + "elevation_m": 839.234901, + "z": 839.234901, + "valid": true + }, + { + "chainage_m": 816.0, + "elevation_m": 838.82685, + "z": 838.82685, + "valid": true + }, + { + "chainage_m": 817.0, + "elevation_m": 838.614957, + "z": 838.614957, + "valid": true + }, + { + "chainage_m": 818.0, + "elevation_m": 838.551397, + "z": 838.551397, + "valid": true + }, + { + "chainage_m": 819.0, + "elevation_m": 838.645889, + "z": 838.645889, + "valid": true + }, + { + "chainage_m": 820.0, + "elevation_m": 838.404772, + "z": 838.404772, + "valid": true + }, + { + "chainage_m": 821.0, + "elevation_m": 838.298295, + "z": 838.298295, + "valid": true + }, + { + "chainage_m": 822.0, + "elevation_m": 838.566679, + "z": 838.566679, + "valid": true + }, + { + "chainage_m": 823.0, + "elevation_m": 838.541365, + "z": 838.541365, + "valid": true + }, + { + "chainage_m": 824.0, + "elevation_m": 838.794145, + "z": 838.794145, + "valid": true + }, + { + "chainage_m": 825.0, + "elevation_m": 838.922882, + "z": 838.922882, + "valid": true + }, + { + "chainage_m": 826.0, + "elevation_m": 838.682894, + "z": 838.682894, + "valid": true + }, + { + "chainage_m": 827.0, + "elevation_m": 838.502808, + "z": 838.502808, + "valid": true + }, + { + "chainage_m": 828.0, + "elevation_m": 839.166363, + "z": 839.166363, + "valid": true + }, + { + "chainage_m": 829.0, + "elevation_m": 839.712109, + "z": 839.712109, + "valid": true + }, + { + "chainage_m": 830.0, + "elevation_m": 839.842892, + "z": 839.842892, + "valid": true + }, + { + "chainage_m": 831.0, + "elevation_m": 839.481282, + "z": 839.481282, + "valid": true + }, + { + "chainage_m": 832.0, + "elevation_m": 839.26726, + "z": 839.26726, + "valid": true + }, + { + "chainage_m": 833.0, + "elevation_m": 839.42614, + "z": 839.42614, + "valid": true + }, + { + "chainage_m": 834.0, + "elevation_m": 839.630968, + "z": 839.630968, + "valid": true + }, + { + "chainage_m": 835.0, + "elevation_m": 839.47773, + "z": 839.47773, + "valid": true + }, + { + "chainage_m": 836.0, + "elevation_m": 839.400899, + "z": 839.400899, + "valid": true + }, + { + "chainage_m": 837.0, + "elevation_m": 839.818858, + "z": 839.818858, + "valid": true + }, + { + "chainage_m": 838.0, + "elevation_m": 839.806895, + "z": 839.806895, + "valid": true + }, + { + "chainage_m": 839.0, + "elevation_m": 839.811075, + "z": 839.811075, + "valid": true + }, + { + "chainage_m": 840.0, + "elevation_m": 839.942178, + "z": 839.942178, + "valid": true + } + ], + "stations": [ + { + "station_id": "station_000000460000", + "chainage_m": 460.0, + "label": "STA.0+460.000", + "kind": "regular" + }, + { + "station_id": "station_000000480000", + "chainage_m": 480.0, + "label": "STA.0+480.000", + "kind": "regular" + }, + { + "station_id": "station_000000484210", + "chainage_m": 484.21, + "label": "STA.0+484.210", + "kind": "irregular", + "structure": "파형강관 D800" + }, + { + "station_id": "station_000000500000", + "chainage_m": 500.0, + "label": "STA.0+500.000", + "kind": "regular" + }, + { + "station_id": "station_000000520000", + "chainage_m": 520.0, + "label": "STA.0+520.000", + "kind": "regular" + }, + { + "station_id": "station_000000540000", + "chainage_m": 540.0, + "label": "STA.0+540.000", + "kind": "regular" + }, + { + "station_id": "station_000000560000", + "chainage_m": 560.0, + "label": "STA.0+560.000", + "kind": "regular" + }, + { + "station_id": "station_000000580000", + "chainage_m": 580.0, + "label": "STA.0+580.000", + "kind": "regular" + }, + { + "station_id": "station_000000581900", + "chainage_m": 581.9, + "label": "STA.0+581.900", + "kind": "irregular", + "structure": "파형강관 D800" + }, + { + "station_id": "station_000000600000", + "chainage_m": 600.0, + "label": "STA.0+600.000", + "kind": "regular" + }, + { + "station_id": "station_000000620000", + "chainage_m": 620.0, + "label": "STA.0+620.000", + "kind": "regular" + }, + { + "station_id": "station_000000640000", + "chainage_m": 640.0, + "label": "STA.0+640.000", + "kind": "regular" + }, + { + "station_id": "station_000000660000", + "chainage_m": 660.0, + "label": "STA.0+660.000", + "kind": "regular" + }, + { + "station_id": "station_000000676590", + "chainage_m": 676.59, + "label": "STA.0+676.590", + "kind": "irregular", + "structure": "파형강관 D1500" + }, + { + "station_id": "station_000000680000", + "chainage_m": 680.0, + "label": "STA.0+680.000", + "kind": "regular" + }, + { + "station_id": "station_000000700000", + "chainage_m": 700.0, + "label": "STA.0+700.000", + "kind": "regular" + }, + { + "station_id": "station_000000720000", + "chainage_m": 720.0, + "label": "STA.0+720.000", + "kind": "regular" + }, + { + "station_id": "station_000000740000", + "chainage_m": 740.0, + "label": "STA.0+740.000", + "kind": "regular" + }, + { + "station_id": "station_000000760000", + "chainage_m": 760.0, + "label": "STA.0+760.000", + "kind": "regular" + }, + { + "station_id": "station_000000769280", + "chainage_m": 769.28, + "label": "STA.0+769.280", + "kind": "irregular", + "structure": "파형강관 D800" + }, + { + "station_id": "station_000000780000", + "chainage_m": 780.0, + "label": "STA.0+780.000", + "kind": "regular" + }, + { + "station_id": "station_000000800000", + "chainage_m": 800.0, + "label": "STA.0+800.000", + "kind": "regular" + }, + { + "station_id": "station_000000820000", + "chainage_m": 820.0, + "label": "STA.0+820.000", + "kind": "regular" + }, + { + "station_id": "station_000000840000", + "chainage_m": 840.0, + "label": "STA.0+840.000", + "kind": "regular" + } + ], + "design_profiles": [ + { + "schema_version": 1, + "id": "design_grade_line", + "name": "계획선", + "basis": "ground_polyline", + "criteria": { + "max_grade_pct": 14.0, + "max_reverse_grade_pct": 5.0, + "min_vertical_radius_m": 100.0, + "min_curve_length_m": 20.0, + "min_tangent_length_m": 20.0, + "vertical_curve_skip_delta_pct": 5.0, + "design_speed_kph": 20, + "terrain_type": "special", + "paved": false, + "main_direction": "auto", + "balance_segment_length_m": null, + "start_elevation_offset_m": 0.0, + "end_elevation_offset_m": 0.0, + "enforce_pipe_clearance": false, + "resolved_main_direction": "descending" + } + } + ], + "profile_alignment": { + "schema_version": 1, + "policy": { + "station_interval_m": 20.0, + "curve_radius_ratio": 0.4, + "default_curve_length_m": 15.0, + "curve_tangent_max_ratio": 0.45, + "curve_skip_legal_exception": false, + "default_curve_radius_m": 8.0, + "balance_tolerance_percent": 10.0, + "edit_step_m": 0.1, + "grade_violation_policy": "warn", + "max_grade_pct": 14.0, + "curve_skip_delta_pct": 5.0, + "paved": false, + "min_plan_radius_m": 0.0, + "hairpin_min_radius_m": 0.0 + }, + "only_explicit_curves": false, + "base_pvi": [ + { + "chainage_m": 460.0, + "elevation_m": 855.2479 + }, + { + "chainage_m": 480.0, + "elevation_m": 854.398 + }, + { + "chainage_m": 484.213, + "elevation_m": 854.1746 + }, + { + "chainage_m": 500.0, + "elevation_m": 853.244 + }, + { + "chainage_m": 520.0, + "elevation_m": 853.1798 + }, + { + "chainage_m": 540.0, + "elevation_m": 854.0777 + }, + { + "chainage_m": 560.0, + "elevation_m": 854.8932 + }, + { + "chainage_m": 580.0, + "elevation_m": 854.5863 + }, + { + "chainage_m": 581.902, + "elevation_m": 854.4747 + }, + { + "chainage_m": 600.0, + "elevation_m": 853.617 + }, + { + "chainage_m": 620.0, + "elevation_m": 851.7089 + }, + { + "chainage_m": 640.0, + "elevation_m": 849.5565 + }, + { + "chainage_m": 660.0, + "elevation_m": 848.391 + }, + { + "chainage_m": 676.591, + "elevation_m": 847.4059 + }, + { + "chainage_m": 680.0, + "elevation_m": 847.2193 + }, + { + "chainage_m": 700.0, + "elevation_m": 845.4289 + }, + { + "chainage_m": 720.0, + "elevation_m": 844.6952 + }, + { + "chainage_m": 740.0, + "elevation_m": 844.3864 + }, + { + "chainage_m": 760.0, + "elevation_m": 842.9962 + }, + { + "chainage_m": 769.28, + "elevation_m": 842.016371 + }, + { + "chainage_m": 780.0, + "elevation_m": 840.8845 + }, + { + "chainage_m": 800.0, + "elevation_m": 839.1705 + }, + { + "chainage_m": 820.0, + "elevation_m": 838.4651 + }, + { + "chainage_m": 840.0, + "elevation_m": 837.4166 + } + ], + "edits": { + "station_offsets": { + "769.280": 0.506058, + "769.380": 0.051428 + }, + "curve_radii": {} + } + } +} \ No newline at end of file diff --git a/resources/tester/test_192_1_moved_pipe_station.py b/resources/tester/test_192_1_moved_pipe_station.py index 9e3d9f902..680bf5e4a 100644 --- a/resources/tester/test_192_1_moved_pipe_station.py +++ b/resources/tester/test_192_1_moved_pipe_station.py @@ -4,7 +4,8 @@ 769.28 에 남음 → B06(측점 769.28)과 B05(관 769.38) 두 자리에 계획고 편집이 남아 0.1m 사이 −465% 계단 · 769.38 은 측점선 없는 꼭짓점. 고침: 판정 폭 0.005m(측점 스냅 폭) · 측점을 다시 뜨면 사라진 측점의 꼭짓점 · 편집을 지움 -(반올림 차이 0.006m 안은 옮김). 사용자 storage 는 읽기만 — 사본(tmp_path)으로 재현. +(반올림 차이 0.006m 안은 옮김). 자료 = 고정 사본(fixtures/b05_192_1_moved_pipe_longitudinal.json · +226fc805 종단 460~840m · 측점 769.28 인 192 이전 상태 · 213-2) — 사용자 storage 에 기대지 않음. """ from __future__ import annotations @@ -22,17 +23,13 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) import B05_Profile.B05_Profile_Engine_Sections as sections # noqa: E402 +from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile # noqa: E402 from B05_Profile.B05_Profile_Router_Confirm import _merge_irregular_into_longitudinal # noqa: E402 from B06_Section.B06_Section_Router_Stations import _missing_marks # noqa: E402 -SOURCE = ( - PROJECT_ROOT - / "storage/1/3/226fc805-26c8-4f5a-9956-bacb200e10a5/B06_Section/longitudinal/longitudinal.json" -) +SOURCE = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json" REL = "B06_Section/longitudinal/longitudinal.json" -needs_copy = pytest.mark.skipif(not SOURCE.is_file(), reason="검증_랩탑메인 storage 없음") - def _copy(tmp_path: Path) -> Path: target = tmp_path / REL @@ -58,20 +55,18 @@ def _irregular(data: dict, old: float, new: float) -> list[dict]: return result -@needs_copy def test_원본_자료가_숨은_꼭짓점을_가짐() -> None: """까닭 근거 — 769.38 꼭짓점은 측점 목록에 없고 두 편집 사이 −465%.""" data = _load(SOURCE) - alignment = data["profile_alignment"] + alignment, _ = rebuild_alignment_profile(data, data["profile_alignment"]["edits"]) stations = [s["chainage_m"] for s in data["stations"]] pvis = [p["chainage_m"] for p in alignment["pvi"]] assert 769.38 in pvis and 769.38 not in stations and 769.28 in stations - assert set(alignment["edits"]["station_offsets"]) == {"769.280", "769.380"} + assert {"769.280", "769.380"} <= set(alignment["edits"]["station_offsets"]) step = next(s for s in alignment["segments"] if s["from_m"] == 769.28) assert step["to_m"] == 769.38 and step["grade_percent"] < -400 -@needs_copy def test_관_옮긴_측점_다시_뜨면_숨은_꼭짓점_사라짐(tmp_path: Path) -> None: path = _copy(tmp_path) data = _load(path) @@ -90,7 +85,6 @@ def test_관_옮긴_측점_다시_뜨면_숨은_꼭짓점_사라짐(tmp_path: Pa assert after["design_profiles"][0]["summary"]["max_grade_pct"] == summary["max_grade_pct"] -@needs_copy def test_반올림_차이만_다르면_편집을_옮김(tmp_path: Path) -> None: path = _copy(tmp_path) data = _load(path) @@ -101,7 +95,6 @@ def test_반올림_차이만_다르면_편집을_옮김(tmp_path: Path) -> None: assert "484.210" not in offsets and offsets["484.213"] == pytest.approx(0.3) -@needs_copy def test_측점이_그대로면_계획선을_건드리지_않음(tmp_path: Path) -> None: path = _copy(tmp_path) data = _load(path) diff --git a/resources/tester/test_213_2_ghost_edit_drop.py b/resources/tester/test_213_2_ghost_edit_drop.py new file mode 100644 index 000000000..71f63b77f --- /dev/null +++ b/resources/tester/test_213_2_ghost_edit_drop.py @@ -0,0 +1,183 @@ +"""213-2 — 유령 계획고 편집 근본 막음. + +① 관을 옮기면 편집 키가 새 자리로 따라감 — 화면 `followedStation` + (B05_Profile_UI_Profile_Alignment.ts · B05 applyIrregularStations) · + B06 movePipeTo 는 resetStationEdit(옛, 새). +② [저장] 때 서버가 측점 · 관 · 구조물 자리 어디에도 없는 편집 키를 버리고 자리 목록을 돌려줌 + (`drop_unplaced_edits` · `_apply_alignment_edits(marks=...)`). +자료 = 고정 사본(fixtures/b05_192_1_moved_pipe_longitudinal.json · 226fc805 종단 460~840m). +""" + +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + +import numpy as np +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[2] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + +from B05_Profile.B05_Profile_Engine_Grade_Alignment import drop_unplaced_edits # noqa: E402 +from B05_Profile.B05_Profile_Router import _apply_alignment_edits # noqa: E402 + +TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc" +FIXTURE = PROJECT_ROOT / "resources/tester/fixtures/b05_192_1_moved_pipe_longitudinal.json" +REL = "B06_Section/longitudinal/longitudinal.json" +GHOST = {"station_offsets": {"769.280": 0.506058, "769.380": 0.051428}, "curve_radii": {}} + + +def test_drop_keeps_station_and_marks_only(): + edits = { + "station_offsets": {"20.000": 1.0, "263.206": 0.5, "263.310": 0.9, "400.000": 0.2}, + "curve_radii": {"263.206": 300.0, "500.000": 80.0}, + } + kept, dropped = drop_unplaced_edits(edits, np.array([0.0, 20.0, 263.21, 263.31])) + # 263.206 = 측점 263.21 의 0.006m 안(반올림) · 263.31 = 관 자리 → 남음 + assert kept["station_offsets"] == {"20.000": 1.0, "263.206": 0.5, "263.310": 0.9} + assert kept["curve_radii"] == {"263.206": 300.0} + assert dropped == [400.0, 500.0] + + +def _project(tmp_path: Path, station_m: float) -> Path: + """고정 사본 — 측점 769.28(옮긴 관, 측점 안 다시 뜸) 또는 769.38(다시 뜸 · 옛 꼭짓점 없음).""" + data = json.loads(FIXTURE.read_text(encoding="utf-8")) + if station_m != 769.28: + for station in data["stations"]: + if abs(station["chainage_m"] - 769.28) < 1e-6: + station.update(chainage_m=station_m, station_id="station_000000769380") + base = data["profile_alignment"]["base_pvi"] + data["profile_alignment"]["base_pvi"] = [ + node for node in base if abs(node["chainage_m"] - 769.28) > 1e-6 + ] + target = tmp_path / REL + target.parent.mkdir(parents=True) + target.write_text(json.dumps(data), encoding="utf-8") + return target + + +def test_save_drops_ghost_after_station_regenerated(tmp_path): + """226fc805 꼴 — 측점 769.38 로 다시 섰는데 세션 초안이 옛 키 769.280 을 다시 보냄.""" + path = _project(tmp_path, 769.38) + alignment, summary, dropped = _apply_alignment_edits(tmp_path, REL, GHOST, [769.38]) + assert dropped == [769.28] + assert alignment["edits"]["station_offsets"] == {"769.380": 0.051428} + near = [p["chainage_m"] for p in alignment["pvi"] if 760 < p["chainage_m"] < 780] + assert near == [769.38] + assert summary["max_grade_pct"] < 100 + saved = json.loads(path.read_text(encoding="utf-8"))["profile_alignment"] + assert "769.280" not in saved["edits"]["station_offsets"] + + +def test_save_keeps_moved_pipe_edit_before_regeneration(tmp_path): + """관만 옮기고 측점은 옛 자리(769.28) — 옛 측점 편집 · 새 관 자리 편집 둘 다 지킴.""" + _project(tmp_path, 769.28) + alignment, _, dropped = _apply_alignment_edits(tmp_path, REL, GHOST, [769.38]) + assert dropped == [] + assert set(alignment["edits"]["station_offsets"]) == {"769.280", "769.380"} + + +def test_save_without_marks_drops_nothing(tmp_path): + """노선이 안 맞아 관 · 구조물 자리를 못 읽으면 버리지 않음(잃는 쪽보다 남는 쪽).""" + _project(tmp_path, 769.38) + _, _, dropped = _apply_alignment_edits(tmp_path, REL, GHOST, None) + assert dropped == [] + + +_RUNNER = """ +import { readFileSync, writeFileSync } from "node:fs"; +import { followedStation } from "./B05_Profile_UI_Profile_Alignment.js"; +const cases = JSON.parse(readFileSync(process.argv[2], "utf8")); +const out = cases.map((c) => followedStation(c.station, c.gone, c.born)?.chainage_m ?? null); +writeFileSync(process.argv[3], JSON.stringify(out)); +""" + +ST = lambda sid, m: {"id": sid, "chainage_m": m} # noqa: E731 +CASES = [ + # 옮긴 관 하나(키 한 번 +0.1) → 따라감 + ( + { + "station": ST("pipe-263.21", 263.21), + "gone": [ST("pipe-263.21", 263.21)], + "born": [ST("pipe-263.31", 263.31)], + }, + 263.31, + ), + # 반올림(263.206 → 263.21) → 따라감 + ( + { + "station": ST("pipe-263.21", 263.206), + "gone": [ST("pipe-263.21", 263.206)], + "born": [ST("pipe-263.21", 263.21)], + }, + 263.21, + ), + # A군 구조물 = 같은 id 로 + ( + { + "station": ST("s1", 100.0), + "gone": [ST("s1", 100.0), ST("pipe-5.00", 5.0)], + "born": [ST("pipe-9.00", 9.0), ST("s1", 130.0)], + }, + 130.0, + ), + # 둘이 한꺼번에 바뀜(지움 + 새로 놓음 둘) → 안 따라감(지움) + ( + { + "station": ST("pipe-5.00", 5.0), + "gone": [ST("pipe-5.00", 5.0), ST("pipe-7.00", 7.0)], + "born": [ST("pipe-50.00", 50.0), ST("pipe-70.00", 70.0)], + }, + None, + ), + # 지우기만 → 지움 + ({"station": ST("pipe-5.00", 5.0), "gone": [ST("pipe-5.00", 5.0)], "born": []}, None), +] + + +def test_followed_station_ts(tmp_path): + if not TSC.is_file(): + pytest.skip("프론트엔드 의존성 미설치") + out = tmp_path / "js" + subprocess.run( # noqa: S603 — 고정 실행 파일 + [ + "node", + str(TSC), + str(PROJECT_ROOT / "B05_Profile" / "B05_Profile_UI_Profile_Alignment.ts"), + "--outDir", + str(out), + "--module", + "esnext", + "--target", + "es2022", + "--moduleResolution", + "bundler", + "--ignoreConfig", + ], + cwd=str(PROJECT_ROOT), + check=True, + capture_output=True, + ) + (out / "runner.mjs").write_text(_RUNNER, encoding="utf-8") + src, dst = tmp_path / "in.json", tmp_path / "out.json" + src.write_text(json.dumps([case for case, _ in CASES]), encoding="utf-8") + subprocess.run( # noqa: S603 + ["node", str(out / "runner.mjs"), str(src), str(dst)], check=True, capture_output=True + ) + assert json.loads(dst.read_text(encoding="utf-8")) == [want for _, want in CASES] + + +def test_move_sites_follow_edits(): + """옮김 자리 둘이 편집을 따라 옮김 — B06 movePipeTo · B05 측점 갱신(문자열 짝 확인).""" + b06 = (PROJECT_ROOT / "B06_Section/B06_Section_UI_Page_Structures_Panel.ts").read_text( + encoding="utf-8" + ) + b05 = (PROJECT_ROOT / "B05_Profile/B05_Profile_UI_Page_Structures.ts").read_text( + encoding="utf-8" + ) + assert "deps.shared.profile?.resetStationEdit(hit.chainage_m, to)" in b06 + assert "followedStation(station, gone, born)" in b05