- 평면 = 위성 + 도엽 등고선 + 다른 갈래(옅게) + 고른 노선 + 측점(누가 = 등록 시작부터) + 지점 끌기 겹 - 끈 지점 = 캐시 · 받기 · 분석 · 보기는 points 로 실어 보냄 · [저장] 때만 D02 규칙으로 정본 - 연도별 갈래 넷 카드(연장 · 최대 기울기 · 위반 수) · 고름 = 캐시 · [저장] = 서버 저장본으로 다시 셈 - 측점 누름 = 하 패널 지반선 횡단(sectionsGround · buildCrossSvg 지반만 인자 · 기본 그대로) - 3D 모달 = 연도 DEM + 노선 + 측점 막대(코리도 · 구조물 안 부름) - 화면 계산 logic 짝(pickMode · chainRows · joinedLine ↔ 파이썬) 거울 시험 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
181 lines
6.6 KiB
TypeScript
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]));
|
|
}
|