fix(459 427 세움): 노선 초안 중 옮긴 시설 = 서버 [저장] 차례와 같은 값 — 따라간 자리로 새 계획선 → 옮긴 자리로 427
- follow 가 노선 초안 전 관 · 구조물이 따라가기만 한 자리를 남김(좌표 있으면 좌표 · 없으면 저장 노선 위 누가거리) - planline: ① 따라간 자리 측점으로 carryBase · 바뀐 구간 시설 세움(_carry_profile 짝) ② 사용자가 옮긴 · 놓은 것을 followMovedNodes + seatBornNodes(_follow_moved_stations 짝) · 격자 자리는 측점 남김 - 검증_sub4 화면 실측에서 찾음: 초안 355 m 계획고 = 지반 · [저장] 뒤 서버 = 옮기기 전 계획선 높이(균형 종단) → 1.4 m 갈림 - 시험 test_459 9 개(균형 종단 · 지반 종단 · 마커로 옮김 = 서버 두 차례와 0.00001 m 안) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,11 +27,16 @@ import type {
|
||||
import {
|
||||
followPlusStations,
|
||||
followStructures,
|
||||
placeOnLine,
|
||||
projectPipePoints,
|
||||
type FollowingPoint,
|
||||
type FollowingStructure,
|
||||
} from "../../common_util/common_util_route_follow";
|
||||
import { PLUS_OWNER_PREFIX, SAME_STATION_M } from "../../common_util/common_util_structure_station";
|
||||
import {
|
||||
followMovedNodes,
|
||||
PLUS_OWNER_PREFIX,
|
||||
SAME_STATION_M,
|
||||
} from "../../common_util/common_util_structure_station";
|
||||
import {
|
||||
buildAlignment,
|
||||
emptyEdits,
|
||||
@@ -112,9 +117,7 @@ export function createFollowStage(deps: {
|
||||
}): StageFn<"follow"> {
|
||||
let ref: GradeRef | null = null;
|
||||
let carried: unknown = null; // 마지막으로 낸 편집 — 입력이 이것이면 기준 그대로
|
||||
let moved = new Set<string>(); // 노선 초안 동안 사용자가 옮긴 · 놓은 시설 id(459 · 427 세움)
|
||||
return (ctx) => {
|
||||
if (!ctx.inputs.route) moved = new Set();
|
||||
if (!ctx.inputs.route && deps.saved) return deps.saved();
|
||||
const before = ctx.before.route?.line ?? deps.savedLine();
|
||||
const now = ctx.out.route;
|
||||
@@ -122,7 +125,6 @@ export function createFollowStage(deps: {
|
||||
const line = xy(now.line);
|
||||
const patch: FollowPatch = {};
|
||||
const { pipes, structures, alignment, plusStations } = ctx.inputs;
|
||||
if (ctx.before.follow) userMoved(ctx.inputs, ctx.before.follow).forEach((id) => moved.add(id));
|
||||
if (Array.isArray(pipes) && pipes.length)
|
||||
patch.pipes = projectPipePoints(pipes as FollowingPoint[], line);
|
||||
if (Array.isArray(structures) && structures.length)
|
||||
@@ -135,7 +137,10 @@ export function createFollowStage(deps: {
|
||||
// 추가 측점도 땅 위 자리 따라감(459 · 서버 `follow_plus_stations` 짝) — 가게는 부르는 쪽이 씀
|
||||
if (plusStations?.length)
|
||||
patch.plusStations = followPlusStations(plusStations, xy(before), line);
|
||||
movedIds.set(patch, new Set(moved));
|
||||
// 노선 초안 전 관 · 구조물이 따라가기만 한 자리 — planline 이 사용자가 옮긴 것을 가름(459 · 427)
|
||||
const origin = deps.saved?.();
|
||||
const home = deps.savedLine();
|
||||
if (origin && home) followedAt.set(patch, followedSpots(origin, xy(home), line));
|
||||
const planline = ctx.before.planline;
|
||||
if (alignment !== carried && JSON.stringify(alignment) !== JSON.stringify(carried))
|
||||
ref =
|
||||
@@ -157,41 +162,81 @@ export function createFollowStage(deps: {
|
||||
/** 같은 꼭짓점 자리(m) — `seatBornNodes` · 파이썬 `_SAME_STATION_M` 과 같은 0.006. */
|
||||
const SEAT_SAME_M = 0.006;
|
||||
|
||||
/** follow 출력마다 그때까지 사용자가 옮긴 시설 id — planline 이 427 세움에 씀(459). */
|
||||
const movedIds = new WeakMap<object, ReadonlySet<string>>();
|
||||
/** follow 출력마다 따라가기만 한 자리(id → 자리 누가거리들) — planline 이 427 세움에 씀(459). */
|
||||
const followedAt = new WeakMap<object, ReadonlyMap<string, number[]>>();
|
||||
|
||||
type Facility = { id?: string | null; structure_id?: string | null; chainage_m?: number | null };
|
||||
type Facility = FollowingStructure & { id?: string | null; structure_id?: string | null };
|
||||
|
||||
/** 입력 관 · 구조물 중 앞 follow 출력과 자리가 다른(사용자가 옮김) · 새로 놓은 것의 id. */
|
||||
function userMoved(
|
||||
inputs: Pick<PipelineInputs, "pipes" | "structures">,
|
||||
followed: FollowPatch,
|
||||
): string[] {
|
||||
const out: string[] = [];
|
||||
for (const key of ["pipes", "structures"] as const) {
|
||||
const now = inputs[key];
|
||||
const was = followed[key];
|
||||
if (!Array.isArray(now) || !Array.isArray(was)) continue;
|
||||
const idOf = (item: Facility) => (key === "pipes" ? item.id : item.structure_id) ?? null;
|
||||
const at = new Map((was as Facility[]).map((item) => [idOf(item), item.chainage_m ?? null]));
|
||||
for (const item of now as Facility[]) {
|
||||
const id = idOf(item);
|
||||
const old = at.get(id);
|
||||
if (id && (old == null || Math.abs((item.chainage_m ?? 0) - old) > SAME_STATION_M))
|
||||
out.push(id);
|
||||
}
|
||||
}
|
||||
/** 땅 위 자리 — 좌표가 있으면 그대로(따라가기만 한 시설 = 정본 좌표 · 초안 둔 채 열기도) · 없으면 누가거리의
|
||||
* `home` 위 자리. */
|
||||
const grounded = (items: Facility[], home: ReadonlyArray<XY>): Facility[] =>
|
||||
items.map((item) =>
|
||||
item.x != null && item.y != null
|
||||
? item
|
||||
: placeOnLine([item], home, [], SECTION_STATION_INTERVAL_M)[0],
|
||||
);
|
||||
|
||||
/** 노선 초안 전 관 · 구조물(`origin` · 저장 노선 `home` 위)을 새 선에 따라 옮긴 자리 — id → 기준 · 구간 끝. */
|
||||
function followedSpots(
|
||||
origin: FollowPatch,
|
||||
home: ReadonlyArray<XY>,
|
||||
line: ReadonlyArray<XY>,
|
||||
): Map<string, number[]> {
|
||||
const spots = new Map<string, number[]>();
|
||||
const put = (id: string | null | undefined, item: FollowingStructure): void => {
|
||||
if (!id) return;
|
||||
const at = [item.chainage_m, item.start_m, item.end_m].filter(
|
||||
(value): value is number => value != null,
|
||||
);
|
||||
spots.set(id, [...new Set(at)]);
|
||||
};
|
||||
if (Array.isArray(origin.pipes))
|
||||
projectPipePoints(grounded(origin.pipes as Facility[], home) as FollowingPoint[], line).forEach(
|
||||
(pipe) => put((pipe as Facility).id, pipe),
|
||||
);
|
||||
if (Array.isArray(origin.structures))
|
||||
followStructures(
|
||||
grounded(origin.structures as Facility[], home),
|
||||
line,
|
||||
undefined,
|
||||
SECTION_STATION_INTERVAL_M,
|
||||
).forEach(({ item }) => put(item.structure_id, item));
|
||||
return spots;
|
||||
}
|
||||
|
||||
/** 사용자가 옮긴 · 새로 놓은 시설 측점 — 따라가기만 한 자리(`spots`)와 다른 것 · 옛 자리(`from` · 새로 놓음 =
|
||||
* null) → 지금 자리(`to`). 한 시설의 여러 측점(구간 끝)은 차례대로 짝. 추가 측점은 뺌. */
|
||||
export function userMoves(
|
||||
stations: ReadonlyArray<Pick<PipelineStation, "chainageM" | "kind" | "id">>,
|
||||
spots: ReadonlyMap<string, ReadonlyArray<number>>,
|
||||
): Array<{ from: number | null; to: number }> {
|
||||
const byId = new Map<string, number[]>();
|
||||
for (const station of stations)
|
||||
if (
|
||||
(station.kind === "pipe" || station.kind === "structure") &&
|
||||
station.id &&
|
||||
!station.id.startsWith(PLUS_OWNER_PREFIX)
|
||||
)
|
||||
byId.set(station.id, [...(byId.get(station.id) ?? []), station.chainageM]);
|
||||
const out: Array<{ from: number | null; to: number }> = [];
|
||||
byId.forEach((now, id) => {
|
||||
const was = [...(spots.get(id) ?? [])].sort((a, b) => a - b);
|
||||
now
|
||||
.sort((a, b) => a - b)
|
||||
.forEach((to, index) => {
|
||||
const from = was.length === now.length ? was[index] : null;
|
||||
if (from === null || Math.abs(from - to) > SAME_STATION_M) out.push({ from, to });
|
||||
});
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 노선 바뀜으로 옮긴 시설 측점(459) — 관 · 구조물 측점 중 옛 선과 같은 자리가 아닌 것(`lineMatcher` 짝
|
||||
* 없음) · 사용자가 옮긴 시설(`moved` id). 추가 측점(`plus-`)은 뺌. 파이썬 `route_moved_stations` 짝(그쪽은
|
||||
* 사용자가 옮긴 것을 관 · 구조물 저장이 세움). */
|
||||
* 없음) · 추가 측점(`plus-`)은 뺌. 파이썬 `route_moved_stations` 짝. */
|
||||
export function routeMovedStations(
|
||||
stations: ReadonlyArray<Pick<PipelineStation, "chainageM" | "kind" | "id">>,
|
||||
oldXy: ReadonlyArray<XY>,
|
||||
newXy: ReadonlyArray<XY>,
|
||||
moved: ReadonlySet<string> = new Set(),
|
||||
): number[] {
|
||||
const match = lineMatcher(oldXy, newXy);
|
||||
return stations
|
||||
@@ -199,7 +244,7 @@ export function routeMovedStations(
|
||||
(station) =>
|
||||
(station.kind === "pipe" || station.kind === "structure") &&
|
||||
!station.id?.startsWith(PLUS_OWNER_PREFIX) &&
|
||||
((station.id !== null && moved.has(station.id)) || match(station.chainageM) === null),
|
||||
match(station.chainageM) === null,
|
||||
)
|
||||
.map((station) => station.chainageM)
|
||||
.sort((a, b) => a - b);
|
||||
@@ -252,39 +297,58 @@ export function createPlanlineStage(deps: PlanlineDeps): StageFn<"planline"> {
|
||||
const profile = groundProfile(samples);
|
||||
if (!profile) return saved; // 못 서는 땅 — 저장 기준 그대로(서버가 [저장] 때 다시 셈)
|
||||
// 바뀐 자리 새 꼭짓점 = 서버 새 지반 첫 계획선(기본 L = config 짝 · 초기값 직선화 · 438)
|
||||
const groundPvi =
|
||||
const pviAt = (stations: number[]) =>
|
||||
groundFollowingBasePvi({
|
||||
samples,
|
||||
stations: ground.stations.map((station) => station.chainageM),
|
||||
stations,
|
||||
policy: { ...saved.policy, default_curve_length_m: DEFAULT_CURVE_LENGTH_M },
|
||||
}) ?? [];
|
||||
const savedLine = deps.savedLine();
|
||||
const span = savedLine ? changedSpan(xy(savedLine), xy(route.line)) : null;
|
||||
const savedPvi = deps.storedPvi?.() ?? saved.basePvi;
|
||||
const baseOf = (basePvi: AlignmentBase["basePvi"]) => ({
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
policy: saved.policy,
|
||||
basePvi,
|
||||
});
|
||||
const edited = Object.keys(ctx.inputs.alignment?.station_offsets ?? {});
|
||||
// 427 세움(459) — 서버 [저장] 차례 짝: ① 노선 저장 = 관 · 구조물이 따라가기만 한 자리 측점으로 새 계획선 ·
|
||||
// 바뀐 구간에 든 시설 측점 세움(`_carry_profile`) ② 관 · 구조물 저장 = 사용자가 옮긴 자리로(`_follow_moved_stations`)
|
||||
const spots = ctx.out.follow ? followedAt.get(ctx.out.follow) : undefined;
|
||||
const moves = spots ? userMoves(ground.stations, spots) : [];
|
||||
// 일반측점 격자 자리는 시설이 떠나도 · 와도 측점이 남음(서버 `grid` 측점 · 272)
|
||||
const interval = saved.policy.station_interval_m || SECTION_STATION_INTERVAL_M;
|
||||
const onGrid = (value: number) =>
|
||||
Math.abs(value - Math.round(value / interval) * interval) < SAME_STATION_M;
|
||||
const gone = moves.flatMap((move) =>
|
||||
move.from === null || onGrid(move.from) ? [] : [move.from],
|
||||
);
|
||||
const born = moves.flatMap((move) => (onGrid(move.to) ? [] : [move.to]));
|
||||
const homeStations = [
|
||||
...ground.stations.filter((station) => !born.includes(station.chainageM)),
|
||||
...gone.map((chainageM) => ({ chainageM, kind: "structure" as const, id: null })),
|
||||
];
|
||||
const carried = span
|
||||
? carryBase(xy(savedLine!), xy(route.line), savedPvi, groundPvi)
|
||||
? carryBase(
|
||||
xy(savedLine!),
|
||||
xy(route.line),
|
||||
savedPvi,
|
||||
pviAt(homeStations.map((station) => station.chainageM)),
|
||||
)
|
||||
: savedPvi;
|
||||
// 옮긴 시설 측점에 427 꼭짓점 다시 세움(459 · 서버 `_carry_profile` 짝 · 사용자가 옮긴 것은 즉시)
|
||||
const followed = ctx.out.follow;
|
||||
const moved = new Set(followed ? movedIds.get(followed) : []);
|
||||
if (followed) userMoved(ctx.inputs, followed).forEach((id) => moved.add(id));
|
||||
const born = savedLine
|
||||
? routeMovedStations(ground.stations, xy(savedLine), xy(route.line), moved)
|
||||
: [];
|
||||
const held = Object.keys(ctx.inputs.alignment?.station_offsets ?? {}).map(Number);
|
||||
const home = span
|
||||
? seatRouteMoved(
|
||||
baseOf(carried),
|
||||
routeMovedStations(homeStations, xy(savedLine!), xy(route.line)),
|
||||
edited.map(Number),
|
||||
)
|
||||
: carried;
|
||||
const step = gone.length || born.length ? followMovedNodes(home, gone, born, edited) : null;
|
||||
return {
|
||||
policy: saved.policy, // 옛 기본 L 이어받음(서버 `carry_policy` 짝)
|
||||
onlyExplicitCurves: saved.onlyExplicitCurves,
|
||||
basePvi: seatRouteMoved(
|
||||
{
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
policy: saved.policy,
|
||||
basePvi: carried,
|
||||
},
|
||||
born,
|
||||
held,
|
||||
),
|
||||
basePvi: step ? seatBornNodes(baseOf(home), step.nodes, step.seat, step.held) : home,
|
||||
chainage: profile.chainage,
|
||||
ground: profile.ground,
|
||||
stations: ground.stations.map((station) => ({
|
||||
|
||||
@@ -38,12 +38,9 @@ const cases = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
||||
const out = cases.map((item) => {
|
||||
switch (item.op) {
|
||||
case "moved":
|
||||
return grade.routeMovedStations(
|
||||
item.stations,
|
||||
xy(item.oldXy),
|
||||
xy(item.newXy),
|
||||
new Set(item.moved ?? []),
|
||||
);
|
||||
return grade.routeMovedStations(item.stations, xy(item.oldXy), xy(item.newXy));
|
||||
case "userMoves":
|
||||
return grade.userMoves(item.stations, new Map(Object.entries(item.spots)));
|
||||
case "planline": {
|
||||
// 계산 묶음 planline 단계 그대로 — 저장 기준(storedPvi) · 새 지반 · 측점 · follow 출력
|
||||
const stage = grade.createPlanlineStage({
|
||||
@@ -62,15 +59,33 @@ const out = cases.map((item) => {
|
||||
follow: followed,
|
||||
},
|
||||
});
|
||||
const result = stage(ctxOf(item.stations, item.followed ?? null));
|
||||
// follow 단계가 따라가기만 한 자리를 남김(노선 초안 전 관 = `origin`)
|
||||
const followed = item.origin
|
||||
? grade.createFollowStage({ savedLine: () => item.oldXy, saved: () => item.origin })({
|
||||
inputs: { route: {}, alignment: null, pipes: item.pipes ?? null, structures: null },
|
||||
before: {},
|
||||
out: { route: { line: item.newXy } },
|
||||
})
|
||||
: null;
|
||||
const result = stage(ctxOf(item.stations, followed));
|
||||
// 시설 없는 같은 측점 = 옮겨 붙인 꼭짓점(427 세움 전) — 파이썬 `seat_route_moved` 입력
|
||||
const plain = stage(
|
||||
ctxOf(
|
||||
item.stations.map((s) => ({ ...s, kind: "grid", id: null })),
|
||||
(item.homeStations ?? item.stations).map((s) => ({ ...s, kind: "grid", id: null })),
|
||||
null,
|
||||
),
|
||||
);
|
||||
return { basePvi: result.basePvi, carried: plain.basePvi };
|
||||
// 노선 저장 뒤(바뀐 구간 세움까지) — 사용자 옮김 전 기준 · 파이썬 관 저장 차례 입력
|
||||
const home = grade.seatRouteMoved(
|
||||
{
|
||||
chainage: item.chainage,
|
||||
ground: item.ground,
|
||||
policy: item.base.policy,
|
||||
basePvi: plain.basePvi,
|
||||
},
|
||||
item.homeBorn ?? [],
|
||||
);
|
||||
return { basePvi: result.basePvi, carried: plain.basePvi, home };
|
||||
}
|
||||
case "carry":
|
||||
return carry.carryBase(xy(item.oldXy), xy(item.newXy), item.oldBase, item.groundPvi);
|
||||
|
||||
@@ -35,6 +35,7 @@ from B05_Profile.B05_Profile_Engine_Grade_Alignment import ( # noqa: E402
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import ( # noqa: E402
|
||||
design_ground_following_profile,
|
||||
rebuild_alignment_profile,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Plus_Stations import ( # noqa: E402
|
||||
follow_plus_stations,
|
||||
@@ -45,6 +46,7 @@ from B05_Profile.B05_Profile_Route_Save_Parts import ( # noqa: E402
|
||||
route_moved_stations,
|
||||
seat_route_moved,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Router_Confirm import _seat_born_nodes # noqa: E402
|
||||
from common_util.common_util_drainage_pipes import PipePoint # noqa: E402
|
||||
|
||||
OLD_XY = [[0.0, 0.0], [200.0, 0.0], [400.0, 0.0]]
|
||||
@@ -63,13 +65,17 @@ def _new_ground(s: float) -> float:
|
||||
return _old_ground(200.0) + 0.05 * (s - 200.0) + 0.8 * math.cos((s - 200.0) / 9.0)
|
||||
|
||||
|
||||
def _old_alignment() -> dict:
|
||||
PIPE = 127.0 # 같은 자리 관(격자 아님)
|
||||
MOVED = 133.0 # 노선 초안 중 마커로 옮긴 자리
|
||||
|
||||
|
||||
def _old_alignment(balanced: bool = False) -> dict:
|
||||
samples = [
|
||||
{"chainage_m": float(s), "elevation_m": round(_old_ground(s), 6), "valid": True}
|
||||
for s in range(0, 401)
|
||||
]
|
||||
stations = [{"chainage_m": float(s), "kind": "regular"} for s in range(0, 401, 20)]
|
||||
stations.append({"chainage_m": 120.0, "kind": "irregular", "structure": "파형강관 D800"})
|
||||
stations.append({"chainage_m": PIPE, "kind": "irregular", "structure": "파형강관 D800"})
|
||||
stations.sort(key=lambda row: row["chainage_m"])
|
||||
options = GradeDesignOptions(
|
||||
max_grade_pct=14.0,
|
||||
@@ -80,18 +86,22 @@ def _old_alignment() -> dict:
|
||||
vertical_curve_skip_delta_pct=0.5,
|
||||
design_speed_kph=20,
|
||||
)
|
||||
alignment, _ = design_ground_following_profile(
|
||||
{"samples": samples, "stations": stations}, options, station_interval_m=20.0
|
||||
)
|
||||
data = {"samples": samples, "stations": stations}
|
||||
alignment, profile = design_ground_following_profile(data, options, station_interval_m=20.0)
|
||||
if balanced: # 계획선이 지반을 안 지남(균형 종단 꼴 · 427 세움 = 옮기기 전 계획선 높이)
|
||||
for node in alignment["base_pvi"]:
|
||||
node["elevation_m"] -= 1.0
|
||||
data["profile_alignment"], data["design_profiles"] = alignment, [profile]
|
||||
alignment, _ = rebuild_alignment_profile(data, {})
|
||||
return alignment
|
||||
|
||||
|
||||
def _stations(pipe_at: float = 120.0) -> list[dict]:
|
||||
def _stations(pipe_at: float = PIPE) -> list[dict]:
|
||||
"""새 노선 측점(TS 꼴) — 격자 · 끝 · 관 둘 · 구조물 하나 · 추가 측점 하나."""
|
||||
rows = [{"chainageM": float(s), "kind": "grid", "id": None} for s in range(0, 421, 20)]
|
||||
rows.append({"chainageM": round(NEW_LEN, 3), "kind": "end", "id": None})
|
||||
rows += [
|
||||
{"chainageM": pipe_at, "kind": "pipe", "id": "p120"},
|
||||
{"chainageM": pipe_at, "kind": "pipe", "id": "p127"},
|
||||
{"chainageM": 251.0, "kind": "structure", "id": "s251"},
|
||||
{"chainageM": 333.0, "kind": "pipe", "id": "p333"},
|
||||
{"chainageM": 77.0, "kind": "structure", "id": "plus-77"},
|
||||
@@ -99,6 +109,29 @@ def _stations(pipe_at: float = 120.0) -> list[dict]:
|
||||
return sorted(rows, key=lambda row: row["chainageM"])
|
||||
|
||||
|
||||
def _origin() -> dict:
|
||||
"""노선 초안 전 관 · 구조물(저장 노선 위) — 새 노선에 따라가면 127 · 251 · 333."""
|
||||
|
||||
def on_new(at: float) -> tuple[float, float]:
|
||||
walked = 0.0
|
||||
for (ax, ay), (bx, by) in zip(NEW_XY, NEW_XY[1:]):
|
||||
span = math.hypot(bx - ax, by - ay)
|
||||
if walked + span >= at:
|
||||
t = (at - walked) / span
|
||||
return ax + (bx - ax) * t, ay + (by - ay) * t
|
||||
walked += span
|
||||
raise ValueError(at)
|
||||
|
||||
def item(at: float, **rest) -> dict:
|
||||
x, y = on_new(at)
|
||||
return {**rest, "chainage_m": float(round(min(max(x, 0.0), 400.0))), "x": x, "y": y}
|
||||
|
||||
return {
|
||||
"pipes": [item(PIPE, id="p127"), item(333.0, id="p333")],
|
||||
"structures": [item(251.0, structure_id="s251", placement="point")],
|
||||
}
|
||||
|
||||
|
||||
def _server_stations(stations: list[dict]) -> list[dict]:
|
||||
"""같은 측점(서버 종단 꼴) — 관 · 구조물 = `structure` 이름 · 추가 측점 = 빈 이름."""
|
||||
names = {"pipe": "파형강관 D800", "structure": "옹벽"}
|
||||
@@ -152,14 +185,16 @@ def _planline_case(alignment: dict, stations: list[dict], **extra) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _seat_py(alignment: dict, carried: list[dict], born: list[float]) -> list[dict]:
|
||||
def _new_alignment(alignment: dict, base: list[dict]) -> dict:
|
||||
samples = [
|
||||
{"chainage_m": float(s), "ground_elevation_m": round(_new_ground(s), 6)}
|
||||
for s in range(0, int(NEW_LEN) + 1)
|
||||
]
|
||||
return seat_route_moved(
|
||||
{"samples": samples, "policy": alignment["policy"], "base_pvi": carried}, born
|
||||
)
|
||||
return {"samples": samples, "policy": alignment["policy"], "base_pvi": base}
|
||||
|
||||
|
||||
def _seat_py(alignment: dict, carried: list[dict], born: list[float]) -> list[dict]:
|
||||
return seat_route_moved(_new_alignment(alignment, carried), born)
|
||||
|
||||
|
||||
def _plan_at(nodes: list[dict], policy: dict, at: float) -> float:
|
||||
@@ -169,61 +204,72 @@ def _plan_at(nodes: list[dict], policy: dict, at: float) -> float:
|
||||
return float(evaluate(s, z, curves, np.array([at]))[0])
|
||||
|
||||
|
||||
def _same_nodes(mine: list[dict], theirs: list[dict]) -> None:
|
||||
def _same_nodes(mine: list[dict], theirs: list[dict], tol: float = 1e-6) -> None:
|
||||
assert [n["chainage_m"] for n in mine] == pytest.approx([n["chainage_m"] for n in theirs])
|
||||
for a, b in zip(mine, theirs, strict=True):
|
||||
assert a["elevation_m"] == pytest.approx(b["elevation_m"], abs=1e-6), a["chainage_m"]
|
||||
assert a["elevation_m"] == pytest.approx(b["elevation_m"], abs=tol), a["chainage_m"]
|
||||
|
||||
|
||||
def test_옮긴_시설_측점_TS_파이썬_같음(tmp_path: Path) -> None:
|
||||
stations = _stations()
|
||||
server = route_moved_stations(_server_stations(stations), OLD_XY, NEW_XY)
|
||||
plain, moved = _ts(
|
||||
stations = _stations(pipe_at=MOVED) + [{"chainageM": 401.0, "kind": "pipe", "id": "new1"}]
|
||||
server = route_moved_stations(_server_stations(_stations()), OLD_XY, NEW_XY)
|
||||
plain, moves = _ts(
|
||||
tmp_path,
|
||||
[
|
||||
{"op": "moved", "stations": stations, "oldXy": OLD_XY, "newXy": NEW_XY},
|
||||
{"op": "moved", "stations": _stations(), "oldXy": OLD_XY, "newXy": NEW_XY},
|
||||
{
|
||||
"op": "moved",
|
||||
"op": "userMoves",
|
||||
"stations": stations,
|
||||
"oldXy": OLD_XY,
|
||||
"newXy": NEW_XY,
|
||||
"moved": ["p120"],
|
||||
"spots": {"p127": [PIPE], "s251": [251.0], "p333": [333.0], "plus-77": [70.0]},
|
||||
},
|
||||
],
|
||||
)
|
||||
assert server == plain == [251.0, 333.0] # 같은 자리 관 120 · 추가 측점 77 은 뺌
|
||||
assert moved == [120.0, 251.0, 333.0] # 사용자가 옮긴 관은 같은 자리여도 세움
|
||||
assert server == plain == [251.0, 333.0] # 같은 자리 관 127 · 추가 측점 77 은 뺌
|
||||
# 사용자가 옮긴 관 = 따라간 자리 → 지금 자리 · 새로 놓은 관 = 옛 자리 없음
|
||||
assert sorted(moves, key=lambda m: m["to"]) == [
|
||||
{"from": PIPE, "to": MOVED},
|
||||
{"from": None, "to": 401.0},
|
||||
]
|
||||
|
||||
|
||||
def test_초안_planline_은_서버와_같은_꼭짓점(tmp_path: Path) -> None:
|
||||
alignment = _old_alignment()
|
||||
@pytest.mark.parametrize("balanced", [False, True])
|
||||
def test_초안_planline_은_서버와_같은_꼭짓점(tmp_path: Path, balanced: bool) -> None:
|
||||
alignment = _old_alignment(balanced)
|
||||
[out] = _ts(tmp_path, [_planline_case(alignment, _stations())])
|
||||
server = _seat_py(alignment, out["carried"], [251.0, 333.0])
|
||||
_same_nodes(out["basePvi"], server)
|
||||
# 옮긴 시설 측점 계획선 = 새 원지반(옛 계획선이 지반 종단) · 같은 자리 관 120 = 옛 높이
|
||||
for at in (251.0, 333.0):
|
||||
assert _plan_at(out["basePvi"], alignment["policy"], at) == pytest.approx(
|
||||
_new_ground(at), abs=1e-4
|
||||
)
|
||||
old = {n["chainage_m"]: n["elevation_m"] for n in out["carried"]}
|
||||
new = {n["chainage_m"]: n["elevation_m"] for n in out["basePvi"]}
|
||||
assert new[120.0] == pytest.approx(old[120.0], abs=1e-9)
|
||||
assert new[PIPE] == pytest.approx(old[PIPE], abs=1e-9) # 같은 자리 관 = 옛 높이
|
||||
if not balanced: # 옮긴 시설 측점 계획선 = 새 원지반(옛 계획선이 지반 종단)
|
||||
for at in (251.0, 333.0):
|
||||
assert _plan_at(out["basePvi"], alignment["policy"], at) == pytest.approx(
|
||||
_new_ground(at), abs=1e-4
|
||||
)
|
||||
|
||||
|
||||
def test_노선_초안_중_마커로_옮긴_관도_즉시_세움(tmp_path: Path) -> None:
|
||||
alignment = _old_alignment()
|
||||
@pytest.mark.parametrize("balanced", [False, True])
|
||||
def test_노선_초안_중_마커로_옮긴_관도_서버_차례와_같은_값(tmp_path: Path, balanced: bool) -> None:
|
||||
"""서버 = 노선 저장(따라간 자리 127 로 새 계획선 · 바뀐 구간 세움) → 관 저장(133 · 427)."""
|
||||
alignment = _old_alignment(balanced)
|
||||
case = _planline_case(
|
||||
alignment,
|
||||
_stations(pipe_at=125.0),
|
||||
pipes=[{"id": "p120", "chainage_m": 125.0}],
|
||||
followed={"pipes": [{"id": "p120", "chainage_m": 120.0}]},
|
||||
_stations(pipe_at=MOVED),
|
||||
homeStations=_stations(),
|
||||
homeBorn=[251.0, 333.0],
|
||||
origin=_origin(),
|
||||
pipes=[{"id": "p127", "chainage_m": MOVED}, {"id": "p333", "chainage_m": 333.0}],
|
||||
)
|
||||
[out] = _ts(tmp_path, [case])
|
||||
server = _seat_py(alignment, out["carried"], [125.0, 251.0, 333.0])
|
||||
_same_nodes(out["basePvi"], server)
|
||||
assert _plan_at(out["basePvi"], alignment["policy"], 125.0) == pytest.approx(
|
||||
_old_ground(125.0), abs=1e-4
|
||||
)
|
||||
home = _seat_py(alignment, out["carried"], [251.0, 333.0]) # 노선 저장 뒤
|
||||
_same_nodes(out["home"], home)
|
||||
kept = [n for n in out["home"] if abs(n["chainage_m"] - PIPE) >= 0.006]
|
||||
server = _seat_born_nodes(_new_alignment(alignment, out["home"]), kept, [MOVED]) # 관 저장 뒤
|
||||
_same_nodes(out["basePvi"], server, tol=1e-5) # 맞춤 풀이 수렴 폭(0.000001 m) 안팎
|
||||
if not balanced:
|
||||
assert _plan_at(out["basePvi"], alignment["policy"], MOVED) == pytest.approx(
|
||||
_old_ground(MOVED), abs=1e-4
|
||||
)
|
||||
|
||||
|
||||
def test_구조물_초안은_서버_따라감_위에(tmp_path: Path) -> None:
|
||||
|
||||
Reference in New Issue
Block a user