Files
Aislo/D03_PlanAnalysis/logic/D03_PlanAnalysis_Plan.ts
T
eomsangdonandClaude Opus 5.5 50e9c1b015 feat(530-3): D03 예정노선 분석 화면 — 평면 · 지점 끌기 · 더 받기 · 갈래 카드 · 측점 · 지반 횡단 · 3D
- 평면 = 위성 + 도엽 등고선 + 다른 갈래(옅게) + 고른 노선 + 측점(누가 = 등록 시작부터) + 지점 끌기 겹
- 끈 지점 = 캐시 · 받기 · 분석 · 보기는 points 로 실어 보냄 · [저장] 때만 D02 규칙으로 정본
- 연도별 갈래 넷 카드(연장 · 최대 기울기 · 위반 수) · 고름 = 캐시 · [저장] = 서버 저장본으로 다시 셈
- 측점 누름 = 하 패널 지반선 횡단(sectionsGround · buildCrossSvg 지반만 인자 · 기본 그대로)
- 3D 모달 = 연도 DEM + 노선 + 측점 막대(코리도 · 구조물 안 부름)
- 화면 계산 logic 짝(pickMode · chainRows · joinedLine ↔ 파이썬) 거울 시험

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 20:11:23 +09:00

181 lines
6.6 KiB
TypeScript

