fix(349-0v): 손으로 고친 노선 [저장] = 계획고 옮김만 · 바뀐 구간 = 화면과 같은 계획 폴리라인
손 편집 [저장]이 추천 갈래를 따라 균형 종단을 새로 풀어 사용자 계획고가 노선 전체에서 바뀌던 흠. 화면은 balanced_profile 안 보냄(profileShown 없앰) · 서버도 균형 종단은 as_initial(추천 [적용])일 때만. 계획고 옮김의 바뀐 구간 = 옛 계획 폴리라인(read_route_plan planned) 대 새로 쓴 계획 폴리라인 — 풀린 노선 geojson(점 간격 다름) 대신 화면 planline 단계와 같은 선. 거울 시험 test_349_0v. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
@@ -19,8 +19,8 @@ API — `POST /api/projects/{id}/route/replan`(`B05_Profile_Router_Replan.replan
|
|||||||
- 관 정본을 지우지 않음 — 좌표를 따라감 · 새 세류선 교차점 기본 관 · 오목 지점 보충만 더함
|
- 관 정본을 지우지 않음 — 좌표를 따라감 · 새 세류선 교차점 기본 관 · 오목 지점 보충만 더함
|
||||||
(`B05_Profile_Route_Save_Parts`) · 배수유역 저장분은 노선 지문이 달라 「낡음」(B04 `_resolve`).
|
(`B05_Profile_Route_Save_Parts`) · 배수유역 저장분은 노선 지문이 달라 「낡음」(B04 `_resolve`).
|
||||||
- 계획고 = 바뀐 구간만 새 지반 첫 계획선 · 그 밖 사용자 계획고 지킴
|
- 계획고 = 바뀐 구간만 새 지반 첫 계획선 · 그 밖 사용자 계획고 지킴
|
||||||
(`B05_Profile_Engine_Grade_Carry`) · 추천 「종단 반영」 켬이면 균형 종단
|
(`B05_Profile_Engine_Grade_Carry` · 바뀐 구간 = 옛 · 새 **계획 폴리라인** — 화면 planline 과
|
||||||
(`apply_profile_target`).
|
같은 선) · 균형 종단(`apply_profile_target`)은 추천 [적용](`as_initial`)만.
|
||||||
- 초기값은 여기서 안 뜸 — 부르는 쪽이 `as_initial` 일 때만(자동설계 체인 직후 + 추천 [적용]).
|
- 초기값은 여기서 안 뜸 — 부르는 쪽이 `as_initial` 일 때만(자동설계 체인 직후 + 추천 [적용]).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -345,8 +345,13 @@ async def run_route_save_chain(
|
|||||||
points: list[dict[str, float]],
|
points: list[dict[str, float]],
|
||||||
previous_params: dict[str, Any],
|
previous_params: dict[str, Any],
|
||||||
profile_target: tuple[list[float], list[float]] | None = None,
|
profile_target: tuple[list[float], list[float]] | None = None,
|
||||||
|
planned_lines: tuple[list[list[float]], list[list[float]]] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""노선 저장 체인 — 성공이면 요약 dict · 실패면 `{"error": 사유}`(파일 · 행은 되살린 뒤)."""
|
"""노선 저장 체인 — 성공이면 요약 dict · 실패면 `{"error": 사유}`(파일 · 행은 되살린 뒤).
|
||||||
|
|
||||||
|
`planned_lines` = (옛 · 새 계획 폴리라인) — 바뀐 구간을 화면 planline 단계와 같은 선으로 가름
|
||||||
|
(없으면 풀린 노선 geojson · 점 간격이 달라 앞머리가 일찍 갈림).
|
||||||
|
"""
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
from B03_FileInput.B03_FileInput_Service_Chain import _log_steps, redesign_solve_request
|
from B03_FileInput.B03_FileInput_Service_Chain import _log_steps, redesign_solve_request
|
||||||
@@ -445,8 +450,7 @@ async def run_route_save_chain(
|
|||||||
relative,
|
relative,
|
||||||
new_route_id,
|
new_route_id,
|
||||||
old_long.get("profile_alignment") or {},
|
old_long.get("profile_alignment") or {},
|
||||||
old_xy,
|
*(planned_lines or (old_xy, new_xy)),
|
||||||
new_xy,
|
|
||||||
)
|
)
|
||||||
marks.append(("계획고 옮김", time.perf_counter()))
|
marks.append(("계획고 옮김", time.perf_counter()))
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,15 @@ def _vertices_of(path: Path) -> list[list[float]]:
|
|||||||
return [[float(vertex.x), float(vertex.y)] for vertex in route.vertices]
|
return [[float(vertex.x), float(vertex.y)] for vertex in route.vertices]
|
||||||
|
|
||||||
|
|
||||||
|
def _planned_line(project_root: Path) -> list[list[float]]:
|
||||||
|
"""화면이 받는 저장 노선(`read_route_plan` planned) — 수정본 · 초기 폴리라인 · 예상노선 차례."""
|
||||||
|
return (
|
||||||
|
_vertices_of(planned_route_working_path(project_root))
|
||||||
|
or _vertices_of(planned_route_initial_path(project_root))
|
||||||
|
or _vertices_of(expected_route_csv_path(project_root))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ensure_expected_route(project_root: Path) -> str:
|
def _ensure_expected_route(project_root: Path) -> str:
|
||||||
"""예상노선(원본) 정본이 없으면 만들어 둔다. 어디서 씨앗을 얻었는지 돌려준다.
|
"""예상노선(원본) 정본이 없으면 만들어 둔다. 어디서 씨앗을 얻었는지 돌려준다.
|
||||||
|
|
||||||
@@ -327,10 +336,12 @@ async def _recompute(
|
|||||||
stored_path: str,
|
stored_path: str,
|
||||||
balanced: RouteInitialRequest | None = None,
|
balanced: RouteInitialRequest | None = None,
|
||||||
terrain: Literal["las", "sheet"] | None = None,
|
terrain: Literal["las", "sheet"] | None = None,
|
||||||
|
planned_lines: tuple[list[list[float]], list[list[float]]] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""노선을 갈아 끼운 뒤의 재계산 — 초기 업로드 체인과 같은 로직을 그대로 탄다.
|
"""노선을 갈아 끼운 뒤의 재계산 — 초기 업로드 체인과 같은 로직을 그대로 탄다.
|
||||||
|
|
||||||
`balanced`(추천 갈래 · 제약)가 오면 그 제약으로 균형 종단을 셈해 1차 종단으로 쓴다(PLAN 74-4).
|
`balanced`(추천 갈래 · 제약)가 오면 그 제약으로 균형 종단을 셈해 1차 종단으로 쓴다(PLAN 74-4).
|
||||||
|
`planned_lines` = (옛 · 새 계획 폴리라인) — 계획고 옮김의 바뀐 구간을 화면과 같은 선으로 가름.
|
||||||
"""
|
"""
|
||||||
import aiomysql
|
import aiomysql
|
||||||
|
|
||||||
@@ -433,6 +444,7 @@ async def _recompute(
|
|||||||
points,
|
points,
|
||||||
previous_params,
|
previous_params,
|
||||||
target,
|
target,
|
||||||
|
planned_lines,
|
||||||
)
|
)
|
||||||
failure = saved.get("error")
|
failure = saved.get("error")
|
||||||
marks.append(("재확정 체인", time.perf_counter()))
|
marks.append(("재확정 체인", time.perf_counter()))
|
||||||
@@ -559,6 +571,7 @@ async def replan_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, An
|
|||||||
working_path = planned_route_working_path(project_root)
|
working_path = planned_route_working_path(project_root)
|
||||||
keep = (working_path, _curves_path(working_path), _nodes_path(working_path))
|
keep = (working_path, _curves_path(working_path), _nodes_path(working_path))
|
||||||
previous = {path: path.read_bytes() if path.is_file() else None for path in keep}
|
previous = {path: path.read_bytes() if path.is_file() else None for path in keep}
|
||||||
|
old_line = await asyncio.to_thread(_planned_line, project_root)
|
||||||
summary = await asyncio.to_thread(
|
summary = await asyncio.to_thread(
|
||||||
functools.partial(
|
functools.partial(
|
||||||
_write_planned_polyline,
|
_write_planned_polyline,
|
||||||
@@ -581,12 +594,18 @@ async def replan_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, An
|
|||||||
summary["violations"],
|
summary["violations"],
|
||||||
radius_m,
|
radius_m,
|
||||||
)
|
)
|
||||||
|
# 균형 종단 새로 풂 = 추천 [적용](`as_initial`)만 — 손으로 고친 노선은 계획고 옮김(344-2).
|
||||||
result = await _recompute(
|
result = await _recompute(
|
||||||
project_id,
|
project_id,
|
||||||
project_root,
|
project_root,
|
||||||
stored_path,
|
stored_path,
|
||||||
(request.initial or RouteInitialRequest()) if request.balanced_profile else None,
|
(
|
||||||
|
(request.initial or RouteInitialRequest())
|
||||||
|
if request.balanced_profile and as_initial
|
||||||
|
else None
|
||||||
|
),
|
||||||
request.terrain,
|
request.terrain,
|
||||||
|
(old_line, await asyncio.to_thread(_vertices_of, working_path)),
|
||||||
)
|
)
|
||||||
if "error" in result:
|
if "error" in result:
|
||||||
# 노선을 되돌린다 — 실패했는데 새 노선만 남으면 화면·정본이 어긋난 채 굳는다.
|
# 노선을 되돌린다 — 실패했는데 새 노선만 남으면 화면·정본이 어긋난 채 굳는다.
|
||||||
|
|||||||
@@ -589,7 +589,6 @@ export async function mountRouteEditor(
|
|||||||
terrain: pickedTerrain,
|
terrain: pickedTerrain,
|
||||||
stationSnap: () => stationSnap,
|
stationSnap: () => stationSnap,
|
||||||
initial,
|
initial,
|
||||||
profileShown: async () => initial.withProfile(), // 띠 없음(345-5) — 지금 추천 갈래가 정함
|
|
||||||
onApplied,
|
onApplied,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -50,8 +50,6 @@ export interface RouteApplyParams {
|
|||||||
current: () => unknown;
|
current: () => unknown;
|
||||||
keep: () => void;
|
keep: () => void;
|
||||||
};
|
};
|
||||||
/** 종단 띠 — 응답을 기다려 띠에 보인 계획선(균형 종단 · 지반고)을 그대로 적용한다(PLAN 75-1). */
|
|
||||||
profileShown: () => Promise<boolean>;
|
|
||||||
/** 저장이 끝나면 페이지를 새로고침 없이 다시 세운다. */
|
/** 저장이 끝나면 페이지를 새로고침 없이 다시 세운다. */
|
||||||
onApplied: () => void | Promise<void>;
|
onApplied: () => void | Promise<void>;
|
||||||
}
|
}
|
||||||
@@ -92,8 +90,7 @@ export function bindRouteApply(params: RouteApplyParams): void {
|
|||||||
return {
|
return {
|
||||||
draft: routeDraftOf(nodes, settings()),
|
draft: routeDraftOf(nodes, settings()),
|
||||||
initial: params.initial.current(),
|
initial: params.initial.current(),
|
||||||
// 띠 응답 전이면 기다린다 — 서버는 같은 노선 · 같은 셈으로 띠 계획선을 다시 낸다.
|
// 손으로 고친 노선 = 계획고 옮김만(344-2 · 349-0v) — 균형 종단은 추천 [적용](`saveRecommendedRoute`)만.
|
||||||
balanced_profile: await params.profileShown(),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
settings,
|
settings,
|
||||||
|
|||||||
@@ -122,7 +122,6 @@ const apply = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Ap
|
|||||||
terrain: () => "sheet",
|
terrain: () => "sheet",
|
||||||
stationSnap: () => true,
|
stationSnap: () => true,
|
||||||
initial: { current: () => ({ mode: "follow" }), keep: () => (kept += 1) },
|
initial: { current: () => ({ mode: "follow" }), keep: () => (kept += 1) },
|
||||||
profileShown: async () => true,
|
|
||||||
onApplied: () => (rebuilt += 1),
|
onApplied: () => (rebuilt += 1),
|
||||||
});
|
});
|
||||||
out.unchanged = await apply.pendingRouteSave("p1");
|
out.unchanged = await apply.pendingRouteSave("p1");
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/* 349-0v 바뀐 구간 거울 — 화면 planline 단계 차례 그대로: 새 노선 = `buildEditedPolyline`(노드 초안) ·
|
||||||
|
* 옛 노선 = 저장 계획 폴리라인 · `changedSpan` → `carryProfileEdits`. 입력 argv[2] JSON · 출력 JSON.
|
||||||
|
*/
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const ts = require(path.join(__dirname, "..", "..", "config", "node_modules", "typescript"));
|
||||||
|
|
||||||
|
const ROOT = path.join(__dirname, "..", "..");
|
||||||
|
|
||||||
|
function loadTs(file, stubs = {}) {
|
||||||
|
const js = ts.transpileModule(fs.readFileSync(path.join(ROOT, file), "utf8"), {
|
||||||
|
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 },
|
||||||
|
}).outputText;
|
||||||
|
const box = { exports: {} };
|
||||||
|
const localRequire = (request) => {
|
||||||
|
if (stubs[request]) return stubs[request];
|
||||||
|
throw new Error(`stub 없음: ${request} (${file})`);
|
||||||
|
};
|
||||||
|
new Function("exports", "module", "require", js)(box.exports, box, localRequire);
|
||||||
|
return box.exports;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curve = loadTs("B05_Profile/logic/B05_Profile_UI_RouteEdit_Curve.ts");
|
||||||
|
const alignment = loadTs("B05_Profile/logic/B05_Profile_UI_Profile_Alignment.ts");
|
||||||
|
const carry = loadTs("B05_Profile/logic/B05_Profile_Grade_Carry.ts", {
|
||||||
|
"./B05_Profile_UI_Profile_Alignment": alignment,
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||||
|
const xy = (line) => line.map(([x, y]) => ({ x, y }));
|
||||||
|
const built = curve.buildEditedPolyline(
|
||||||
|
input.nodes,
|
||||||
|
input.curveOn,
|
||||||
|
input.curveRadius,
|
||||||
|
input.minRadiusM,
|
||||||
|
);
|
||||||
|
const span = carry.changedSpan(xy(input.old_line), xy(built.vertices));
|
||||||
|
process.stdout.write(
|
||||||
|
JSON.stringify({
|
||||||
|
new_line: built.vertices,
|
||||||
|
span,
|
||||||
|
result: span
|
||||||
|
? carry.carryProfileEdits(input.base_pvi, input.edits, span, input.ground_pvi)
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
@@ -455,6 +455,19 @@ def test_추천_종단_반영이면_균형_종단(chain_world, monkeypatch):
|
|||||||
assert "balanced" in chain_world.order and "profile" not in chain_world.order
|
assert "balanced" in chain_world.order and "profile" not in chain_world.order
|
||||||
|
|
||||||
|
|
||||||
|
def test_계획고_옮김은_계획_폴리라인으로_가름(chain_world, monkeypatch):
|
||||||
|
seen: list = []
|
||||||
|
|
||||||
|
async def carry(root, relative, route_id, alignment, old_xy, new_xy):
|
||||||
|
seen.append((old_xy, new_xy))
|
||||||
|
|
||||||
|
monkeypatch.setattr(save, "_carry_profile", carry)
|
||||||
|
_run(chain_world, planned_lines=([[0, 0], [1, 0]], [[0, 0], [2, 0]]))
|
||||||
|
assert seen == [([[0, 0], [1, 0]], [[0, 0], [2, 0]])] # 화면 planline 과 같은 선(349-0v)
|
||||||
|
_run(chain_world)
|
||||||
|
assert seen[-1][1] == [list(p) for p in NEW] # 없으면 풀린 노선 geojson
|
||||||
|
|
||||||
|
|
||||||
# ── 초기값 규칙 ───────────────────────────────────────────────────────────────
|
# ── 초기값 규칙 ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
@@ -492,6 +505,22 @@ def test_추천_적용은_초기값_다시(api_world):
|
|||||||
assert result["initial_refreshed"] is True and len(api_world) == 1
|
assert result["initial_refreshed"] is True and len(api_world) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_균형_종단은_추천_적용만_손_저장은_계획고_옮김(api_world, monkeypatch):
|
||||||
|
calls: list = []
|
||||||
|
|
||||||
|
async def recompute(project_id, root, stored, balanced=None, terrain=None, lines=None):
|
||||||
|
calls.append((balanced, lines))
|
||||||
|
return {"route_id": 9, "total_length_m": 10.0}
|
||||||
|
|
||||||
|
monkeypatch.setattr(replan, "_recompute", recompute)
|
||||||
|
monkeypatch.setattr(replan, "_planned_line", lambda root: [[0.0, 0.0], [4.0, 4.0]])
|
||||||
|
monkeypatch.setattr(replan, "_vertices_of", lambda path: [[0.0, 0.0], [5.0, 5.0]])
|
||||||
|
asyncio.run(replan.replan_route(uuid4(), _body(balanced_profile=True))) # 손으로 고친 노선
|
||||||
|
asyncio.run(replan.replan_route(uuid4(), _body(balanced_profile=True, as_initial=True)))
|
||||||
|
assert calls[0] == (None, ([[0.0, 0.0], [4.0, 4.0]], [[0.0, 0.0], [5.0, 5.0]]))
|
||||||
|
assert calls[1][0] is not None # 추천 [적용] = 균형 종단 그대로
|
||||||
|
|
||||||
|
|
||||||
def test_초안_칸이_어긋나면_400(api_world):
|
def test_초안_칸이_어긋나면_400(api_world):
|
||||||
body = save.RouteSaveRequest(draft={"nodes": [[0, 0], [1, 1]], "curveOn": [True]})
|
body = save.RouteSaveRequest(draft={"nodes": [[0, 0], [1, 1]], "curveOn": [True]})
|
||||||
response = asyncio.run(replan.replan_route(uuid4(), body))
|
response = asyncio.run(replan.replan_route(uuid4(), body))
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ def test_초안은_계약_RouteDraft_꼴(out):
|
|||||||
)
|
)
|
||||||
assert draft["curveOn"] == [True, False, True] and draft["curveRadius"] == [None, 25, None]
|
assert draft["curveOn"] == [True, False, True] and draft["curveRadius"] == [None, 25, None]
|
||||||
assert draft["stationSnap"] is True and draft["terrain"] == "sheet"
|
assert draft["stationSnap"] is True and draft["terrain"] == "sheet"
|
||||||
assert out["body"]["initial"] == {"mode": "follow"} and out["body"]["balanced_profile"] is True
|
assert out["body"]["initial"] == {"mode": "follow"}
|
||||||
|
assert "balanced_profile" not in out["body"] # 손으로 고친 노선 = 계획고 옮김만(349-0v)
|
||||||
assert "as_initial" not in out["body"] # 손으로 고친 노선 = 초기값 그대로
|
assert "as_initial" not in out["body"] # 손으로 고친 노선 = 초기값 그대로
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""349-0v 손으로 고친 노선 [저장] — 미리보기 계획고 = 저장 뒤 계획고(바뀐 구간 거울).
|
||||||
|
|
||||||
|
화면 planline 단계: 옛 = 저장 계획 폴리라인(`read_route_plan` planned) · 새 = `buildEditedPolyline`(노드
|
||||||
|
초안) → `changedSpan` → `carryProfileEdits`. 서버 노선 저장: 옛 = 같은 계획 폴리라인 CSV · 새 =
|
||||||
|
`_write_planned_polyline` 이 쓴 CSV → `changed_span` → `carry_profile_edits`. 두 길이 같은 구간 · 같은
|
||||||
|
계획고를 내야 함(풀린 노선 geojson 으로 가르면 점 간격이 달라 앞머리가 일찍 갈렸음).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
HERE = Path(__file__).resolve().parent
|
||||||
|
ROOT = HERE.parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from B05_Profile import B05_Profile_Router_Replan as replan # noqa: E402
|
||||||
|
from B05_Profile.B05_Profile_Engine_Grade_Carry import ( # noqa: E402
|
||||||
|
carry_profile_edits,
|
||||||
|
changed_span,
|
||||||
|
)
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
|
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||||||
|
reason="프론트엔드 의존성 미설치",
|
||||||
|
)
|
||||||
|
|
||||||
|
RADIUS_M = 30.0
|
||||||
|
OLD_NODES = [(0.0, 0.0), (180.0, 20.0), (320.0, 140.0), (480.0, 130.0), (620.0, 260.0)]
|
||||||
|
OLD_NODES += [(800.0, 250.0), (950.0, 380.0)]
|
||||||
|
MOVED = 4 # 가운데 뒤쪽 꺾임점 하나를 끎(검증_sub2c 끌기와 같은 꼴)
|
||||||
|
NEW_NODES = [(x + 25.0, y - 30.0) if i == MOVED else (x, y) for i, (x, y) in enumerate(OLD_NODES)]
|
||||||
|
CURVE_ON = [True] * len(OLD_NODES)
|
||||||
|
RADII: list[float | None] = [None] * len(OLD_NODES)
|
||||||
|
|
||||||
|
|
||||||
|
def _csv_line(path: Path, nodes) -> list[list[float]]:
|
||||||
|
replan._write_planned_polyline(
|
||||||
|
path, list(nodes), RADIUS_M, simplify=False, curve_flags=CURVE_ON, radii=RADII
|
||||||
|
)
|
||||||
|
return replan._vertices_of(path)
|
||||||
|
|
||||||
|
|
||||||
|
def _length(line) -> float:
|
||||||
|
return sum(math.dist(a, b) for a, b in zip(line, line[1:]))
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def world(tmp_path_factory):
|
||||||
|
tmp = tmp_path_factory.mktemp("span")
|
||||||
|
old_line = _csv_line(tmp / "old.csv", OLD_NODES) # 저장 노선(화면 · 서버 같은 파일)
|
||||||
|
new_line = _csv_line(tmp / "new.csv", NEW_NODES) # 서버가 [저장] 때 쓰는 새 계획 폴리라인
|
||||||
|
total_old, total_new = _length(old_line), _length(new_line)
|
||||||
|
base_pvi = [
|
||||||
|
{"chainage_m": round(s, 6), "elevation_m": 100.0 + 0.04 * s}
|
||||||
|
for s in [0.0, 150.0, 300.0, 450.0, 600.0, 750.0, 900.0, total_old]
|
||||||
|
]
|
||||||
|
ground_pvi = [
|
||||||
|
{"chainage_m": round(s, 6), "elevation_m": 101.0 + 0.03 * s}
|
||||||
|
for s in [x * 40.0 for x in range(int(total_new // 40) + 1)] + [total_new]
|
||||||
|
]
|
||||||
|
edits = {
|
||||||
|
"station_offsets": {"150.000": 0.8, "600.000": -0.5, "900.000": 0.4},
|
||||||
|
"curve_radii": {"300.000": 400.0},
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"nodes": [list(p) for p in NEW_NODES],
|
||||||
|
"curveOn": CURVE_ON,
|
||||||
|
"curveRadius": RADII,
|
||||||
|
"minRadiusM": RADIUS_M,
|
||||||
|
"old_line": old_line,
|
||||||
|
"base_pvi": base_pvi,
|
||||||
|
"edits": edits,
|
||||||
|
"ground_pvi": ground_pvi,
|
||||||
|
}
|
||||||
|
source = tmp / "input.json"
|
||||||
|
source.write_text(json.dumps(payload), encoding="utf-8")
|
||||||
|
proc = subprocess.run( # noqa: S603 — 고정 실행 파일
|
||||||
|
["node", str(HERE / "helper_349_0v_span.cjs"), str(source)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return {**payload, "new_line_py": new_line, "ts": json.loads(proc.stdout)}
|
||||||
|
|
||||||
|
|
||||||
|
def test_새_계획_폴리라인_화면_서버_같음(world):
|
||||||
|
ts_line = world["ts"]["new_line"]
|
||||||
|
assert len(ts_line) == len(world["new_line_py"])
|
||||||
|
assert max(math.dist(a, b) for a, b in zip(ts_line, world["new_line_py"])) < 1e-3
|
||||||
|
|
||||||
|
|
||||||
|
def test_바뀐_구간_화면_서버_같음(world):
|
||||||
|
span = changed_span(
|
||||||
|
[tuple(p) for p in world["old_line"]], [tuple(p) for p in world["new_line_py"]]
|
||||||
|
)
|
||||||
|
ts_span = world["ts"]["span"]
|
||||||
|
assert span is not None and ts_span is not None
|
||||||
|
for key in ("start_m", "old_end_m", "new_end_m", "delta_m"):
|
||||||
|
assert span[key] == pytest.approx(ts_span[key], abs=1e-3), key
|
||||||
|
assert (span["start_fixed"], span["end_fixed"]) == (
|
||||||
|
ts_span["start_fixed"],
|
||||||
|
ts_span["end_fixed"],
|
||||||
|
)
|
||||||
|
# 끈 꺾임점 앞앞 꺾임점의 곡선 끝(마지막 같은 점)부터 — 노선 앞머리부터 다시 풀지 않음
|
||||||
|
assert _length(OLD_NODES[: MOVED - 1]) < span["start_m"] < _length(OLD_NODES[:MOVED])
|
||||||
|
|
||||||
|
|
||||||
|
def test_옮긴_계획고_화면_서버_같음(world):
|
||||||
|
span = changed_span(
|
||||||
|
[tuple(p) for p in world["old_line"]], [tuple(p) for p in world["new_line_py"]]
|
||||||
|
)
|
||||||
|
base, edits = carry_profile_edits(world["base_pvi"], world["edits"], span, world["ground_pvi"])
|
||||||
|
ts = world["ts"]["result"]
|
||||||
|
flat = [v for n in base for v in (n["chainage_m"], n["elevation_m"])]
|
||||||
|
assert flat == pytest.approx(
|
||||||
|
[v for n in ts["basePvi"] for v in (n["chainage_m"], n["elevation_m"])], abs=1e-3
|
||||||
|
) # 서버 새 선 = CSV 0.1 mm 반올림 · 화면 = 원값
|
||||||
|
assert edits == ts["edits"]
|
||||||
|
assert edits["station_offsets"]["150.000"] == 0.8 # 바뀐 구간 앞 사용자 계획고 그대로
|
||||||
@@ -24,8 +24,8 @@ def test_띠_없음_화면_요청_서버_길():
|
|||||||
assert "routeedit__strip" not in _read("B05_Profile_UI_Style_RouteEdit.css")
|
assert "routeedit__strip" not in _read("B05_Profile_UI_Style_RouteEdit.css")
|
||||||
editor = _read("B05_Profile_UI_RouteEdit.ts")
|
editor = _read("B05_Profile_UI_RouteEdit.ts")
|
||||||
assert "ProfileStrip" not in editor and "strip." not in editor
|
assert "ProfileStrip" not in editor and "strip." not in editor
|
||||||
# [노선 반영]의 균형 종단 여부 = 지금 추천 갈래(띠가 보이던 값과 같은 규칙)
|
# 손으로 고친 노선 [저장] = 균형 종단 안 보냄(349-0v) — 추천 [적용]만
|
||||||
assert "profileShown: async () => initial.withProfile()," in editor
|
assert "profileShown" not in editor
|
||||||
assert "profile-preview" not in _read("B05_Profile_Api_Replan.ts")
|
assert "profile-preview" not in _read("B05_Profile_Api_Replan.ts")
|
||||||
assert "is-tick" not in _read("B05_Profile_UI_RouteEdit_Recommend_Profile.ts")
|
assert "is-tick" not in _read("B05_Profile_UI_RouteEdit_Recommend_Profile.ts")
|
||||||
assert not hasattr(initial, "profile_preview") and not hasattr(initial, "ProfilePreviewRequest")
|
assert not hasattr(initial, "profile_preview") and not hasattr(initial, "ProfilePreviewRequest")
|
||||||
|
|||||||
Reference in New Issue
Block a user