feat(측점열쇠): 346-2 덧 2 열쇠 한 벌 — 구간형 :start · :end · 묶음 측점 종류(grid · end) · 같은 열쇠 둘째 규칙 공용
- common_util_station_key(ts · py 짝): role 덧붙임(구간형 시작 · 끝 = structure:<id>:start · :end · 기준은 structure:<id> 그대로) · grid(원래 격자 번호) · end(BP/EP) 종류 · dedupeStationKeys(같은 열쇠 둘째부터 at:<누가거리>) - B06 stationKeysOf 가 같은 규칙(측점 계획 · 구조물 목록의 시작 · 끝 자리 · 공용 dedupe) - 거울 시험에 새 갈래(구간형 · grid · end · dedupe) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QfHKQs7J3hQELKMWQMUJEy
This commit is contained in:
@@ -18,7 +18,13 @@
|
||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
||||
import { pipeIdAt } from "../A00_Common/design/design_pipe_ids";
|
||||
import { showToast } from "@ui/ui_template_elements";
|
||||
import { chainageKey, looseChainage, stationKey } from "@util/common_util_station_key";
|
||||
import {
|
||||
chainageKey,
|
||||
dedupeStationKeys,
|
||||
looseChainage,
|
||||
stationKey,
|
||||
type StationKeyRole,
|
||||
} from "@util/common_util_station_key";
|
||||
import { inferStationInterval } from "@util/common_util_svg";
|
||||
import type { CrossSection, SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
|
||||
@@ -46,7 +52,8 @@ type Raw = Record<string, unknown> & { [HINT]?: Record<string, number> };
|
||||
|
||||
const lists = new Map<string, Keyed[]>();
|
||||
/** 프로젝트마다 구조물 측점 → id(측점 계획 · 구조물 목록). */
|
||||
const owners = new Map<string, Array<{ id: string; kind: "pipe" | "structure"; at: number }>>();
|
||||
type Owner = { id: string; kind: "pipe" | "structure"; at: number; role: StationKeyRole };
|
||||
const owners = new Map<string, Owner[]>();
|
||||
let current: { projectId: string; routeId: number } | null = null;
|
||||
|
||||
const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${routeId}`;
|
||||
@@ -54,7 +61,7 @@ const scopeOf = (projectId: string, routeId: number): string => `${projectId}:${
|
||||
/** 측점 계획(초안 · `planStations`)이 낸 덮는 구조물 id — 관 세션 id 는 `pipe-` 머리. */
|
||||
export function useStationOwners(
|
||||
projectId: string,
|
||||
planned: ReadonlyArray<{ chainage_m: number; owners: string[] }>,
|
||||
planned: ReadonlyArray<{ chainage_m: number; owners: string[]; role?: StationKeyRole }>,
|
||||
): void {
|
||||
owners.set(
|
||||
projectId,
|
||||
@@ -63,6 +70,7 @@ export function useStationOwners(
|
||||
id,
|
||||
kind: id.startsWith("pipe-") ? ("pipe" as const) : ("structure" as const),
|
||||
at: station.chainage_m,
|
||||
role: station.role ?? "anchor",
|
||||
})),
|
||||
),
|
||||
);
|
||||
@@ -71,12 +79,25 @@ export function useStationOwners(
|
||||
/** 구조물 목록(횡단 보기) — 한 자리 구조물의 id. 관 자리는 관 장부가 정함. */
|
||||
export function useStructureIds(
|
||||
projectId: string,
|
||||
structures: ReadonlyArray<{ structure_id?: string | null; chainage_m?: number | null }>,
|
||||
structures: ReadonlyArray<{
|
||||
structure_id?: string | null;
|
||||
chainage_m?: number | null;
|
||||
start_m?: number | null;
|
||||
end_m?: number | null;
|
||||
}>,
|
||||
): void {
|
||||
const known = (owners.get(projectId) ?? []).filter((owner) => owner.kind === "pipe");
|
||||
for (const item of structures) {
|
||||
if (item.structure_id && typeof item.chainage_m === "number")
|
||||
known.push({ id: item.structure_id, kind: "structure", at: item.chainage_m });
|
||||
const id = item.structure_id;
|
||||
if (!id) continue;
|
||||
// 기준 측점 = structure:<id> · 구간형 시작 · 끝 = :start · :end(열쇠 한 벌 · common_util_station_key)
|
||||
const marks: Array<[StationKeyRole, number | null | undefined]> = [
|
||||
["anchor", item.chainage_m],
|
||||
["start", item.start_m],
|
||||
["end", item.end_m],
|
||||
];
|
||||
for (const [role, at] of marks)
|
||||
if (typeof at === "number") known.push({ id, kind: "structure", at, role });
|
||||
}
|
||||
owners.set(projectId, known);
|
||||
}
|
||||
@@ -84,20 +105,19 @@ export function useStructureIds(
|
||||
const looksLikePipe = (section: CrossSection): boolean =>
|
||||
Boolean(section.culvert || section.box) || /^.+ D\d+/.test(section.structure ?? "");
|
||||
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:`). */
|
||||
/** 측점 목록 → 열쇠(같은 열쇠가 둘이면 뒤엣것은 `at:` · `dedupeStationKeys`). */
|
||||
export function stationKeysOf(
|
||||
projectId: string | null,
|
||||
sections: ReadonlyArray<CrossSection>,
|
||||
intervalM: number | null,
|
||||
): Keyed[] {
|
||||
const known = projectId ? (owners.get(projectId) ?? []) : [];
|
||||
const seen = new Set<string>();
|
||||
return sections.map((section) => {
|
||||
const keys = sections.map((section) => {
|
||||
const at = section.chainage_m;
|
||||
let key: string;
|
||||
if (section.kind === "irregular") {
|
||||
const owner = known.find((entry) => Math.abs(entry.at - at) < NEAR_M);
|
||||
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind);
|
||||
if (owner) key = stationKey("irregular", at, intervalM, owner.id, owner.kind, owner.role);
|
||||
else if (looksLikePipe(section))
|
||||
key = stationKey("pipe", at, intervalM, pipeIdAt(at, projectId));
|
||||
else
|
||||
@@ -105,10 +125,10 @@ export function stationKeysOf(
|
||||
? stationKey("regular", at, intervalM)
|
||||
: stationKey("irregular", at, intervalM);
|
||||
} else key = stationKey(section.kind, at, intervalM);
|
||||
if (seen.has(key)) key = `at:${chainageKey(at)}`;
|
||||
seen.add(key);
|
||||
return { key, chainageM: at };
|
||||
return key;
|
||||
});
|
||||
const chainages = sections.map((section) => section.chainage_m);
|
||||
return dedupeStationKeys(keys, chainages).map((key, i) => ({ key, chainageM: chainages[i] }));
|
||||
}
|
||||
|
||||
type KeyedDetail = Pick<SectionDetailResponse, "cross_sections" | "longitudinal">;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
일반 = ``grid:k``(누가거리 ÷ 측점 간격 반올림) · 관 = ``pipe:<id>`` · 구조물 = ``structure:<id>`` ·
|
||||
시종점 = ``end:BP`` · ``end:EP`` · id 를 모르는 구조물 측점 = ``at:<누가거리 0.01 m>``.
|
||||
구간형 구조물 시작 · 끝 측점 = ``structure:<id>:start`` · ``:end``(기준 측점은 ``structure:<id>``).
|
||||
한 목록에 같은 열쇠가 둘이면 둘째부터 ``at:<누가거리>``(``dedupe_station_keys``).
|
||||
|
||||
⚠ 파이썬·TS 짝 — ``common_util_station_key.ts`` ``stationKey`` ↔ ``station_key``.
|
||||
한쪽을 고치면 다른 쪽도 고치고 거울 시험 ``resources/tester/test_344_3_station_key_mirror.py``.
|
||||
@@ -31,19 +33,43 @@ def station_key(
|
||||
interval_m: float | None,
|
||||
station_id: str | None = None,
|
||||
id_kind: str = "structure",
|
||||
role: str = "anchor",
|
||||
) -> str:
|
||||
"""측점 열쇠 — irregular 는 id 가 있으면 pipe: · structure:(``id_kind``) · 없으면 at:."""
|
||||
"""측점 열쇠 — irregular 는 id 가 있으면 pipe: · structure:(``id_kind``) · 없으면 at:.
|
||||
|
||||
구간형 시작 · 끝은 ``role`` 로 ``:start`` · ``:end``. ``grid`` · ``end`` 는 ``station_id`` 에
|
||||
원래 격자 번호 · BP/EP(없으면 누가거리 ÷ 간격 반올림 · 0 이면 BP).
|
||||
"""
|
||||
loose = f"at:{chainage_key(chainage_m)}"
|
||||
suffix = "" if role == "anchor" else f":{role}"
|
||||
if kind == "bp":
|
||||
return "end:BP"
|
||||
if kind == "ep":
|
||||
return "end:EP"
|
||||
if kind == "end":
|
||||
return (
|
||||
f"end:{station_id if station_id is not None else ('BP' if chainage_m <= 0 else 'EP')}"
|
||||
)
|
||||
if kind in ("pipe", "structure"):
|
||||
return f"{kind}:{station_id}" if station_id else f"at:{chainage_key(chainage_m)}"
|
||||
return f"{kind}:{station_id}{suffix}" if station_id else loose
|
||||
if kind == "irregular":
|
||||
return f"{id_kind}:{station_id}" if station_id else f"at:{chainage_key(chainage_m)}"
|
||||
return f"{id_kind}:{station_id}{suffix}" if station_id else loose
|
||||
if kind == "grid" and station_id is not None:
|
||||
return f"grid:{station_id}"
|
||||
if interval_m and interval_m > 0:
|
||||
return f"grid:{_half_up(chainage_m / interval_m)}"
|
||||
return f"at:{chainage_key(chainage_m)}"
|
||||
return loose
|
||||
|
||||
|
||||
def dedupe_station_keys(keys: list[str], chainages: list[float]) -> list[str]:
|
||||
"""한 목록에 같은 열쇠가 둘이면 둘째부터 ``at:<누가거리>`` — 열쇠마다 측점 하나."""
|
||||
seen: set[str] = set()
|
||||
out: list[str] = []
|
||||
for key, chainage in zip(keys, chainages, strict=True):
|
||||
unique = f"at:{chainage_key(chainage)}" if key in seen else key
|
||||
seen.add(unique)
|
||||
out.append(unique)
|
||||
return out
|
||||
|
||||
|
||||
def loose_chainage(key: str) -> float | None:
|
||||
|
||||
@@ -3,21 +3,29 @@
|
||||
* 측점 열쇠 한 규칙(PLAN 344-3) — 노선이 바뀌어도 같은 측점을 같은 이름으로 부름.
|
||||
* 일반 = `grid:k`(누가거리 ÷ 측점 간격 반올림) · 관 = `pipe:<id>` · 구조물 = `structure:<id>` ·
|
||||
* 시종점 = `end:BP` · `end:EP` · id 를 모르는 구조물 측점 = `at:<누가거리 0.01 m>`(옛 초안 열쇠와 같은 몫).
|
||||
* 구간형 구조물의 시작 · 끝 측점 = `structure:<id>:start` · `:end`(기준 측점은 `structure:<id>` 그대로).
|
||||
* 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>`(`dedupeStationKeys`).
|
||||
* 쓰는 곳 = 서버 `B05_Profile_Route_Save_Parts` · 묶음 stations 단계 · B06 세션 횡단 초안(`stationKeysOf`).
|
||||
*
|
||||
* ⚠ 파이썬·TS 짝 — `common_util_station_key.py` `station_key` ↔ `stationKey`. 한쪽을 고치면 다른 쪽도
|
||||
* 고치고 거울 시험 `resources/tester/test_344_3_station_key_mirror.py`.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 측점 종류 — 서버 측점 `kind`(bp · ep · regular · irregular) 그대로 받음. */
|
||||
export type StationKeyKind = "bp" | "ep" | "regular" | "irregular" | "pipe" | "structure";
|
||||
/** 측점 종류 — 서버 측점 `kind`(bp · ep · regular · irregular) · 묶음 측점 `kind`(grid · end · pipe · structure). */
|
||||
export type StationKeyKind =
|
||||
"bp" | "ep" | "regular" | "irregular" | "grid" | "end" | "pipe" | "structure";
|
||||
|
||||
/** 구간형 구조물 측점 자리 — 기준(anchor) · 시작 · 끝. */
|
||||
export type StationKeyRole = "anchor" | "start" | "end";
|
||||
|
||||
/** 누가거리 0.01 m — 옛 초안 열쇠(`toFixed(2)`)와 같은 자릿수. */
|
||||
export const chainageKey = (chainageM: number): string =>
|
||||
(Math.round(chainageM * 100) / 100).toFixed(2);
|
||||
|
||||
/**
|
||||
* 측점 열쇠. irregular 는 id 가 있으면 `pipe:` · `structure:`(종류는 `idKind`) · 없으면 `at:`.
|
||||
* 간격이 없거나 0 이하면 일반 측점도 `at:`.
|
||||
* 측점 열쇠. irregular 는 id 가 있으면 `pipe:` · `structure:`(종류는 `idKind`) · 없으면 `at:` · 구간형 시작 · 끝은
|
||||
* `role` 로 `:start` · `:end` 덧붙임. `grid` · `end` 는 `id` 에 원래 격자 번호 · `BP`/`EP` 를 줄 수 있음(없으면
|
||||
* 누가거리 ÷ 간격 반올림 · 0 이면 BP). 간격이 없거나 0 이하면 일반 측점도 `at:`.
|
||||
*/
|
||||
export function stationKey(
|
||||
kind: StationKeyKind,
|
||||
@@ -25,14 +33,31 @@ export function stationKey(
|
||||
intervalM: number | null,
|
||||
id: string | null = null,
|
||||
idKind: "pipe" | "structure" = "structure",
|
||||
role: StationKeyRole = "anchor",
|
||||
): string {
|
||||
const loose = `at:${chainageKey(chainageM)}`;
|
||||
const suffix = role === "anchor" ? "" : `:${role}`;
|
||||
if (kind === "bp") return "end:BP";
|
||||
if (kind === "ep") return "end:EP";
|
||||
if (kind === "pipe" || kind === "structure")
|
||||
return id ? `${kind}:${id}` : `at:${chainageKey(chainageM)}`;
|
||||
if (kind === "irregular") return id ? `${idKind}:${id}` : `at:${chainageKey(chainageM)}`;
|
||||
if (kind === "end") return `end:${id ?? (chainageM <= 0 ? "BP" : "EP")}`;
|
||||
if (kind === "pipe" || kind === "structure") return id ? `${kind}:${id}${suffix}` : loose;
|
||||
if (kind === "irregular") return id ? `${idKind}:${id}${suffix}` : loose;
|
||||
if (kind === "grid" && id !== null) return `grid:${id}`;
|
||||
if (intervalM && intervalM > 0) return `grid:${Math.round(chainageM / intervalM)}`;
|
||||
return `at:${chainageKey(chainageM)}`;
|
||||
return loose;
|
||||
}
|
||||
|
||||
/** 한 목록에 같은 열쇠가 둘이면 둘째부터 `at:<누가거리>` — 열쇠마다 측점 하나. */
|
||||
export function dedupeStationKeys(
|
||||
keys: ReadonlyArray<string>,
|
||||
chainages: ReadonlyArray<number>,
|
||||
): string[] {
|
||||
const seen = new Set<string>();
|
||||
return keys.map((key, index) => {
|
||||
const unique = seen.has(key) ? `at:${chainageKey(chainages[index])}` : key;
|
||||
seen.add(unique);
|
||||
return unique;
|
||||
});
|
||||
}
|
||||
|
||||
/** 옛 초안 열쇠(누가거리 글 `"85.05"`) · `at:` 열쇠의 누가거리 — 측점 열쇠면 null. */
|
||||
|
||||
@@ -16,6 +16,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from common_util.common_util_station_key import ( # noqa: E402
|
||||
chainage_key,
|
||||
dedupe_station_keys,
|
||||
loose_chainage,
|
||||
station_key,
|
||||
)
|
||||
@@ -24,7 +25,17 @@ TS = PROJECT_ROOT / "common_util" / "common_util_station_key.ts"
|
||||
TYPESCRIPT = PROJECT_ROOT / "config" / "node_modules" / "typescript"
|
||||
|
||||
CASES = [
|
||||
("bp", 0.0, 20.0, None, "structure"),
|
||||
("bp", 0.0, 20.0, None, "structure", "anchor"),
|
||||
# 5파 — 묶음 측점 종류(grid · end) · 구간형 시작 · 끝(:start · :end)
|
||||
("grid", 41.3, 20.0, "2", "structure", "anchor"),
|
||||
("grid", 41.3, 20.0, None, "structure", "anchor"),
|
||||
("end", 0.0, 20.0, None, "structure", "anchor"),
|
||||
("end", 99.0, 20.0, None, "structure", "anchor"),
|
||||
("end", 99.0, 20.0, "EP", "structure", "anchor"),
|
||||
("structure", 30.0, 20.0, "st-9", "structure", "start"),
|
||||
("structure", 36.0, 20.0, "st-9", "structure", "end"),
|
||||
("irregular", 30.0, 20.0, "st-9", "structure", "start"),
|
||||
("pipe", 30.0, 20.0, None, "structure", "end"),
|
||||
("ep", 812.345, 20.0, None, "structure"),
|
||||
("regular", 40.0, 20.0, None, "structure"),
|
||||
("regular", 50.0, 20.0, None, "structure"), # 2.5 → 3(위로)
|
||||
@@ -37,6 +48,7 @@ CASES = [
|
||||
("pipe", 12.344, 20.0, "pipe-s1", "structure"),
|
||||
("structure", 12.345, 20.0, None, "structure"),
|
||||
]
|
||||
DEDUPE = (["grid:1", "structure:st-1", "grid:1", "structure:st-1"], [20.0, 25.0, 20.004, 25.006])
|
||||
LOOSE = ["85.05", "at:85.05", "grid:4", "pipe:pipe-s3", "-1.5", "end:BP"]
|
||||
|
||||
SCRIPT = r"""
|
||||
@@ -50,7 +62,8 @@ new Function("exports", "module", js)(box.exports, box);
|
||||
const m = box.exports;
|
||||
const input = JSON.parse(process.argv[3]);
|
||||
process.stdout.write(JSON.stringify({
|
||||
keys: input.cases.map((c) => m.stationKey(c[0], c[1], c[2], c[3], c[4])),
|
||||
keys: input.cases.map((c) => m.stationKey(c[0], c[1], c[2], c[3], c[4], c[5] ?? "anchor")),
|
||||
dedupe: m.dedupeStationKeys(input.dedupe[0], input.dedupe[1]),
|
||||
chainage: input.cases.map((c) => m.chainageKey(c[1])),
|
||||
loose: input.loose.map((k) => m.looseChainage(k)),
|
||||
}));
|
||||
@@ -65,7 +78,7 @@ def test_파이썬과_TS_열쇠_같음():
|
||||
SCRIPT,
|
||||
str(TYPESCRIPT),
|
||||
str(TS),
|
||||
json.dumps({"cases": CASES, "loose": LOOSE}),
|
||||
json.dumps({"cases": CASES, "loose": LOOSE, "dedupe": DEDUPE}),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -77,6 +90,7 @@ def test_파이썬과_TS_열쇠_같음():
|
||||
assert out["keys"] == [station_key(*case) for case in CASES]
|
||||
assert out["chainage"] == [chainage_key(case[1]) for case in CASES]
|
||||
assert out["loose"] == [loose_chainage(key) for key in LOOSE]
|
||||
assert out["dedupe"] == dedupe_station_keys(*DEDUPE)
|
||||
|
||||
|
||||
def test_열쇠_규칙():
|
||||
@@ -86,3 +100,9 @@ def test_열쇠_규칙():
|
||||
assert station_key("irregular", 85.054, 20.0, "pipe-s3", "pipe") == "pipe:pipe-s3"
|
||||
assert station_key("irregular", 85.054, 20.0) == "at:85.05"
|
||||
assert loose_chainage("85.05") == 85.05 and loose_chainage("grid:4") is None
|
||||
# 5파 — 구간형 시작 · 끝 · 기준은 그대로 · 원래 격자 번호 · 같은 열쇠 둘째는 at:
|
||||
assert station_key("structure", 30.0, 20.0, "st-9", role="start") == "structure:st-9:start"
|
||||
assert station_key("structure", 33.0, 20.0, "st-9") == "structure:st-9"
|
||||
assert station_key("grid", 41.3, 20.0, "2") == "grid:2"
|
||||
assert station_key("end", 99.0, 20.0) == "end:EP"
|
||||
assert dedupe_station_keys(*DEDUPE) == ["grid:1", "structure:st-1", "at:20.00", "at:25.01"]
|
||||
|
||||
Reference in New Issue
Block a user