/* =============================================================================
* D03_PlanAnalysis_Plan.ts
* D 예정노선 2단계 분석 화면 계산(PLAN 530-3) — 갈래 고름 · 이은 누가 · 이은 중심선 · 측점 ·
* 지점(연도 경계) · 받은 지형 범위 판정. DOM · fetch · sessionStorage 없음.
*
* ⚠ 파이썬·TS 짝 — `D03_PlanAnalysis_Analyze.py` `pick_mode` · `chain` · `joined_line` ↔
* `pickMode` · `chainRows` · `joinedLine`. 한쪽을 고치면 다른 쪽도 고치고 거울 시험
* `resources/tester/test_530_3_plan_logic.py`. 화면은 고르는 즉시 이 짝으로 · [저장]은 서버가 다시 셈.
* ========================================================================== */
export type Vertex = [number, number];
/** 짝: `D03_PlanAnalysis_Analyze.PLAN_MODES`(follow 뺌). */
export const PLAN_MODES = ["gradeline", "earthwork", "gentle", "weighted"] as const;
export type PlanMode = (typeof PLAN_MODES)[number];
/** 짝: `D03_PlanAnalysis_Terrain.PLAN_CORRIDOR_M` — 지점이 연도 DEM 가장자리에서 이만큼 안이어야
* 탐색 복도가 받은 지형 안에 듦(DEM = 구간 상자 + 복도 + 50 m). */
export const PLAN_REACH_M = 300;
export interface PlanRoute {
planned?: Vertex[];
metrics?: {
length_m: number;
max_grade_pct?: number;
over_limit?: { total: number };
};
error?: string;
}
export interface YearAnalysis {
year: number;
start: Vertex;
end: Vertex;
routes: Record<string, PlanRoute>;
}
export interface ChainRow {
year: number;
mode: string | null;
length_m: number;
start_chainage_m: number;
end_chainage_m: number;
}
const round3 = (value: number): number => Number(value.toFixed(3));
/** 고른 갈래 — 없거나 빈 갈래면 차례대로 첫 성공 갈래. */
export function pickMode(result: YearAnalysis, wanted: string | null | undefined): string | null {
const routes = result.routes ?? {};
const ok = (mode: string): boolean => mode in routes && !("error" in routes[mode]);
if (wanted && ok(wanted)) return wanted;
return PLAN_MODES.find(ok) ?? null;
}
/** 연도별 고른 갈래 · 시작 · 끝 누가(이어 셈) — 앞 연도 끝 누가 = 다음 연도 시작 누가. */
export function chainRows(
results: ReadonlyArray<YearAnalysis>,
choices: Readonly<Record<number, string | null | undefined>>,
startChainageM: number,
): ChainRow[] {
let at = startChainageM || 0;
return results.map((result) => {
const mode = pickMode(result, choices[result.year]);
const length = mode ? Number(result.routes[mode].metrics?.length_m ?? 0) : 0;
const row = {
year: result.year,
mode,
length_m: round3(length),
start_chainage_m: round3(at),
end_chainage_m: round3(at + length),
};
at += length;
return row;
});
}
const samePoint = (a: Vertex, b: Vertex): boolean =>
Math.abs(a[0] - b[0]) < 1e-3 && Math.abs(a[1] - b[1]) < 1e-3;
/** 고른 갈래를 이은 중심선 — 경계점 한 번만(꺾인 채). */
export function joinedLine(
results: ReadonlyArray<YearAnalysis>,
rows: ReadonlyArray<ChainRow>,
): Vertex[] {
const line: Vertex[] = [];
results.forEach((result, i) => {
const mode = rows[i]?.mode;
if (!mode) return;
const planned = result.routes[mode].planned ?? [];
const head = line.length && planned.length && samePoint(line[line.length - 1], planned[0]);
line.push(...(head ? planned.slice(1) : planned));
});
return line;
}
/** 측점 누가(오름차순) — 연도 경계마다(경계점 = 측점) + 간격 배수 + 끝. 노선 없는 연도는 건너뜀. */
export function stationChainages(rows: ReadonlyArray<ChainRow>, intervalM: number): number[] {
const out: number[] = [];
const push = (value: number): void => {
if (!out.length || value - out[out.length - 1] > 1e-6) out.push(round3(value));
};
for (const row of rows) {
if (!row.mode) continue;
push(row.start_chainage_m);
let next = Math.floor(row.start_chainage_m / intervalM + 1e-9) * intervalM + intervalM;
for (; next < row.end_chainage_m - 1e-6; next += intervalM) push(next);
push(row.end_chainage_m);
}
return out;
}
/** 측점 그리기 · 집기용 선 — 첫 점 앞에 첫 구간 방향으로 `startM` 만큼 뒤로 뺀 점을 붙여 선 위 누가 =
* 등록 시작 누가부터(`drawStationTicks` · `stationAtScreen` 은 선 첫 점을 0 으로 셈). */
export function leadInLine(line: ReadonlyArray<Vertex>, startM: number): Vertex[] {
if (line.length < 2 || !(startM > 0)) return [...line];
const [a, b] = line;
const length = Math.hypot(b[0] - a[0], b[1] - a[1]) || 1;
const back: Vertex = [
a[0] - ((b[0] - a[0]) / length) * startM,
a[1] - ((b[1] - a[1]) / length) * startM,
];
return [back, ...line];
}
/** 누가가 든 연도 행(경계는 앞 연도) — 노선 있는 연도만 · 밖이면 null. */
export function rowAt(rows: ReadonlyArray<ChainRow>, chainageM: number): ChainRow | null {
return (
rows.find(
(row) =>
row.mode &&
chainageM >= row.start_chainage_m - 1e-6 &&
chainageM <= row.end_chainage_m + 1e-6,
) ?? null
);
}
/** 연도별 지점 → 마디 P0…Pn(직전 종점 = 다음 시점). */
export function nodesOf(years: ReadonlyArray<{ start: Vertex; end: Vertex }>): Vertex[] {
return years.length ? [years[0].start, ...years.map((year) => year.end)] : [];
}
/** 마디 → 연도별 지점(서버 `points` 꼴). */
export function pointsOf(
years: ReadonlyArray<number>,
nodes: ReadonlyArray<Vertex>,
): Array<{ year: number; start: Vertex; end: Vertex }> {
return years.map((year, i) => ({ year, start: nodes[i], end: nodes[i + 1] }));
}
export interface Box {
x0: number;
y0: number;
x1: number;
y1: number;
}
/** 마디마다 받은 지형 안인가 — 그 마디를 끝점으로 쓰는 연도(앞 · 뒤) DEM 상자를 `reachM` 만큼 줄인 안.
* 연도 DEM 이 없으면 밖. */
export function nodesInReach(
nodes: ReadonlyArray<Vertex>,
boxes: ReadonlyArray<Box | null>,
reachM: number = PLAN_REACH_M,
): boolean[] {
const inside = (point: Vertex, box: Box | null): boolean =>
!!box &&
point[0] >= box.x0 + reachM &&
point[0] <= box.x1 - reachM &&
point[1] >= box.y0 + reachM &&
point[1] <= box.y1 - reachM;
return nodes.map((point, i) => {
const years = [i - 1, i].filter((year) => year >= 0 && year < boxes.length);
return years.every((year) => inside(point, boxes[year]));
});
}
/** 마디가 정본과 같은가(1 mm — 짝: `D02_PlanBase_Router.SAME_M`). */
export function sameNodes(a: ReadonlyArray<Vertex>, b: ReadonlyArray<Vertex>): boolean {
return a.length === b.length && a.every((point, i) => samePoint(point, b[i]));
}