fix(B05·B06): 한 자리 측점 둘 숨음 — 측점 0.006 m 안 꼭짓점 · 편집 키를 그 측점으로 모음(213-1)
- 까닭: 자동설계 관 측점 263.206 → 화면 저장 0.01 m 반올림 263.21 로 측점이 다시 섬 · 계획선 꼭짓점 · 편집 키는 옛 글자 그대로 → 새 측점 편집과 0.004 m 사이 두 꼭짓점(−15003% 계단 · 엄상준_검증 No.13+3) - 계획선 공용 함수 짝(파이썬 snap_to_stations ↔ TS snapToStations) — 측점 위 것이 이김 · 옮긴 꼭짓점은 옛 선의 측점 높이(측점 계획고 그대로) · 양 끝 · 측점에서 먼 키는 그대로 · 읽을 때 · [저장] 때 모두 정리 - 192 병합(_follow_moved_stations)도 옛 측점 0.006 m 안 글자를 같은 측점으로 봄 - 시험 test_213_1(합성 · 거울 · 엄상준 사본 재현) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F3mdb91125mPoC6tpsQx4P
This commit is contained in:
@@ -158,6 +158,62 @@ def tangent_elevation(pvi_s: np.ndarray, pvi_z: np.ndarray, targets: np.ndarray)
|
||||
return np.interp(targets, pvi_s, pvi_z)
|
||||
|
||||
|
||||
#: 반올림만 다른 같은 측점(m) — 관 측점 0.001m(자동설계) ↔ 0.01m(화면 저장) 차이.
|
||||
#: 화면 `SAME_STATION_SNAP_M`(B05_Profile_UI_Profile_Alignment.ts) 와 짝.
|
||||
SAME_STATION_SNAP_M = 0.006
|
||||
|
||||
|
||||
def _nearest_station(value: float, station_s: np.ndarray) -> float | None:
|
||||
if not station_s.size:
|
||||
return None
|
||||
near = float(station_s[int(np.argmin(np.abs(station_s - value)))])
|
||||
return near if abs(near - value) < SAME_STATION_SNAP_M else None
|
||||
|
||||
|
||||
def snap_to_stations(
|
||||
base_s: np.ndarray,
|
||||
base_z: np.ndarray,
|
||||
edits: dict[str, dict[str, float]],
|
||||
station_s: np.ndarray,
|
||||
) -> tuple[np.ndarray, np.ndarray, dict[str, dict[str, float]]]:
|
||||
"""측점 0.006m 안의 꼭짓점(base_pvi) · 편집 키를 그 측점으로 모은다(213-1).
|
||||
|
||||
관 측점이 반올림으로 옮겨도(263.206 → 263.21) 꼭짓점 · 편집 키는 옛 글자로 남아, 새 측점
|
||||
편집과 0.004m 사이 두 꼭짓점이 됨(−15000% 계단 · 숨은 측점). 겹치면 측점 위 것이 이김.
|
||||
옮긴 꼭짓점은 옛 선의 측점 높이를 받아 측점 계획고가 안 바뀜. 양 끝 · 측점에서 먼 키는 그대로.
|
||||
"""
|
||||
last = len(base_s) - 1
|
||||
nodes: dict[float, tuple[float, float, float]] = {}
|
||||
for index, (s, z) in enumerate(zip(base_s.tolist(), base_z.tolist())):
|
||||
near = None if index in (0, last) else _nearest_station(s, station_s)
|
||||
at = s if near is None else near
|
||||
gap = 0.0 if near is None else abs(near - s)
|
||||
slot = round(at, _KEY_DECIMALS)
|
||||
if slot not in nodes or gap < nodes[slot][0]:
|
||||
# 옮긴 꼭짓점 높이 = 옛 선의 그 측점 높이 — 측점 계획고가 그대로
|
||||
nodes[slot] = (gap, at, z if near is None else float(np.interp(at, base_s, base_z)))
|
||||
ordered = [nodes[slot] for slot in sorted(nodes)]
|
||||
snapped: dict[str, dict[str, float]] = {}
|
||||
for name, values in edits.items():
|
||||
best: dict[str, tuple[float, float]] = {}
|
||||
for key, value in values.items():
|
||||
try:
|
||||
chainage = float(key)
|
||||
except (TypeError, ValueError):
|
||||
chainage = float("nan")
|
||||
near = _nearest_station(chainage, station_s) if np.isfinite(chainage) else None
|
||||
target = key if near is None else chainage_key(near)
|
||||
gap = 0.0 if near is None else abs(near - chainage)
|
||||
if target not in best or gap < best[target][0]:
|
||||
best[target] = (gap, value)
|
||||
snapped[name] = {key: value for key, (_, value) in best.items()}
|
||||
return (
|
||||
np.array([item[1] for item in ordered], dtype=np.float64),
|
||||
np.array([item[2] for item in ordered], dtype=np.float64),
|
||||
snapped,
|
||||
)
|
||||
|
||||
|
||||
def resolve_pvi(
|
||||
base_s: np.ndarray,
|
||||
base_z: np.ndarray,
|
||||
@@ -407,6 +463,22 @@ def build_alignment(
|
||||
if value is not None
|
||||
}
|
||||
|
||||
station_s = np.array(
|
||||
sorted(
|
||||
float(station["chainage_m"])
|
||||
for station in stations
|
||||
if isinstance(station.get("chainage_m"), (int, float))
|
||||
),
|
||||
dtype=np.float64,
|
||||
)
|
||||
base_s, base_z, snapped = snap_to_stations(
|
||||
base_s,
|
||||
base_z,
|
||||
{"station_offsets": station_offsets, "curve_radii": curve_radii},
|
||||
station_s,
|
||||
)
|
||||
station_offsets, curve_radii = snapped["station_offsets"], snapped["curve_radii"]
|
||||
|
||||
pvi_s, pvi_z, sources = resolve_pvi(base_s, base_z, station_offsets)
|
||||
curves, warnings = build_curves(
|
||||
pvi_s, pvi_z, policy, curve_radii, only_explicit=only_explicit_curves
|
||||
|
||||
@@ -214,27 +214,41 @@ def _follow_moved_stations(data: dict[str, Any], before: list[float]) -> dict[st
|
||||
None,
|
||||
)
|
||||
moves[chainage_key(old)] = None if near is None else chainage_key(near)
|
||||
|
||||
def moved(value: float) -> tuple[bool, str | None]:
|
||||
"""옛 측점 0.006m 안이면 (참, 옮길 키 · 지우면 None) — 옛 글자(263.206)도 같은 측점(213-1).
|
||||
남은 측점 위 값은 그대로."""
|
||||
if chainage_key(value) in after_keys:
|
||||
return False, None
|
||||
for old_key, new_key in moves.items():
|
||||
if abs(float(old_key) - value) < _SAME_STATION_M:
|
||||
return True, new_key
|
||||
return False, None
|
||||
|
||||
edits = stored.get("edits") or {}
|
||||
next_edits: dict[str, dict[str, Any]] = {}
|
||||
changed = False
|
||||
for name in ("station_offsets", "curve_radii"):
|
||||
values = dict(edits.get(name) or {})
|
||||
for old_key, new_key in moves.items():
|
||||
if old_key in values:
|
||||
value = values.pop(old_key)
|
||||
if new_key is not None:
|
||||
values.setdefault(new_key, value)
|
||||
changed = True
|
||||
# 옛 측점 글자 그대로인 키가 먼저 — 반올림 글자보다 이김
|
||||
for key in sorted(values, key=lambda item: item not in moves):
|
||||
hit, new_key = moved(float(key))
|
||||
if not hit:
|
||||
continue
|
||||
value = values.pop(key)
|
||||
if new_key is not None:
|
||||
values.setdefault(new_key, value)
|
||||
changed = True
|
||||
next_edits[name] = values
|
||||
base: list[dict[str, Any]] = []
|
||||
for node in stored["base_pvi"]:
|
||||
key = chainage_key(node["chainage_m"])
|
||||
if key not in moves:
|
||||
hit, new_key = moved(float(node["chainage_m"]))
|
||||
if not hit:
|
||||
base.append(node)
|
||||
continue
|
||||
changed = True
|
||||
if moves[key] is not None:
|
||||
base.append({**node, "chainage_m": float(moves[key])})
|
||||
if new_key is not None:
|
||||
base.append({**node, "chainage_m": float(new_key)})
|
||||
if not changed:
|
||||
return None
|
||||
stored["base_pvi"] = base
|
||||
|
||||
@@ -182,6 +182,65 @@ export function toAlignmentBase(alignment: ProfileAlignment): AlignmentBase {
|
||||
};
|
||||
}
|
||||
|
||||
/** 반올림만 다른 같은 측점(m) — 서버 `SAME_STATION_SNAP_M`(B05_Profile_Engine_Grade_Alignment.py) 와 짝. */
|
||||
const SAME_STATION_SNAP_M = 0.006;
|
||||
|
||||
function nearestStation(value: number, stationS: number[]): number | null {
|
||||
let near: number | null = null;
|
||||
stationS.forEach((station) => {
|
||||
if (near === null || Math.abs(station - value) < Math.abs(near - value)) near = station;
|
||||
});
|
||||
return near !== null && Math.abs(near - value) < SAME_STATION_SNAP_M ? near : null;
|
||||
}
|
||||
|
||||
function snapKeys(values: Record<string, number>, stationS: number[]): Record<string, number> {
|
||||
const best = new Map<string, { gap: number; value: number }>();
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
const chainage = Number(key);
|
||||
const near = Number.isFinite(chainage) ? nearestStation(chainage, stationS) : null;
|
||||
const target = near === null ? key : chainageKey(near);
|
||||
const gap = near === null ? 0 : Math.abs(near - chainage);
|
||||
const held = best.get(target);
|
||||
if (!held || gap < held.gap) best.set(target, { gap, value });
|
||||
});
|
||||
return Object.fromEntries([...best].map(([key, entry]) => [key, entry.value]));
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점 0.006m 안의 꼭짓점(base_pvi) · 편집 키를 그 측점으로 모은다(213-1) — 서버
|
||||
* `snap_to_stations` 와 같은 규칙. 관 측점이 반올림으로 옮겨도(263.206 → 263.21) 옛 글자로 남아
|
||||
* 0.004m 사이 두 꼭짓점이 되던 흠. 겹치면 측점 위 것이 이김 · 옮긴 꼭짓점은 옛 선의 측점 높이
|
||||
* (측점 계획고 그대로) · 양 끝 · 측점에서 먼 키는 그대로.
|
||||
*/
|
||||
function snapToStations(
|
||||
base: AlignmentBase,
|
||||
edits: AlignmentEdits,
|
||||
): { basePvi: AlignmentNode[]; edits: AlignmentEdits } {
|
||||
const stationS = base.stations.map((station) => station.chainage_m);
|
||||
const baseS = base.basePvi.map((node) => node.chainage_m);
|
||||
const baseZ = base.basePvi.map((node) => node.elevation_m);
|
||||
const last = base.basePvi.length - 1;
|
||||
const nodes = new Map<number, { gap: number; node: AlignmentNode }>();
|
||||
base.basePvi.forEach((node, index) => {
|
||||
const near = index === 0 || index === last ? null : nearestStation(node.chainage_m, stationS);
|
||||
const at = near ?? node.chainage_m;
|
||||
const gap = near === null ? 0 : Math.abs(near - node.chainage_m);
|
||||
const slot = Number(at.toFixed(3));
|
||||
const held = nodes.get(slot);
|
||||
// 옮긴 꼭짓점 높이 = 옛 선의 그 측점 높이 — 측점 계획고가 그대로
|
||||
const elevation = near === null ? node.elevation_m : interpolate(baseS, baseZ, at);
|
||||
if (!held || gap < held.gap)
|
||||
nodes.set(slot, { gap, node: { chainage_m: at, elevation_m: elevation } });
|
||||
});
|
||||
return {
|
||||
basePvi: [...nodes.keys()].sort((a, b) => a - b).map((slot) => nodes.get(slot)!.node),
|
||||
edits: {
|
||||
station_offsets: snapKeys(edits.station_offsets, stationS),
|
||||
curve_radii: snapKeys(edits.curve_radii, stationS),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 자동 변화점에 사용자 편집 측점을 합쳐 최종 변화점 집합을 만든다. */
|
||||
function resolvePvi(
|
||||
base: AlignmentBase,
|
||||
@@ -316,8 +375,10 @@ function normalizeEdits(edits: AlignmentEdits | undefined): AlignmentEdits {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildAlignment(base: AlignmentBase, input: AlignmentEdits): ProfileAlignment {
|
||||
const edits = normalizeEdits(input);
|
||||
export function buildAlignment(raw: AlignmentBase, input: AlignmentEdits): ProfileAlignment {
|
||||
const snapped = snapToStations(raw, normalizeEdits(input));
|
||||
const base: AlignmentBase = { ...raw, basePvi: snapped.basePvi };
|
||||
const edits = snapped.edits;
|
||||
const warnings: string[] = [];
|
||||
const nodes = resolvePvi(base, edits.station_offsets);
|
||||
const pviS = nodes.map((node) => node.chainage_m);
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
"""213-1 — 한 자리에 측점 둘이 숨는 흠(엄상준_검증 d2245e3b · No.13+3).
|
||||
|
||||
까닭: 자동설계 관 측점 263.206(0.001m) → 화면 저장이 관을 0.01m 로 반올림(263.21) → 측점은
|
||||
263.21 로 다시 섰는데 계획선 꼭짓점(base_pvi) · 편집 키는 옛 글자 「263.206」 그대로 → 새 측점
|
||||
편집 「263.210」과 0.004m 사이 두 꼭짓점(−15003% 계단).
|
||||
고침: 계획선 공용 함수(파이썬 `snap_to_stations` ↔ TS `snapToStations`)가 측점 0.006m 안의
|
||||
꼭짓점 · 편집 키를 그 측점으로 모음(측점 위 것이 이김 · 측점 계획고 그대로) · 192 병합도
|
||||
옛 글자를 같은 측점으로 봄. 사용자 storage 는 읽기만 — 사본(tmp_path).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import json
|
||||
import shutil
|
||||
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 ( # noqa: E402
|
||||
AlignmentPolicy,
|
||||
build_alignment,
|
||||
)
|
||||
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
|
||||
|
||||
TSC = PROJECT_ROOT / "config" / "node_modules" / "typescript" / "bin" / "tsc"
|
||||
SOURCE = (
|
||||
PROJECT_ROOT
|
||||
/ "storage/1/3/d2245e3b-0468-4a90-ac5c-50b2adcb56e7/B06_Section/longitudinal/longitudinal.json"
|
||||
)
|
||||
REL = "B06_Section/longitudinal/longitudinal.json"
|
||||
needs_copy = pytest.mark.skipif(not SOURCE.is_file(), reason="엄상준_검증 storage 없음")
|
||||
|
||||
# 합성 — 격자 20m · 관 측점 263.206 → 263.21 반올림 · 끝 꼭짓점 1000.0(끝 측점 999.998)
|
||||
STATIONS = [0.0, 20.0, 40.0, 263.21, 280.0, 300.0, 999.998]
|
||||
BASE = [
|
||||
(0.0, 100.0),
|
||||
(20.0, 99.0),
|
||||
(40.0, 98.5),
|
||||
(263.206, 90.0),
|
||||
(280.0, 89.0),
|
||||
(300.0, 88.0),
|
||||
(1000.0, 60.0),
|
||||
]
|
||||
EDITS = {
|
||||
"station_offsets": {"263.206": 1.5, "263.210": 0.9, "40.000": 0.2, "500.000": 0.3},
|
||||
"curve_radii": {"263.206": 300.0},
|
||||
}
|
||||
|
||||
|
||||
def _synthetic() -> dict:
|
||||
chainage = np.arange(0.0, 1000.0, 1.0)
|
||||
return {
|
||||
"base_s": np.array([s for s, _ in BASE]),
|
||||
"base_z": np.array([z for _, z in BASE]),
|
||||
"chainage": np.r_[chainage, 1000.0],
|
||||
"ground": np.r_[100.0 - chainage * 0.04, 60.0],
|
||||
"stations": [
|
||||
{"station_id": f"st{index}", "chainage_m": value}
|
||||
for index, value in enumerate(STATIONS)
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _python(edits: dict) -> dict:
|
||||
data = _synthetic()
|
||||
return build_alignment(**data, policy=AlignmentPolicy.from_dict({}), edits=copy.deepcopy(edits))
|
||||
|
||||
|
||||
def _close_pairs(alignment: dict, limit: float = 0.006) -> list[tuple[float, float]]:
|
||||
pvi = [node["chainage_m"] for node in alignment["pvi"]]
|
||||
return [(a, b) for a, b in zip(pvi, pvi[1:], strict=False) if b - a < limit]
|
||||
|
||||
|
||||
def test_rounded_station_keys_collapse_onto_station():
|
||||
alignment = _python(EDITS)
|
||||
pvi = {round(node["chainage_m"], 3): node for node in alignment["pvi"]}
|
||||
assert _close_pairs(alignment) == []
|
||||
assert 263.206 not in pvi and 263.21 in pvi
|
||||
# 측점 위 편집(263.210 = 0.9)이 이김 · 반경도 측점으로 옮김 · 측점에서 먼 키(500)는 그대로
|
||||
assert alignment["edits"]["station_offsets"] == {"263.210": 0.9, "40.000": 0.2, "500.000": 0.3}
|
||||
assert alignment["edits"]["curve_radii"] == {"263.210": 300.0}
|
||||
# 양 끝 꼭짓점은 그대로(끝 1000.0 을 측점 999.998 로 당기지 않음)
|
||||
assert [node["chainage_m"] for node in alignment["base_pvi"]][-1] == 1000.0
|
||||
assert max(abs(segment["grade_percent"]) for segment in alignment["segments"]) < 100
|
||||
|
||||
|
||||
def test_snapped_base_keeps_station_elevation():
|
||||
"""편집 없는 측점 — 꼭짓점을 측점으로 옮겨도 그 측점 계획고(탄젠트)는 그대로."""
|
||||
alignment = _python({"station_offsets": {}, "curve_radii": {}})
|
||||
node = next(n for n in alignment["base_pvi"] if abs(n["chainage_m"] - 263.21) < 1e-9)
|
||||
expected = float(np.interp(263.21, [s for s, _ in BASE], [z for _, z in BASE]))
|
||||
assert node["elevation_m"] == pytest.approx(expected, abs=1e-6)
|
||||
|
||||
|
||||
_RUNNER = """
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { buildAlignment } from "./B05_Profile_UI_Profile_Alignment.js";
|
||||
const input = JSON.parse(readFileSync(process.argv[2], "utf8"));
|
||||
const result = buildAlignment(input.base, input.edits);
|
||||
writeFileSync(process.argv[3], JSON.stringify(result));
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ts_alignment(tmp_path_factory):
|
||||
if not TSC.is_file():
|
||||
pytest.skip("프론트엔드 의존성 미설치")
|
||||
tmp = tmp_path_factory.mktemp("align213")
|
||||
out = tmp / "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")
|
||||
data = _synthetic()
|
||||
base = {
|
||||
"policy": AlignmentPolicy.from_dict({}).as_dict(),
|
||||
"onlyExplicitCurves": False,
|
||||
"basePvi": [{"chainage_m": s, "elevation_m": z} for s, z in BASE],
|
||||
"chainage": data["chainage"].tolist(),
|
||||
"ground": data["ground"].tolist(),
|
||||
"stations": data["stations"],
|
||||
}
|
||||
src, dst = tmp / "in.json", tmp / "out.json"
|
||||
src.write_text(json.dumps({"base": base, "edits": EDITS}), encoding="utf-8")
|
||||
subprocess.run( # noqa: S603
|
||||
["node", str(out / "runner.mjs"), str(src), str(dst)], check=True, capture_output=True
|
||||
)
|
||||
return json.loads(dst.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def test_mirror_python_ts(ts_alignment):
|
||||
py = _python(EDITS)
|
||||
ts = ts_alignment
|
||||
assert ts["edits"] == py["edits"]
|
||||
assert len(ts["base_pvi"]) == len(py["base_pvi"])
|
||||
for a, b in zip(ts["base_pvi"], py["base_pvi"], strict=True):
|
||||
assert a["chainage_m"] == pytest.approx(b["chainage_m"], abs=1e-6)
|
||||
assert a["elevation_m"] == pytest.approx(b["elevation_m"], abs=1e-6)
|
||||
assert [n["chainage_m"] for n in ts["pvi"]] == pytest.approx(
|
||||
[n["chainage_m"] for n in py["pvi"]], abs=1e-6
|
||||
)
|
||||
assert [n["elevation_m"] for n in ts["pvi"]] == pytest.approx(
|
||||
[n["elevation_m"] for n in py["pvi"]], abs=1e-6
|
||||
)
|
||||
for a, b in zip(ts["stations"], py["stations"], strict=True):
|
||||
assert a["plan_elevation_m"] == pytest.approx(b["plan_elevation_m"], abs=1e-6)
|
||||
|
||||
|
||||
def _copy(tmp_path: Path) -> Path:
|
||||
target = tmp_path / REL
|
||||
target.parent.mkdir(parents=True)
|
||||
shutil.copyfile(SOURCE, target)
|
||||
return target
|
||||
|
||||
|
||||
@needs_copy
|
||||
def test_d2245e3b_no13_3_spike_gone(tmp_path):
|
||||
data = json.loads(_copy(tmp_path).read_text(encoding="utf-8"))
|
||||
edits = data["profile_alignment"]["edits"]
|
||||
assert {"263.206", "263.210"} <= set(edits["station_offsets"]) # 재현 조건
|
||||
alignment, _ = rebuild_alignment_profile(data, edits)
|
||||
assert _close_pairs(alignment) == []
|
||||
assert "263.206" not in alignment["edits"]["station_offsets"]
|
||||
assert max(abs(segment["grade_percent"]) for segment in alignment["segments"]) < 100
|
||||
|
||||
|
||||
@needs_copy
|
||||
def test_d2245e3b_station_regen_drops_rounded_key(tmp_path):
|
||||
"""[측점 만들기] 로 관 측점 263.21 → 263.31 — 옛 글자 263.206 꼭짓점 · 편집도 함께 지워짐."""
|
||||
path = _copy(tmp_path)
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
irregular = []
|
||||
for station in data["stations"]:
|
||||
if station.get("kind") != "irregular":
|
||||
continue
|
||||
item = copy.deepcopy(station)
|
||||
if abs(item["chainage_m"] - 263.21) < 1e-6:
|
||||
item["chainage_m"] = 263.31
|
||||
irregular.append(item)
|
||||
_merge_irregular_into_longitudinal(tmp_path, REL, irregular)
|
||||
saved = json.loads(path.read_text(encoding="utf-8"))["profile_alignment"]
|
||||
leftovers = [
|
||||
value
|
||||
for value in [n["chainage_m"] for n in saved["base_pvi"]]
|
||||
+ [float(key) for key in saved["edits"]["station_offsets"]]
|
||||
if 263.2 < value < 263.22
|
||||
]
|
||||
assert leftovers == []
|
||||
assert _close_pairs(saved, 0.05) == []
|
||||
Reference in New Issue
Block a user