feat(509-5 · 509-2 B05 추천 지형): 분석 지형 고르기 = 추천 노선 창 안 · 지형마다 추천 미리 셈 · [적용] = 창 지형이 설계 지형 · 평면은 지형 이름만 · 설계 지형 격자 하나(수치지형도 적용이면 그 격자)
- 전처리 미리 셈이 고를 수 있는 지형마다(설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만) 다섯 갈래 + 초기 종단 - 창 지형 바꿈 = 그 지형 저장본만 · 없으면 409 안내(+[재계산]) · 기본값 판정에서 지형 뺌 - POST /route/initial terrain = 창 지형 · [적용] draft.terrain = 창 지형 · 손 [저장]은 지형 안 바꿈(서버도 as_initial 일 때만) - 평면 분석 지형 고르기 · 화면 취향(routeedit-terrain) 걷음 · 지반고 · 묶음 초안 = 설계 지형(null) · 적용 뒤 받아 둔 격자 버림 - 시험 test_509_5 · test_509_2 · 걷은 고르기를 보던 옛 시험 새 동작으로(352_3 건너뜀) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -72,8 +72,6 @@ export const STATE_REGISTRY = {
|
|||||||
"routeedit-view": { bucket: "pref", scope: "project" },
|
"routeedit-view": { bucket: "pref", scope: "project" },
|
||||||
/** B05 계획노선 편집 창 — 시점 · 종점 · 교각점 고정. [저장] 대상이 아니라 draft 에 안 둠 */
|
/** B05 계획노선 편집 창 — 시점 · 종점 · 교각점 고정. [저장] 대상이 아니라 draft 에 안 둠 */
|
||||||
"routeedit-locks": { bucket: "pref", scope: "project" },
|
"routeedit-locks": { bucket: "pref", scope: "project" },
|
||||||
/** B05 계획노선 편집 창 — 분석 지형(las · sheet). 확정 지표면은 안 바꿈 */
|
|
||||||
"routeedit-terrain": { bucket: "pref", scope: "project" },
|
|
||||||
/** B05 계획노선 편집 창 — 측점 스냅 간격(10 · station · 280-4) */
|
/** B05 계획노선 편집 창 — 측점 스냅 간격(10 · station · 280-4) */
|
||||||
"routeedit-snap-step": { bucket: "pref", scope: "project" },
|
"routeedit-snap-step": { bucket: "pref", scope: "project" },
|
||||||
"masshaul-open": {
|
"masshaul-open": {
|
||||||
|
|||||||
@@ -61,11 +61,7 @@ import {
|
|||||||
savedPlusRows,
|
savedPlusRows,
|
||||||
} from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
} from "../../B05_Profile/B05_Profile_Api_PlusStations";
|
||||||
import { pageReplaying } from "../../B05_Profile/B05_Profile_UI_Profile_History";
|
import { pageReplaying } from "../../B05_Profile/B05_Profile_UI_Profile_History";
|
||||||
import {
|
import { fetchRoutePlan, type RoutePlanResponse } from "../../B05_Profile/B05_Profile_Api_Replan";
|
||||||
analysisTerrain,
|
|
||||||
fetchRoutePlan,
|
|
||||||
type RoutePlanResponse,
|
|
||||||
} from "../../B05_Profile/B05_Profile_Api_Replan";
|
|
||||||
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
import { fetchStructureTypes } from "../../B05_Profile/B05_Profile_Api_Structures";
|
||||||
import { forgetPageData, pagePipes, pageStructures, trackPageBusy } from "./design_page_data";
|
import { forgetPageData, pagePipes, pageStructures, trackPageBusy } from "./design_page_data";
|
||||||
import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
|
import type { PagePipelineOptions, ProfileLink } from "./design_pipeline_page_types";
|
||||||
@@ -637,7 +633,7 @@ function createPagePipeline(
|
|||||||
const wasDraft = pipe.input("route") !== null;
|
const wasDraft = pipe.input("route") !== null;
|
||||||
let draft: RouteDraft | null = null;
|
let draft: RouteDraft | null = null;
|
||||||
if (edit) {
|
if (edit) {
|
||||||
draft = { ...edit, rev: (stored?.rev ?? 0) + 1, terrain: analysisTerrain(projectId) };
|
draft = { ...edit, rev: (stored?.rev ?? 0) + 1, terrain: null }; // 설계 지형(509-5)
|
||||||
if (stored && same({ ...stored, rev: 0 }, { ...draft, rev: 0 })) draft = stored;
|
if (stored && same({ ...stored, rev: 0 }, { ...draft, rev: 0 })) draft = stored;
|
||||||
}
|
}
|
||||||
if (draft && !wasDraft && !held) held = holdNow();
|
if (draft && !wasDraft && !held) held = holdNow();
|
||||||
|
|||||||
@@ -6,51 +6,24 @@
|
|||||||
* POST /projects/{id}/route/replan → 노선 초안 저장(346-3 · draft = 계약 RouteDraft · as_initial)
|
* POST /projects/{id}/route/replan → 노선 초안 저장(346-3 · draft = 계약 RouteDraft · as_initial)
|
||||||
* GET /projects/{id}/route/terrain-sources → 고를 수 있는 분석 지형(라이다 · 수치지형도)
|
* GET /projects/{id}/route/terrain-sources → 고를 수 있는 분석 지형(라이다 · 수치지형도)
|
||||||
*
|
*
|
||||||
* 편집 창 분석(지반고 · 횡단 미리보기 · 추천)은 **고른 분석 지형**(`terrain`)을 실어
|
* 편집 창 분석(지반고 · 횡단 미리보기)은 설계 지형 그대로 — 지형 고르기는 추천 창 안 · [적용]이
|
||||||
* 보낸다(PLAN 133-1) — [확인] 재계산은 확정 지표면 그대로라 안 실음.
|
* 그 지형을 설계 지형으로(509-5).
|
||||||
*
|
*
|
||||||
* 재계산은 배수유역부터 전 단계를 다시 도는 무거운 작업이라(용화 67측점 기준 3분대)
|
* 재계산은 배수유역부터 전 단계를 다시 도는 무거운 작업이라(용화 67측점 기준 3분대)
|
||||||
* 타임아웃을 길게 잡는다 — 기본값으로 두면 중간에 끊긴다.
|
* 타임아웃을 길게 잡는다 — 기본값으로 두면 중간에 끊긴다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { API_BASE_URL } from "@config/config_frontend";
|
import { API_BASE_URL } from "@config/config_frontend";
|
||||||
import { readState, writeState } from "../A00_Common/b_page_state";
|
|
||||||
import { heightAt } from "../common_util/common_util_surface_sampler";
|
import { heightAt } from "../common_util/common_util_surface_sampler";
|
||||||
import { loadDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
import { loadDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||||
import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types";
|
import type { RouteDraft } from "../A00_Common/design/logic/design_pipeline_types";
|
||||||
import type { RouteNodeEdit } from "./logic/B05_Profile_UI_RouteEdit_Edits";
|
import type { RouteNodeEdit } from "./logic/B05_Profile_UI_RouteEdit_Edits";
|
||||||
import type { RouteReset } from "./logic/B05_Profile_Route_Reset";
|
import type { RouteReset } from "./logic/B05_Profile_Route_Reset";
|
||||||
|
|
||||||
/** 편집 창 분석 지형 — las = 확정 지표면(라이다) · sheet = 수치지형도 서피스. */
|
/** 분석 지형 — las = 확정 지표면(라이다) · sheet = 수치지형도 서피스. 고르기는 추천 노선 창 안(509-5). */
|
||||||
export type AnalysisTerrain = "las" | "sheet";
|
export type AnalysisTerrain = "las" | "sheet";
|
||||||
|
|
||||||
/** 편집기가 열 때 알리는 지형 사정(352-3) — 저장하지 않음. 고를 수 있나(라이다 · 수치지형도 둘) · 설계 지형. */
|
/** 이 프로젝트가 고를 수 있는 분석 지형(둘이면 추천 창에 고르기)과 지금 설계 지형. */
|
||||||
const terrainContext = new Map<string, { choosable: boolean; design: AnalysisTerrain }>();
|
|
||||||
|
|
||||||
export const setTerrainContext = (
|
|
||||||
projectId: string,
|
|
||||||
choosable: boolean,
|
|
||||||
design: AnalysisTerrain,
|
|
||||||
): void => void terrainContext.set(projectId, { choosable, design });
|
|
||||||
|
|
||||||
/** 지금 쓰는 분석 지형 — 하나뿐인 프로젝트는 그것 · 둘이면 사용자가 고른 값(화면 상태 보관소 pref 통 ·
|
|
||||||
* 노선 편집 고정과 같은 자리), 고른 적 없으면 데이터대로(= 설계 지형 · 라이다가 있으면 라이다).
|
|
||||||
* 고른 값은 [저장] 때 설계 지형이 됨(PLAN 133-2). */
|
|
||||||
export const analysisTerrain = (
|
|
||||||
projectId: string,
|
|
||||||
fallback: AnalysisTerrain = "las",
|
|
||||||
): AnalysisTerrain => {
|
|
||||||
const context = terrainContext.get(projectId);
|
|
||||||
if (context && !context.choosable) return context.design;
|
|
||||||
const saved = readState<string>("routeedit-terrain", projectId);
|
|
||||||
return saved === "sheet" || saved === "las" ? saved : (context?.design ?? fallback);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 사용자가 고를 때만 기억 — 열 때 기본값은 쓰지 않음. */
|
|
||||||
export const setAnalysisTerrain = (projectId: string, terrain: AnalysisTerrain): void =>
|
|
||||||
writeState("routeedit-terrain", terrain, projectId);
|
|
||||||
|
|
||||||
/** 이 프로젝트가 고를 수 있는 분석 지형(둘이면 고르기를 띄운다)과 지금 설계 지형. */
|
|
||||||
export async function fetchTerrainSources(
|
export async function fetchTerrainSources(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<{ sources: AnalysisTerrain[]; design: AnalysisTerrain }> {
|
): Promise<{ sources: AnalysisTerrain[]; design: AnalysisTerrain }> {
|
||||||
@@ -216,14 +189,14 @@ export async function replanRoute(
|
|||||||
*
|
*
|
||||||
* 새 계산이 아니라 확정된 지표면을 **읽기만** 하므로 편집 중에 불러도 된다 — 다만 끄는 동안
|
* 새 계산이 아니라 확정된 지표면을 **읽기만** 하므로 편집 중에 불러도 된다 — 다만 끄는 동안
|
||||||
* 프레임마다 부르지는 않는다(찍는 순간에만). 지표면 밖은 `null` 로 온다.
|
* 프레임마다 부르지는 않는다(찍는 순간에만). 지표면 밖은 `null` 로 온다.
|
||||||
* 설계 지형 격자를 받아 두었고 분석 지형이 그것과 같으면 **브라우저 표본**(서버와 같은 값 ·
|
* 설계 지형 격자를 받아 두었으면 **브라우저 표본**(서버와 같은 값 · PLAN 344-1) — 아니면
|
||||||
* PLAN 344-1) — 아니면 서버에 물음. 자릿수는 서버 응답과 같게 mm. */
|
* 서버에 물음. 자릿수는 서버 응답과 같게 mm. */
|
||||||
export async function fetchRouteElevations(
|
export async function fetchRouteElevations(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
points: Array<[number, number]>,
|
points: Array<[number, number]>,
|
||||||
): Promise<Array<number | null>> {
|
): Promise<Array<number | null>> {
|
||||||
const local = await loadDesignTerrainGrid(projectId);
|
const local = await loadDesignTerrainGrid(projectId);
|
||||||
if (local && analysisTerrain(projectId) === local.design) {
|
if (local) {
|
||||||
return points.map(([x, y]) => {
|
return points.map(([x, y]) => {
|
||||||
const z = heightAt(local.grid, x, y);
|
const z = heightAt(local.grid, x, y);
|
||||||
return Number.isNaN(z) ? null : Math.round(z * 1000) / 1000;
|
return Number.isNaN(z) ? null : Math.round(z * 1000) / 1000;
|
||||||
@@ -231,7 +204,7 @@ export async function fetchRouteElevations(
|
|||||||
}
|
}
|
||||||
const payload = await requestJson<{ z: Array<number | null> }>(
|
const payload = await requestJson<{ z: Array<number | null> }>(
|
||||||
`/projects/${projectId}/route/elevations`,
|
`/projects/${projectId}/route/elevations`,
|
||||||
{ method: "POST", body: JSON.stringify({ points, terrain: analysisTerrain(projectId) }) },
|
{ method: "POST", body: JSON.stringify({ points }) },
|
||||||
60000,
|
60000,
|
||||||
);
|
);
|
||||||
return payload.z;
|
return payload.z;
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
* B05_Profile_Api_RouteInitial.ts
|
* B05_Profile_Api_RouteInitial.ts
|
||||||
* 초기노선 갈래 요청(계획서 20) — 로직 하나를 골라 서버가 새 계획노선 노드를 낸다.
|
* 초기노선 갈래 요청(계획서 20) — 로직 하나를 골라 서버가 새 계획노선 노드를 낸다.
|
||||||
*
|
*
|
||||||
* POST /projects/{id}/route/initial { mode, weights?, criteria? } → 노드 · 비교값 · 공통 제약 기준값
|
* POST /projects/{id}/route/initial { mode, weights?, criteria?, terrain? } → 노드 · 비교값 · 공통 제약 기준값
|
||||||
*
|
*
|
||||||
* 계산 자리는 **서버 단독**(CLAUDE.md 5장 ③) — 화면은 부르고, 받은 노드를 편집 캐시에 싣고,
|
* 계산 자리는 **서버 단독**(CLAUDE.md 5장 ③) — 화면은 부르고, 받은 노드를 편집 캐시에 싣고,
|
||||||
* 받은 비교값·기준값을 보이기만 한다. 모양은 계획서 20 「API」 줄(서버 `initial_route`) —
|
* 받은 비교값·기준값을 보이기만 한다. 모양은 계획서 20 「API」 줄(서버 `initial_route`) —
|
||||||
* 답을 펴는 자리는 `normalizeRouteInitial`(`_RouteEdit_Initial_Model`) 한 곳이다.
|
* 답을 펴는 자리는 `normalizeRouteInitial`(`_RouteEdit_Initial_Model`) 한 곳이다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { analysisTerrain, requestJson } from "./B05_Profile_Api_Replan";
|
import { requestJson, type AnalysisTerrain } from "./B05_Profile_Api_Replan";
|
||||||
|
|
||||||
/** 갈래 셈 대기 상한 — 노선 여러 벌을 셈해 견주므로 넉넉히 잡는다. */
|
/** 갈래 셈 대기 상한 — 노선 여러 벌을 셈해 견주므로 넉넉히 잡는다. */
|
||||||
const INITIAL_TIMEOUT_MS = 5 * 60 * 1000;
|
const INITIAL_TIMEOUT_MS = 5 * 60 * 1000;
|
||||||
@@ -44,6 +44,8 @@ export interface RouteInitialRequest {
|
|||||||
with_profile?: boolean;
|
with_profile?: boolean;
|
||||||
/** [재계산] — 이 입력의 저장본을 지우고 새로 셈(421 · 서버 420). */
|
/** [재계산] — 이 입력의 저장본을 지우고 새로 셈(421 · 서버 420). */
|
||||||
recompute?: boolean;
|
recompute?: boolean;
|
||||||
|
/** 창에서 고른 지형(509-5) — 지형마다 미리 셈 저장본 · 안 보내면 설계 지형. */
|
||||||
|
terrain?: AnalysisTerrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 서버 길이 아직 없다(404·405) — 화면은 이 글로 알린다. */
|
/** 서버 길이 아직 없다(404·405) — 화면은 이 글로 알린다. */
|
||||||
@@ -60,7 +62,7 @@ export async function fetchRouteInitial(
|
|||||||
`/projects/${projectId}/route/initial`,
|
`/projects/${projectId}/route/initial`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify({ ...request, terrain: analysisTerrain(projectId) }),
|
body: JSON.stringify(request),
|
||||||
},
|
},
|
||||||
INITIAL_TIMEOUT_MS,
|
INITIAL_TIMEOUT_MS,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { type DtmGrid, parseDtmGrid } from "../common_util/common_util_surface_s
|
|||||||
import type { GroundStageDeps } from "./logic/B05_Profile_Pipeline_RouteGround";
|
import type { GroundStageDeps } from "./logic/B05_Profile_Pipeline_RouteGround";
|
||||||
|
|
||||||
export interface DesignTerrainGrid {
|
export interface DesignTerrainGrid {
|
||||||
/** 이 격자의 지형 — 편집 창 분석 지형이 이것과 같을 때만 씀. */
|
/** 이 격자의 지형 — 계산 묶음 노선 초안 지형(null = 설계)이 이것과 같을 때만 씀. */
|
||||||
design: "las" | "sheet";
|
design: "las" | "sheet";
|
||||||
grid: DtmGrid;
|
grid: DtmGrid;
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,12 @@ export function loadDesignTerrainGrid(projectId: string): Promise<DesignTerrainG
|
|||||||
return pending;
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 설계 지형이 바뀜(추천 [적용] · 509-5) — 받아 둔 격자를 버림 · 다음 부름에 새 지형 격자를 받음. */
|
||||||
|
export function forgetDesignTerrainGrid(projectId: string): void {
|
||||||
|
loaded.delete(projectId);
|
||||||
|
ready.delete(projectId);
|
||||||
|
}
|
||||||
|
|
||||||
/** 지금 쓸 수 있는 격자 — 받는 중이면 null 을 돌려주고 받기를 띄움(기다리지 않음). */
|
/** 지금 쓸 수 있는 격자 — 받는 중이면 null 을 돌려주고 받기를 띄움(기다리지 않음). */
|
||||||
export function designTerrainGridNow(projectId: string): DesignTerrainGrid | null {
|
export function designTerrainGridNow(projectId: string): DesignTerrainGrid | null {
|
||||||
if (!ready.has(projectId)) void loadDesignTerrainGrid(projectId);
|
if (!ready.has(projectId)) void loadDesignTerrainGrid(projectId);
|
||||||
|
|||||||
@@ -127,12 +127,11 @@ def is_default(
|
|||||||
terrain: Any,
|
terrain: Any,
|
||||||
saved: dict[str, Any] | None = None,
|
saved: dict[str, Any] | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""지킬 결과 — 기본값(빈 제약 · 50/25/25 · 설계 지형) 또는 저장된 제약(`initial_choice`) 그대로.
|
"""지킬 결과 — 기본값(빈 제약 · 50/25/25) 또는 저장된 제약(`initial_choice`) 그대로 · 지형은
|
||||||
|
안 따짐(지형마다 미리 셈 · 509-5 · `terrain` 은 부르는 쪽 꼴 맞춤).
|
||||||
|
|
||||||
모달이 여는 값이라 「최근 2벌」 정리에서 따로 센다(351-1 · 사용자 시도에 안 밀림).
|
모달이 여는 값이라 「최근 2벌」 정리에서 따로 센다(351-1 · 사용자 시도에 안 밀림).
|
||||||
"""
|
"""
|
||||||
if terrain is not None:
|
|
||||||
return False
|
|
||||||
filled = {key: value for key, value in (criteria or {}).items() if value is not None}
|
filled = {key: value for key, value in (criteria or {}).items() if value is not None}
|
||||||
used = resolve_weights(mode, weights)
|
used = resolve_weights(mode, weights)
|
||||||
if not filled and (mode != "weighted" or used == DEFAULT_WEIGHTS):
|
if not filled and (mode != "weighted" or used == DEFAULT_WEIGHTS):
|
||||||
|
|||||||
@@ -637,7 +637,7 @@ async def _save_route(project_id: UUID, body: RouteSaveRequest) -> dict[str, Any
|
|||||||
if request.balanced_profile and as_initial
|
if request.balanced_profile and as_initial
|
||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
request.terrain,
|
request.terrain if as_initial else None, # 지형 바꿈 = 추천 [적용]만(509-5)
|
||||||
(old_line, await asyncio.to_thread(_vertices_of, working_path)),
|
(old_line, await asyncio.to_thread(_vertices_of, working_path)),
|
||||||
as_initial,
|
as_initial,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import functools
|
import functools
|
||||||
|
import itertools
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@@ -26,7 +27,7 @@ logger = logging.getLogger(__name__)
|
|||||||
# 미리 셈 진행(364-4) — 프로젝트마다 하나만 돎 · 도는 동안 또 오면 끝난 뒤 한 번 더(입력이 바뀜)
|
# 미리 셈 진행(364-4) — 프로젝트마다 하나만 돎 · 도는 동안 또 오면 끝난 뒤 한 번 더(입력이 바뀜)
|
||||||
_RUNNING: dict[str, dict[str, Any]] = {}
|
_RUNNING: dict[str, dict[str, Any]] = {}
|
||||||
# 미리 셈 갈래인데 저장본이 없을 때(505-1) — 프로젝트 접근은 셈하지 않음
|
# 미리 셈 갈래인데 저장본이 없을 때(505-1) — 프로젝트 접근은 셈하지 않음
|
||||||
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다."
|
NOT_PREPARED = "추천 저장본이 없습니다 — 전처리를 다시 하면 생깁니다 · [재계산]이면 지금 셉니다."
|
||||||
|
|
||||||
|
|
||||||
@replan.router.post("/{project_id}/route/initial", response_model=None)
|
@replan.router.post("/{project_id}/route/initial", response_model=None)
|
||||||
@@ -249,7 +250,8 @@ async def project_standard(project_id: UUID) -> dict[str, Any] | None:
|
|||||||
|
|
||||||
|
|
||||||
async def precompute_defaults(project_id: UUID) -> None:
|
async def precompute_defaults(project_id: UUID) -> None:
|
||||||
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯.
|
"""체인 끝 예약(PLAN 327-2 · 351-1) — 기본 다섯 알약 + 저장된 제약(initial_choice)의 다섯 ×
|
||||||
|
고를 수 있는 지형(509-5 · 설계 지형 먼저 · 수치지형도는 DB 모델 있을 때만).
|
||||||
|
|
||||||
모달은 저장된 제약 · 비중으로 열리므로 그 값으로도 미리 셈한다. 있으면 꺼내기만.
|
모달은 저장된 제약 · 비중으로 열리므로 그 값으로도 미리 셈한다. 있으면 꺼내기만.
|
||||||
다섯 갈래 모두 종단 켬(420) · 첫머리에 30일 지난 다른 판 · 소스 지문 저장본을 지운다(420-2).
|
다섯 갈래 모두 종단 켬(420) · 첫머리에 30일 지난 다른 판 · 소스 지문 저장본을 지운다(420-2).
|
||||||
@@ -260,11 +262,13 @@ async def precompute_defaults(project_id: UUID) -> None:
|
|||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
variants: list[tuple[Any, Any]] = [(None, None)]
|
variants: list[tuple[Any, Any]] = [(None, None)]
|
||||||
modes = list(DEFAULT_MODES)
|
modes = list(DEFAULT_MODES)
|
||||||
|
terrains: list[Any] = [None]
|
||||||
try:
|
try:
|
||||||
paths = await replan._project_paths(project_id)
|
paths = await replan._project_paths(project_id)
|
||||||
except LookupError:
|
except LookupError:
|
||||||
paths = None
|
paths = None
|
||||||
if paths is not None:
|
if paths is not None:
|
||||||
|
terrains = await precompute_terrains(project_id, paths[0])
|
||||||
removed = await asyncio.to_thread(purge_outdated, paths[0])
|
removed = await asyncio.to_thread(purge_outdated, paths[0])
|
||||||
if removed:
|
if removed:
|
||||||
logger.info("추천 옛 판 저장본 지움: project_id=%s %d", project_id, removed)
|
logger.info("추천 옛 판 저장본 지움: project_id=%s %d", project_id, removed)
|
||||||
@@ -276,29 +280,51 @@ async def precompute_defaults(project_id: UUID) -> None:
|
|||||||
modes.sort(key=lambda mode: mode != saved["logic"] or mode == "follow")
|
modes.sort(key=lambda mode: mode != saved["logic"] or mode == "follow")
|
||||||
progress = _RUNNING.get(str(project_id))
|
progress = _RUNNING.get(str(project_id))
|
||||||
if progress is not None:
|
if progress is not None:
|
||||||
progress.update(total=len(variants) * len(modes))
|
progress.update(total=len(terrains) * len(variants) * len(modes))
|
||||||
for criteria, weights in variants:
|
for terrain, (criteria, weights), mode in itertools.product(terrains, variants, modes):
|
||||||
for mode in modes:
|
if progress is not None:
|
||||||
if progress is not None:
|
progress["mode"] = mode
|
||||||
progress["mode"] = mode
|
# 화면과 같은 몸통 — 창 지형 · 비중은 비중 갈래만 · 「종단 반영」 기본 켬(420)
|
||||||
# 화면과 같은 몸통 — 설계 지형 · 비중은 비중 갈래만 · 「종단 반영」 기본 켬(420)
|
request = replan.RouteInitialRequest(
|
||||||
request = replan.RouteInitialRequest(
|
mode=mode,
|
||||||
mode=mode,
|
with_profile=True,
|
||||||
with_profile=True,
|
criteria=criteria,
|
||||||
criteria=criteria,
|
weights=weights if mode == "weighted" else None,
|
||||||
weights=weights if mode == "weighted" else None,
|
terrain=terrain,
|
||||||
|
)
|
||||||
|
outcome = await compute_initial(project_id, request, precompute=True)
|
||||||
|
if isinstance(outcome, JSONResponse):
|
||||||
|
logger.warning(
|
||||||
|
"추천 미리 셈 멈춤(%s %s %s): project_id=%s",
|
||||||
|
terrain,
|
||||||
|
mode,
|
||||||
|
outcome.status_code,
|
||||||
|
project_id,
|
||||||
)
|
)
|
||||||
outcome = await compute_initial(project_id, request, precompute=True)
|
return
|
||||||
if isinstance(outcome, JSONResponse):
|
if progress is not None:
|
||||||
logger.warning(
|
progress["done"] += 1
|
||||||
"추천 미리 셈 멈춤(%s %s): project_id=%s", mode, outcome.status_code, project_id
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if progress is not None:
|
|
||||||
progress["done"] += 1
|
|
||||||
logger.info("추천 미리 셈 끝: project_id=%s %.1fs", project_id, time.perf_counter() - started)
|
logger.info("추천 미리 셈 끝: project_id=%s %.1fs", project_id, time.perf_counter() - started)
|
||||||
|
|
||||||
|
|
||||||
|
async def precompute_terrains(project_id: UUID, project_root: Path) -> list[Any]:
|
||||||
|
"""미리 셈 지형(509-5) — 설계 지형 먼저 · 추천 창이 고를 수 있는 다른 지형."""
|
||||||
|
from B05_Profile.B05_Profile_Router_Terrain import _MODELS_SUBDIR, choosable_terrains
|
||||||
|
from common_util.common_util_surface_confirmation import (
|
||||||
|
design_terrain,
|
||||||
|
get_surface_confirmation_params,
|
||||||
|
)
|
||||||
|
|
||||||
|
pool = get_db_pool()
|
||||||
|
async with pool.acquire() as connection:
|
||||||
|
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||||
|
sources = await choosable_terrains(
|
||||||
|
connection, project_id, params, Path(project_root) / _MODELS_SUBDIR
|
||||||
|
)
|
||||||
|
design = design_terrain(params)
|
||||||
|
return [design, *[source for source in sources if source != design]]
|
||||||
|
|
||||||
|
|
||||||
async def run_precompute(project_id: UUID) -> None:
|
async def run_precompute(project_id: UUID) -> None:
|
||||||
"""미리 셈 한 줄(364-4) — 이미 돌면 「한 번 더」만 적고 돌아감(겹쳐 셈 막음)."""
|
"""미리 셈 한 줄(364-4) — 이미 돌면 「한 번 더」만 적고 돌아감(겹쳐 셈 막음)."""
|
||||||
key = str(project_id)
|
key = str(project_id)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
GET /api/projects/{id}/surface/models/{mid}/grid → DTM 격자 원시 이진(브라우저 표본기 · ETag)
|
GET /api/projects/{id}/surface/models/{mid}/grid → DTM 격자 원시 이진(브라우저 표본기 · ETag)
|
||||||
|
|
||||||
분석 지형(`terrain`, PLAN 133-1) — las = 라이다 · sheet = 수치지형도 · None = 지금 설계 지형.
|
분석 지형(`terrain`, PLAN 133-1) — las = 라이다 · sheet = 수치지형도 · None = 지금 설계 지형.
|
||||||
모달 분석만 고른 지형을 쓴다 — 설계 지형(확정)은 [확인] 때만 바뀜(133-2 · `/route/replan`).
|
고르기는 추천 노선 창 안(509-5) — 설계 지형(확정)은 추천 [적용] 때만 바뀜(`/route/replan`).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -116,11 +116,7 @@ async def read_route_elevations(project_id: UUID, request: ElevationRequest) ->
|
|||||||
|
|
||||||
@router.get("/{project_id}/route/terrain-sources", response_model=None)
|
@router.get("/{project_id}/route/terrain-sources", response_model=None)
|
||||||
async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
||||||
"""편집 모달이 고를 수 있는 분석 지형 — 둘이면 고르기를 띄움 · 하나면 숨김.
|
"""추천 창이 고를 수 있는 분석 지형(509-5 · 둘이면 고르기) · 설계 지형 · 설계 지형 격자 자리."""
|
||||||
|
|
||||||
설계 지형 아닌 쪽은 DB 완료 모델이 있어야 둠 — [확인]이 그 모델로 바꾸므로(505-2 5d)."""
|
|
||||||
from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection
|
|
||||||
|
|
||||||
pool = get_db_pool()
|
pool = get_db_pool()
|
||||||
async with pool.acquire() as connection:
|
async with pool.acquire() as connection:
|
||||||
stored = await get_project_storage_relative_path(connection, project_id)
|
stored = await get_project_storage_relative_path(connection, project_id)
|
||||||
@@ -132,14 +128,7 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
|||||||
params = await get_surface_confirmation_params(connection, str(project_id))
|
params = await get_surface_confirmation_params(connection, str(project_id))
|
||||||
confirmed = await _confirmed_model(connection, project_id)
|
confirmed = await _confirmed_model(connection, project_id)
|
||||||
models_dir = Path(resolve_stored_project_path(stored)) / _MODELS_SUBDIR
|
models_dir = Path(resolve_stored_project_path(stored)) / _MODELS_SUBDIR
|
||||||
sources = await asyncio.to_thread(analysis_terrain_sources, params, models_dir)
|
sources = await choosable_terrains(connection, project_id, params, models_dir)
|
||||||
for source in [s for s in sources if s != design_terrain(params)]:
|
|
||||||
try:
|
|
||||||
await find_surface_model_for_selection(
|
|
||||||
connection, project_id, analysis_terrain_params(params, source)
|
|
||||||
)
|
|
||||||
except LookupError:
|
|
||||||
sources.remove(source)
|
|
||||||
return {
|
return {
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"project_id": str(project_id),
|
"project_id": str(project_id),
|
||||||
@@ -149,6 +138,24 @@ async def read_terrain_sources(project_id: UUID) -> dict | JSONResponse:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def choosable_terrains(
|
||||||
|
connection: aiomysql.Connection, project_id: UUID, params: dict, models_dir: Path
|
||||||
|
) -> list[str]:
|
||||||
|
"""고를 수 있는 분석 지형 — 설계 지형 아닌 쪽은 DB 완료 모델이 있어야 둠(505-2 5d) · 추천 창
|
||||||
|
고르기 · 지형마다 추천 미리 셈(509-5)이 같이 씀."""
|
||||||
|
from B04_PreProcess.B04_PreProcess_Service import find_surface_model_for_selection
|
||||||
|
|
||||||
|
sources = await asyncio.to_thread(analysis_terrain_sources, params, models_dir)
|
||||||
|
for source in [s for s in sources if s != design_terrain(params)]:
|
||||||
|
try:
|
||||||
|
await find_surface_model_for_selection(
|
||||||
|
connection, project_id, analysis_terrain_params(params, source)
|
||||||
|
)
|
||||||
|
except LookupError:
|
||||||
|
sources.remove(source)
|
||||||
|
return sources
|
||||||
|
|
||||||
|
|
||||||
async def _confirmed_model(
|
async def _confirmed_model(
|
||||||
connection: aiomysql.Connection, project_id: UUID
|
connection: aiomysql.Connection, project_id: UUID
|
||||||
) -> tuple[int, str, str] | None:
|
) -> tuple[int, str, str] | None:
|
||||||
|
|||||||
@@ -31,11 +31,8 @@ import {
|
|||||||
import { createContourLock } from "./B05_Profile_UI_RouteEdit_ContourLock";
|
import { createContourLock } from "./B05_Profile_UI_RouteEdit_ContourLock";
|
||||||
import { fetchDrainageLayers } from "./B05_Profile_UI_Drainage_Parts";
|
import { fetchDrainageLayers } from "./B05_Profile_UI_Drainage_Parts";
|
||||||
import {
|
import {
|
||||||
analysisTerrain,
|
|
||||||
fetchRoutePlan,
|
fetchRoutePlan,
|
||||||
fetchTerrainSources,
|
fetchTerrainSources,
|
||||||
setAnalysisTerrain,
|
|
||||||
setTerrainContext,
|
|
||||||
type AnalysisTerrain,
|
type AnalysisTerrain,
|
||||||
} from "./B05_Profile_Api_Replan";
|
} from "./B05_Profile_Api_Replan";
|
||||||
import { bindRouteApply } from "./B05_Profile_UI_RouteEdit_Apply";
|
import { bindRouteApply } from "./B05_Profile_UI_RouteEdit_Apply";
|
||||||
@@ -116,7 +113,7 @@ export async function mountRouteEditor(
|
|||||||
const stationIntervalM = options.stationIntervalM ?? 20;
|
const stationIntervalM = options.stationIntervalM ?? 20;
|
||||||
const chrome = createRouteEditChrome(parent, options.tools);
|
const chrome = createRouteEditChrome(parent, options.tools);
|
||||||
const { overlay, canvas, status, measureBox, measureText, measureButton } = chrome;
|
const { overlay, canvas, status, measureBox, measureText, measureButton } = chrome;
|
||||||
const { minorContourButton, terrainSelect, stationSnapButton } = chrome;
|
const { minorContourButton, terrainName, stationSnapButton } = chrome;
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
|
|
||||||
let expected: Vertex[] = [];
|
let expected: Vertex[] = [];
|
||||||
@@ -161,11 +158,10 @@ export async function mountRouteEditor(
|
|||||||
let meta: VWorldMeta | null = null;
|
let meta: VWorldMeta | null = null;
|
||||||
/** 바탕 등고선 한 벌 — LAS 것이거나 도엽 것. 고르기는 `_Contour` 몫. */
|
/** 바탕 등고선 한 벌 — LAS 것이거나 도엽 것. 고르기는 `_Contour` 몫. */
|
||||||
let contours: RouteEditContours | null = null;
|
let contours: RouteEditContours | null = null;
|
||||||
/** 지금 설계 지형(PLAN 133-2) — 고른 분석 지형이 이와 다르면 [확인]이 설계 지형을 바꾼다. */
|
/** 지금 설계 지형 — 편집 분석이 쓰는 지형 · 바꾸기는 추천 창 [적용](509-5). */
|
||||||
let designTerrain: AnalysisTerrain = "las";
|
let designTerrain: AnalysisTerrain = "las";
|
||||||
/** [확인]에 실을 분석 지형 — 고르기가 안 보이면 null(설계 지형 그대로). */
|
/** 추천 창이 고를 지형 — `/route/terrain-sources` 를 읽은 뒤. */
|
||||||
const pickedTerrain = (): AnalysisTerrain | null =>
|
let terrainSources: { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null = null;
|
||||||
terrainSelect.closest("label")!.hidden ? null : analysisTerrain(projectId);
|
|
||||||
/** 등고선 말고 함께 깔 도엽 레이어(하천중심선). */
|
/** 등고선 말고 함께 깔 도엽 레이어(하천중심선). */
|
||||||
let otherSheets: PreparedLayer[] = [];
|
let otherSheets: PreparedLayer[] = [];
|
||||||
/** 고른 등고선 가닥 — 없으면 -1(계획서 0-9 ⑦). */
|
/** 고른 등고선 가닥 — 없으면 -1(계획서 0-9 ⑦). */
|
||||||
@@ -267,6 +263,7 @@ export async function mountRouteEditor(
|
|||||||
planned: () => planned,
|
planned: () => planned,
|
||||||
planMinRadiusM: () => minRadiusM,
|
planMinRadiusM: () => minRadiusM,
|
||||||
apply: (snapshot, message) => restoreSnapshot(snapshot, message, true),
|
apply: (snapshot, message) => restoreSnapshot(snapshot, message, true),
|
||||||
|
terrains: () => terrainSources,
|
||||||
});
|
});
|
||||||
|
|
||||||
function resize(): void {
|
function resize(): void {
|
||||||
@@ -447,8 +444,8 @@ export async function mountRouteEditor(
|
|||||||
historyControls.sync();
|
historyControls.sync();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 계산 묶음에 알림 — 노선 · 분석 지형 · 스냅이 저장분과 다르면 초안 · 같으면 null(352-3 · 430 · 433-14 ②). */
|
/** 계산 묶음에 알림 — 노선 · 스냅이 저장분과 다르면 초안 · 같으면 null(430 · 433-14 ②). */
|
||||||
const otherChanged = (): boolean => pickedTerrain() !== designTerrain || snap.changed();
|
const otherChanged = (): boolean => snap.changed();
|
||||||
function tellDraft(): void {
|
function tellDraft(): void {
|
||||||
if (!history) return;
|
if (!history) return;
|
||||||
const dirty = lineChanged(history) || otherChanged();
|
const dirty = lineChanged(history) || otherChanged();
|
||||||
@@ -587,9 +584,7 @@ export async function mountRouteEditor(
|
|||||||
const unbindApply = bindRouteApply({
|
const unbindApply = bindRouteApply({
|
||||||
projectId,
|
projectId,
|
||||||
nodes: () => ({ planned, curveOn, curveRadius, curveLock, curveArc }),
|
nodes: () => ({ planned, curveOn, curveRadius, curveLock, curveArc }),
|
||||||
// 분석 지형만 바꿔도 [확인]이 설계 지형을 바꿔 다시 셈한다(PLAN 133-2).
|
|
||||||
changed: () => (!!history && lineChanged(history)) || otherChanged(),
|
changed: () => (!!history && lineChanged(history)) || otherChanged(),
|
||||||
terrain: pickedTerrain,
|
|
||||||
snap: snap.draft,
|
snap: snap.draft,
|
||||||
initial,
|
initial,
|
||||||
onApplied,
|
onApplied,
|
||||||
@@ -633,21 +628,16 @@ export async function mountRouteEditor(
|
|||||||
syncCurveBar();
|
syncCurveBar();
|
||||||
if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]);
|
if (!planned.length) planned = plannedLine.map((vertex) => [vertex[0], vertex[1]]);
|
||||||
meta = drainage.meta;
|
meta = drainage.meta;
|
||||||
// 분석 지형 고르기(PLAN 133-1) — 라이다 · 수치지형도 둘 다 있을 때만 보임 · 열 때 안 씀(352-3).
|
// 설계 지형 이름만(509-5) — 고르기는 추천 창 안 · 둘 다 있을 때만 이름을 보임.
|
||||||
// 고른 적 없으면 데이터대로(설계 지형 · 라이다가 있으면 라이다) · 사용자가 바꿀 때만 기억.
|
|
||||||
designTerrain = sources?.design ?? "las";
|
designTerrain = sources?.design ?? "las";
|
||||||
const choosable = !!sources?.sources.includes("las") && !!sources?.sources.includes("sheet");
|
terrainSources = sources;
|
||||||
terrainSelect.closest("label")!.hidden = !choosable;
|
terrainName.hidden = !(sources?.sources.includes("las") && sources.sources.includes("sheet"));
|
||||||
setTerrainContext(projectId, choosable, designTerrain);
|
terrainName.textContent = `지형 ${designTerrain === "sheet" ? "수치지형도" : "라이다"}`;
|
||||||
terrainSelect.value = analysisTerrain(projectId);
|
|
||||||
// 등고선은 따로 고른다(LAS 우선 · 수치지형도를 고르면 도엽). 나머지 도엽 레이어(하천중심선)만 배경으로 깐다.
|
// 등고선은 따로 고른다(LAS 우선 · 수치지형도를 고르면 도엽). 나머지 도엽 레이어(하천중심선)만 배경으로 깐다.
|
||||||
// 확정 모델 등고선은 설계 지형이 라이다일 때만 라이다 것 — 수치지형도 설계면 도엽 선(글도 도엽).
|
// 확정 모델 등고선은 설계 지형이 라이다일 때만 라이다 것 — 수치지형도 설계면 도엽 선(글도 도엽).
|
||||||
const loadBackground = () =>
|
const loadBackground = () =>
|
||||||
loadRouteEditBackground(projectId, drainage, {
|
loadRouteEditBackground(projectId, drainage, {
|
||||||
surfaceModelId:
|
surfaceModelId: designTerrain === "las" ? (options.surfaceModelId ?? null) : null,
|
||||||
analysisTerrain(projectId) === "las" && designTerrain === "las"
|
|
||||||
? (options.surfaceModelId ?? null)
|
|
||||||
: null,
|
|
||||||
intervalM: options.contourIntervalM ?? 1,
|
intervalM: options.contourIntervalM ?? 1,
|
||||||
smooth: options.smooth ?? false,
|
smooth: options.smooth ?? false,
|
||||||
});
|
});
|
||||||
@@ -670,21 +660,6 @@ export async function mountRouteEditor(
|
|||||||
};
|
};
|
||||||
useBackground(await loadBackground());
|
useBackground(await loadBackground());
|
||||||
if (closed) return;
|
if (closed) return;
|
||||||
// 고르면 모달 안 분석을 고른 지형으로 다시 그림 — 등고선 · 기울기 · 종단 띠 · 횡단 · 잰 값.
|
|
||||||
let terrainTicket = 0;
|
|
||||||
terrainSelect.addEventListener("change", () => {
|
|
||||||
setAnalysisTerrain(projectId, terrainSelect.value === "sheet" ? "sheet" : "las");
|
|
||||||
tellDraft(); // 노선이 그대로여도 지형이 설계와 다르면 초안으로 다시 셈(352-3).
|
|
||||||
const mine = ++terrainTicket;
|
|
||||||
void loadBackground().then((background) => {
|
|
||||||
if (closed || mine !== terrainTicket) return;
|
|
||||||
useBackground(background);
|
|
||||||
measure.clear();
|
|
||||||
slope.refresh();
|
|
||||||
showStatus();
|
|
||||||
draw();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
viewWanted = true;
|
viewWanted = true;
|
||||||
resize(); // 칸 크기가 서 있으면 여기서 보던 화면을 연다(아니면 처음 보일 때).
|
resize(); // 칸 크기가 서 있으면 여기서 보던 화면을 연다(아니면 처음 보일 때).
|
||||||
showStatus();
|
showStatus();
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
* `route` 칸 = `RouteSaveBody`)에 싣는다 — 노선이 마지막 저장 그대로면 null(안 보냄 · 472-6 6-B).
|
* `route` 칸 = `RouteSaveBody`)에 싣는다 — 노선이 마지막 저장 그대로면 null(안 보냄 · 472-6 6-B).
|
||||||
* · 서버가 노선 저장 → 관 · 구조물 다시 얹기 → 세션 횡단 고침 넘김(`routeCarryPart` 가 낸 옛 노선 칸 · 같은 TS)까지
|
* · 서버가 노선 저장 → 관 · 구조물 다시 얹기 → 세션 횡단 고침 넘김(`routeCarryPart` 가 낸 옛 노선 칸 · 같은 TS)까지
|
||||||
* 한 번에 · 다 저장한 뒤 `routeSaved` · 페이지는 새로고침 없이 다시 세운다(`rebuildAfterRouteSave`).
|
* 한 번에 · 다 저장한 뒤 `routeSaved` · 페이지는 새로고침 없이 다시 세운다(`rebuildAfterRouteSave`).
|
||||||
* · 추천 [적용] = `saveRecommendedRoute` — `as_initial: true` 로 바로 저장(초기값 = 추천 노선 · 사용자 확정).
|
* · 추천 [적용] = `saveRecommendedRoute` — `as_initial: true` 로 바로 저장(초기값 = 추천 노선 · 사용자 확정) ·
|
||||||
|
* 창에서 고른 지형이 설계 지형(509-5 · 손 [저장]은 지형 안 바꿈).
|
||||||
* 편집기가 살아 있는 동안 여기 등록부에 「지금 초안」 · 「다시 세우기」를 건다(프로젝트 하나에 하나).
|
* 편집기가 살아 있는 동안 여기 등록부에 「지금 초안」 · 「다시 세우기」를 건다(프로젝트 하나에 하나).
|
||||||
* 실패하면 서버가 옛 노선으로 되살리고 화면 초안은 그대로 남는다(오류 한 줄은 부르는 쪽).
|
* 실패하면 서버가 옛 노선으로 되살리고 화면 초안은 그대로 남는다(오류 한 줄은 부르는 쪽).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
@@ -30,6 +31,7 @@ import {
|
|||||||
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
import { balloonOffsetsPayload } from "@util/common_util_mass_haul_balance_view";
|
||||||
import type { SavePart } from "../A00_Common/design/design_save_api";
|
import type { SavePart } from "../A00_Common/design/design_save_api";
|
||||||
import { clearRouteLatestCache } from "./B05_Profile_Api_Fetch";
|
import { clearRouteLatestCache } from "./B05_Profile_Api_Fetch";
|
||||||
|
import { forgetDesignTerrainGrid } from "./B05_Profile_Api_TerrainGrid";
|
||||||
import { reloadSavedPlusStations } from "./B05_Profile_Api_PlusStations";
|
import { reloadSavedPlusStations } from "./B05_Profile_Api_PlusStations";
|
||||||
import {
|
import {
|
||||||
replanRoute,
|
replanRoute,
|
||||||
@@ -55,8 +57,6 @@ export interface RouteApplyParams {
|
|||||||
nodes: () => RouteNodes;
|
nodes: () => RouteNodes;
|
||||||
/** 마지막 저장 뒤 노선이 바뀌었나 — 안 바뀌었으면 보내지 않는다. */
|
/** 마지막 저장 뒤 노선이 바뀌었나 — 안 바뀌었으면 보내지 않는다. */
|
||||||
changed: () => boolean;
|
changed: () => boolean;
|
||||||
/** 설계 지형으로 보낼 분석 지형(PLAN 133-2) — 고르기가 안 보이면 null(지금 설계 지형 그대로). */
|
|
||||||
terrain: () => string | null;
|
|
||||||
/** [측점 스냅] 켬끔(요구 224-2) · 간격(390-5) — 곡선표에 적혀 측점 셈이 읽음. */
|
/** [측점 스냅] 켬끔(요구 224-2) · 간격(390-5) — 곡선표에 적혀 측점 셈이 읽음. */
|
||||||
snap: () => SnapSetting;
|
snap: () => SnapSetting;
|
||||||
/** 초기노선 갈래 — 저장에 지금 추천을 싣고, 결과 캐시를 비운 뒤 다시 적는다. */
|
/** 초기노선 갈래 — 저장에 지금 추천을 싣고, 결과 캐시를 비운 뒤 다시 적는다. */
|
||||||
@@ -95,7 +95,8 @@ export function routeDraftOf(nodes: RouteNodes, settings: RouteSettings): RouteD
|
|||||||
/** 편집기를 등록부에 건다 — 같은 프로젝트에 새 편집기가 서면 그쪽으로 바뀐다. 돌려준 함수 = 편집기가 떠날 때 풂
|
/** 편집기를 등록부에 건다 — 같은 프로젝트에 새 편집기가 서면 그쪽으로 바뀐다. 돌려준 함수 = 편집기가 떠날 때 풂
|
||||||
* (안 풀면 노선 저장 뒤 다시 선 페이지에서 옛 편집기 「고친 것 있음」이 [저장] · [확정]마다 노선 저장을 다시 돌림 · 445-2). */
|
* (안 풀면 노선 저장 뒤 다시 선 페이지에서 옛 편집기 「고친 것 있음」이 [저장] · [확정]마다 노선 저장을 다시 돌림 · 445-2). */
|
||||||
export function bindRouteApply(params: RouteApplyParams): () => void {
|
export function bindRouteApply(params: RouteApplyParams): () => void {
|
||||||
const settings = () => ({ ...params.snap(), terrain: params.terrain() });
|
// 손으로 고친 노선 = 설계 지형 그대로 — 지형 바꿈은 추천 [적용]만(509-5)
|
||||||
|
const settings = () => ({ ...params.snap(), terrain: null });
|
||||||
const source: RouteSource = {
|
const source: RouteSource = {
|
||||||
pending: async () => {
|
pending: async () => {
|
||||||
const nodes = params.nodes();
|
const nodes = params.nodes();
|
||||||
@@ -191,18 +192,26 @@ export async function saveRecommendedRoute(
|
|||||||
nodes: RouteNodes,
|
nodes: RouteNodes,
|
||||||
initial: unknown,
|
initial: unknown,
|
||||||
balancedProfile: boolean,
|
balancedProfile: boolean,
|
||||||
|
terrain: string | null = null,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const settings = sources.get(projectId)?.settings() ?? { stationSnap: false, terrain: null };
|
const settings = sources.get(projectId)?.settings() ?? { stationSnap: false, terrain: null };
|
||||||
showLoadingOverlay();
|
showLoadingOverlay();
|
||||||
try {
|
try {
|
||||||
const saved = await saveRouteDraft(projectId, {
|
const saved = await saveRouteDraft(projectId, {
|
||||||
// 383-1 — 추천 선은 곡선 MC 를 일반측점에 맞춘 선 → [측점 스냅] 켬 · 측점 No.(390-5)로 저장
|
// 383-1 — 추천 선은 곡선 MC 를 일반측점에 맞춘 선 → [측점 스냅] 켬 · 측점 No.(390-5)로 저장
|
||||||
draft: routeDraftOf(nodes, { ...settings, stationSnap: true, stationSnapStep: null }),
|
// 창에서 고른 지형 = 설계 지형(509-5 · 서버 `switch_design_terrain`)
|
||||||
|
draft: routeDraftOf(nodes, {
|
||||||
|
...settings,
|
||||||
|
stationSnap: true,
|
||||||
|
stationSnapStep: null,
|
||||||
|
terrain,
|
||||||
|
}),
|
||||||
initial,
|
initial,
|
||||||
balanced_profile: balancedProfile,
|
balanced_profile: balancedProfile,
|
||||||
as_initial: true,
|
as_initial: true,
|
||||||
});
|
});
|
||||||
clearDrafts(projectId);
|
clearDrafts(projectId);
|
||||||
|
forgetDesignTerrainGrid(projectId); // 지형이 바뀌었으면 새 지형 격자로(509-2)
|
||||||
await reloadSavedPlusStations(projectId); // 페이지를 다시 세워도 남는 옛 저장분(493-5)
|
await reloadSavedPlusStations(projectId); // 페이지를 다시 세워도 남는 옛 저장분(493-5)
|
||||||
sources.get(projectId)?.saved();
|
sources.get(projectId)?.saved();
|
||||||
const reset = routeResetNotice(saved.route_reset, saved.pipes?.crossing ?? 0);
|
const reset = routeResetNotice(saved.route_reset, saved.pipes?.crossing ?? 0);
|
||||||
|
|||||||
@@ -58,7 +58,8 @@ export interface RouteEditChrome {
|
|||||||
/** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */
|
/** [측점 스냅] 켬끔(요구 224-2) — `aria-pressed` · `is-active` 는 본체가 맞춤. */
|
||||||
stationSnapButton: HTMLButtonElement;
|
stationSnapButton: HTMLButtonElement;
|
||||||
/** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */
|
/** 분석 지형 고르기(PLAN 133-1) — 라이다 있는 프로젝트만 보임(감싼 label 의 hidden). */
|
||||||
terrainSelect: HTMLSelectElement;
|
/** 설계 지형 이름(509-5) — 고르기는 추천 창 안. */
|
||||||
|
terrainName: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 손잡이 자리(24) + 틈(8) — 접혀도 남는 폭(`.ui-side-panels` margin-left). */
|
/** 손잡이 자리(24) + 틈(8) — 접혀도 남는 폭(`.ui-side-panels` margin-left). */
|
||||||
@@ -119,13 +120,8 @@ export function createRouteEditChrome(
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<i class="b05-route__view-separator" aria-hidden="true"></i>
|
<i class="b05-route__view-separator" aria-hidden="true"></i>
|
||||||
<label class="b05-routeedit__terrain" hidden
|
<span class="b05-routeedit__terrain" data-slot="terrain-name" hidden
|
||||||
title="노선 편집 분석(등고선 · 지반고 · 기울기 · 횡단 미리보기 · 추천)에 쓸 지형 — 확정 지표면 · 종횡단은 안 바뀝니다">분석 지형
|
title="설계 지형 — 바꾸기는 [노선 추천] 창에서 지형을 골라 [적용]"></span>
|
||||||
<select class="ui-select ui-select--sm" data-act="terrain">
|
|
||||||
<option value="las">라이다</option>
|
|
||||||
<option value="sheet">수치지형도</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="b05-routeedit__hint">
|
<div class="b05-routeedit__hint">
|
||||||
<div class="b05-routeedit__hint-body">
|
<div class="b05-routeedit__hint-body">
|
||||||
@@ -253,7 +249,7 @@ export function createRouteEditChrome(
|
|||||||
const minorContourButton = overlay.querySelector<HTMLButtonElement>(
|
const minorContourButton = overlay.querySelector<HTMLButtonElement>(
|
||||||
'[data-act="contour-minor"]',
|
'[data-act="contour-minor"]',
|
||||||
)!;
|
)!;
|
||||||
const terrainSelect = overlay.querySelector<HTMLSelectElement>('[data-act="terrain"]')!;
|
const terrainName = overlay.querySelector<HTMLElement>('[data-slot="terrain-name"]')!;
|
||||||
const stationSnapButton = overlay.querySelector<HTMLButtonElement>('[data-act="station-snap"]')!;
|
const stationSnapButton = overlay.querySelector<HTMLButtonElement>('[data-act="station-snap"]')!;
|
||||||
bindFootprintToggle(overlay.querySelector<HTMLButtonElement>('[data-act="footprint"]')!);
|
bindFootprintToggle(overlay.querySelector<HTMLButtonElement>('[data-act="footprint"]')!);
|
||||||
bindMarkToggle(overlay.querySelector<HTMLButtonElement>('[data-act="stations"]')!, "stations");
|
bindMarkToggle(overlay.querySelector<HTMLButtonElement>('[data-act="stations"]')!, "stations");
|
||||||
@@ -269,6 +265,6 @@ export function createRouteEditChrome(
|
|||||||
measureButton,
|
measureButton,
|
||||||
minorContourButton,
|
minorContourButton,
|
||||||
stationSnapButton,
|
stationSnapButton,
|
||||||
terrainSelect,
|
terrainName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@
|
|||||||
* 요청했을 때 서버가 답으로 준 값(진짜 기본값)을 기억해 둔 것이다.
|
* 요청했을 때 서버가 답으로 준 값(진짜 기본값)을 기억해 둔 것이다.
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
|
|
||||||
import { fetchRoutePlan, type RoutePlanResponse } from "./B05_Profile_Api_Replan";
|
import {
|
||||||
|
fetchRoutePlan,
|
||||||
|
type AnalysisTerrain,
|
||||||
|
type RoutePlanResponse,
|
||||||
|
} from "./B05_Profile_Api_Replan";
|
||||||
import {
|
import {
|
||||||
fetchPrecomputeProgress,
|
fetchPrecomputeProgress,
|
||||||
fetchRouteInitial,
|
fetchRouteInitial,
|
||||||
@@ -57,6 +61,8 @@ export interface InitialPanelParams {
|
|||||||
planMinRadiusM: () => number;
|
planMinRadiusM: () => number;
|
||||||
/** 받은 노선을 편집값으로 싣는다 — 되돌리기 한 걸음으로 쌓인다. */
|
/** 받은 노선을 편집값으로 싣는다 — 되돌리기 한 걸음으로 쌓인다. */
|
||||||
apply: (snapshot: RouteEditSnapshot, message: string) => void;
|
apply: (snapshot: RouteEditSnapshot, message: string) => void;
|
||||||
|
/** 추천 창이 고를 지형 · 지금 설계 지형(509-5) — 못 읽었으면 null(설계 지형 하나). */
|
||||||
|
terrains: () => { sources: AnalysisTerrain[]; design: AnalysisTerrain } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InitialPanel {
|
export interface InitialPanel {
|
||||||
@@ -126,6 +132,7 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
|||||||
criteria: RouteInitialCriteria | null,
|
criteria: RouteInitialCriteria | null,
|
||||||
withProfile: boolean,
|
withProfile: boolean,
|
||||||
recompute: boolean,
|
recompute: boolean,
|
||||||
|
terrain: AnalysisTerrain,
|
||||||
): Promise<InitialChoice> {
|
): Promise<InitialChoice> {
|
||||||
const response = await fetchRouteInitial(projectId, {
|
const response = await fetchRouteInitial(projectId, {
|
||||||
mode: logic,
|
mode: logic,
|
||||||
@@ -133,8 +140,9 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
|||||||
...(criteria ? { criteria } : {}),
|
...(criteria ? { criteria } : {}),
|
||||||
with_profile: withProfile,
|
with_profile: withProfile,
|
||||||
...(recompute ? { recompute: true } : {}),
|
...(recompute ? { recompute: true } : {}),
|
||||||
|
terrain,
|
||||||
});
|
});
|
||||||
return normalizeRouteInitial(response, logic, weights, criteria, withProfile);
|
return { ...normalizeRouteInitial(response, logic, weights, criteria, withProfile), terrain };
|
||||||
}
|
}
|
||||||
|
|
||||||
recommendButton.addEventListener("click", () => {
|
recommendButton.addEventListener("click", () => {
|
||||||
@@ -158,6 +166,8 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
|||||||
expected: (plan?.expected ?? []).map(([x, y]) => [x, y]),
|
expected: (plan?.expected ?? []).map(([x, y]) => [x, y]),
|
||||||
current: params.planned().map(([x, y]) => [x, y]),
|
current: params.planned().map(([x, y]) => [x, y]),
|
||||||
request,
|
request,
|
||||||
|
terrains: params.terrains()?.sources ?? [],
|
||||||
|
terrain: params.terrains()?.design ?? "las",
|
||||||
progress: () => fetchPrecomputeProgress(projectId),
|
progress: () => fetchPrecomputeProgress(projectId),
|
||||||
confirmText: () => applyResetText(projectId),
|
confirmText: () => applyResetText(projectId),
|
||||||
onApply: (picked) => {
|
onApply: (picked) => {
|
||||||
@@ -179,6 +189,7 @@ export function createInitialPanel(params: InitialPanelParams): InitialPanel {
|
|||||||
with_profile: choiceWithProfile(picked), // initial_choice 에 남김(420)
|
with_profile: choiceWithProfile(picked), // initial_choice 에 남김(420)
|
||||||
},
|
},
|
||||||
choiceWithProfile(picked),
|
choiceWithProfile(picked),
|
||||||
|
picked.terrain ?? null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -215,6 +215,8 @@ export interface InitialChoice {
|
|||||||
/** 시종점 높이 차 ÷ 연장(%) · 상한을 넘나(종단으로 못 풂) — 서버 `ends_grade_pct` · `ends_over_limit`(419). */
|
/** 시종점 높이 차 ÷ 연장(%) · 상한을 넘나(종단으로 못 풂) — 서버 `ends_grade_pct` · `ends_over_limit`(419). */
|
||||||
endsGradePct?: number | null;
|
endsGradePct?: number | null;
|
||||||
endsOverLimit?: boolean;
|
endsOverLimit?: boolean;
|
||||||
|
/** 추천 창에서 고른 지형(509-5) — [적용]이 설계 지형으로. 옛 캐시면 없음(설계 지형 그대로). */
|
||||||
|
terrain?: "las" | "sheet";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 거의 같은 노선인 다른 갈래 하나 — 두 노선 사이 가장 먼 거리(m). */
|
/** 거의 같은 노선인 다른 갈래 하나 — 두 노선 사이 가장 먼 거리(m). */
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
* 서버 부르기 · 캐시 · 「현재 추천」은 `_Initial` 몫이고 여기는 화면만 만든다.
|
* 서버 부르기 · 캐시 · 「현재 추천」은 `_Initial` 몫이고 여기는 화면만 만든다.
|
||||||
* 균형 종단(계획서 63-4) — 후보마다 서버가 세운 종단의 한 줄 · 그림 · 셈한 후보 표는 이 모달
|
* 균형 종단(계획서 63-4) — 후보마다 서버가 세운 종단의 한 줄 · 그림 · 셈한 후보 표는 이 모달
|
||||||
* 안에서만 보인다(`_Recommend_Profile`) · 닫으면 버린다.
|
* 안에서만 보인다(`_Recommend_Profile`) · 닫으면 버린다.
|
||||||
|
* 지형 고르기(509-5 · 라이다 · 수치지형도 둘일 때) — 바꾸면 그 지형 미리 셈 저장본 · 없으면 안내 + [재계산] ·
|
||||||
|
* [적용]하면 그 지형이 설계 지형(초기값도 그 지형으로).
|
||||||
* 적용될 초기 종단(476) — 서버가 [적용]과 같은 코드로 세운 종단을 같은 그림에 선 · 한 줄로 더한다
|
* 적용될 초기 종단(476) — 서버가 [적용]과 같은 코드로 세운 종단을 같은 그림에 선 · 한 줄로 더한다
|
||||||
* (끔이어도) · [적용]은 「종단 · 횡단 손 입력이 모두 지워짐」 확인 뒤(요구 277).
|
* (끔이어도) · [적용]은 「종단 · 횡단 손 입력이 모두 지워짐」 확인 뒤(요구 277).
|
||||||
* ========================================================================== */
|
* ========================================================================== */
|
||||||
@@ -25,6 +27,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
} from "@ui/ui_template_elements";
|
} from "@ui/ui_template_elements";
|
||||||
import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
|
import { toggleButton } from "./B05_Profile_UI_Panel_Fields";
|
||||||
|
import type { AnalysisTerrain } from "./B05_Profile_Api_Replan";
|
||||||
import type {
|
import type {
|
||||||
PrecomputeProgress,
|
PrecomputeProgress,
|
||||||
RouteInitialCriteria,
|
RouteInitialCriteria,
|
||||||
@@ -125,6 +128,8 @@ const CRITERIA_FIELD: Record<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TERRAIN_NAME: Record<AnalysisTerrain, string> = { las: "라이다", sheet: "수치지형도" };
|
||||||
|
|
||||||
export interface RecommendParams {
|
export interface RecommendParams {
|
||||||
/** 칸에 처음 채울 제약 겹침(직접 넣은 값만). */
|
/** 칸에 처음 채울 제약 겹침(직접 넣은 값만). */
|
||||||
criteria: RouteInitialCriteria | null;
|
criteria: RouteInitialCriteria | null;
|
||||||
@@ -148,7 +153,11 @@ export interface RecommendParams {
|
|||||||
criteria: RouteInitialCriteria | null,
|
criteria: RouteInitialCriteria | null,
|
||||||
withProfile: boolean,
|
withProfile: boolean,
|
||||||
recompute: boolean,
|
recompute: boolean,
|
||||||
|
terrain: AnalysisTerrain,
|
||||||
) => Promise<InitialChoice>;
|
) => Promise<InitialChoice>;
|
||||||
|
/** 고를 수 있는 지형(509-5) — 둘이면 창에 고르기 · 처음 값 = 지금 설계 지형. */
|
||||||
|
terrains: AnalysisTerrain[];
|
||||||
|
terrain: AnalysisTerrain;
|
||||||
/** [적용] — 미리 본 노선을 바로 저장한다(새 초기값). */
|
/** [적용] — 미리 본 노선을 바로 저장한다(새 초기값). */
|
||||||
onApply: (choice: InitialChoice) => void;
|
onApply: (choice: InitialChoice) => void;
|
||||||
/** [적용] 확인 창 글 — 초기화로 지워질 수(492-2 · [저장]과 같은 글). */
|
/** [적용] 확인 창 글 — 초기화로 지워질 수(492-2 · [저장]과 같은 글). */
|
||||||
@@ -169,6 +178,9 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
let preview: InitialChoice | null = null;
|
let preview: InitialChoice | null = null;
|
||||||
/** 종단 반영(421) — 켬 = 후보마다 균형 종단 · 끔 = 계획고 = 지반고. */
|
/** 종단 반영(421) — 켬 = 후보마다 균형 종단 · 끔 = 계획고 = 지반고. */
|
||||||
let profileOn = params.withProfile;
|
let profileOn = params.withProfile;
|
||||||
|
/** 창 지형(509-5) — 바꾸면 그 지형 저장본 · [적용]이 설계 지형으로. */
|
||||||
|
let terrain = params.terrain;
|
||||||
|
const choosable = params.terrains.includes("las") && params.terrains.includes("sheet");
|
||||||
let busy = false;
|
let busy = false;
|
||||||
/** 마지막 미리 보기 뒤 슬라이더 · 제약 칸을 만졌다 — 미리 본 노선이 칸 값과 다르다. */
|
/** 마지막 미리 보기 뒤 슬라이더 · 제약 칸을 만졌다 — 미리 본 노선이 칸 값과 다르다. */
|
||||||
let dirty = false;
|
let dirty = false;
|
||||||
@@ -211,6 +223,12 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
<div class="b05-routerec__bars" data-slot="bars"></div>
|
<div class="b05-routerec__bars" data-slot="bars"></div>
|
||||||
<span class="b05-routerec__sum" data-weight-sum></span>
|
<span class="b05-routerec__sum" data-weight-sum></span>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="b05-routerec__terrain" data-terrain-row hidden
|
||||||
|
title="추천 · 초기 종단을 셀 지형 — [적용]하면 이 지형이 설계 지형(평면 · 종단 · 횡단)">지형
|
||||||
|
<select class="ui-select ui-select--sm" data-act="terrain">
|
||||||
|
<option value="las">라이다</option>
|
||||||
|
<option value="sheet">수치지형도</option>
|
||||||
|
</select></label>
|
||||||
<div class="b05-routerec__criteria" data-slot="criteria"></div>
|
<div class="b05-routerec__criteria" data-slot="criteria"></div>
|
||||||
<label class="b05-routerec__target" data-target-row hidden
|
<label class="b05-routerec__target" data-target-row hidden
|
||||||
title="영선 노선이 따라갈 종단기울기(%) — 비우면 위 종단기울기 상한">목표 기울기
|
title="영선 노선이 따라갈 종단기울기(%) — 비우면 위 종단기울기 상한">목표 기울기
|
||||||
@@ -336,6 +354,9 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
rerun.title = "지금 칸 값으로 서버 저장본을 지우고 새로 셈";
|
rerun.title = "지금 칸 값으로 서버 저장본을 지우고 새로 셈";
|
||||||
rerun.before(profileToggle);
|
rerun.before(profileToggle);
|
||||||
const stateEl = q<HTMLElement>("[data-state]");
|
const stateEl = q<HTMLElement>("[data-state]");
|
||||||
|
const terrainSelect = q<HTMLSelectElement>('[data-act="terrain"]');
|
||||||
|
q<HTMLElement>("[data-terrain-row]").hidden = !choosable;
|
||||||
|
terrainSelect.value = terrain;
|
||||||
const line = (name: string): HTMLElement => q<HTMLElement>(`[data-line="${name}"]`);
|
const line = (name: string): HTMLElement => q<HTMLElement>(`[data-line="${name}"]`);
|
||||||
const criteriaInputs = body.querySelectorAll<HTMLInputElement>("[data-criteria]");
|
const criteriaInputs = body.querySelectorAll<HTMLInputElement>("[data-criteria]");
|
||||||
bindRecommendHelp(body);
|
bindRecommendHelp(body);
|
||||||
@@ -420,6 +441,7 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
q<HTMLElement>("[data-target-row]").hidden = logic !== "gradeline";
|
q<HTMLElement>("[data-target-row]").hidden = logic !== "gradeline";
|
||||||
// 종단 반영 오른쪽 · 늘 켜짐(입력 오류 · 셈 중 빼고 · 421)
|
// 종단 반영 오른쪽 · 늘 켜짐(입력 오류 · 셈 중 빼고 · 421)
|
||||||
rerun.disabled = busy || invalid.size > 0;
|
rerun.disabled = busy || invalid.size > 0;
|
||||||
|
terrainSelect.disabled = busy;
|
||||||
profileToggle.disabled = busy;
|
profileToggle.disabled = busy;
|
||||||
profileToggle.setAttribute("aria-pressed", String(profileOn));
|
profileToggle.setAttribute("aria-pressed", String(profileOn));
|
||||||
profileToggle.classList.toggle("is-active", profileOn);
|
profileToggle.classList.toggle("is-active", profileOn);
|
||||||
@@ -498,7 +520,9 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
preview = null;
|
preview = null;
|
||||||
// 후보마다 균형 종단을 셈해 처음 한 번은 수십 초 — 지난 초를 보인다(계획서 63-4).
|
// 후보마다 균형 종단을 셈해 처음 한 번은 수십 초 — 지난 초를 보인다(계획서 63-4).
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
const label = `${choiceLabel(logic, sentWeights)}${withProfile ? "" : " · 종단 끔"}`;
|
const label = `${choiceLabel(logic, sentWeights)}${withProfile ? "" : " · 종단 끔"}${
|
||||||
|
choosable ? ` · ${TERRAIN_NAME[terrain]}` : ""
|
||||||
|
}`;
|
||||||
/** 서버가 뒤에서 미리 셈 중이면 그 진행(364-4) — 2초마다 읽음. */
|
/** 서버가 뒤에서 미리 셈 중이면 그 진행(364-4) — 2초마다 읽음. */
|
||||||
let behind = "";
|
let behind = "";
|
||||||
const tick = (): void => {
|
const tick = (): void => {
|
||||||
@@ -529,6 +553,7 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
Object.keys(criteria).length ? criteria : null,
|
Object.keys(criteria).length ? criteria : null,
|
||||||
withProfile,
|
withProfile,
|
||||||
recompute,
|
recompute,
|
||||||
|
terrain,
|
||||||
);
|
);
|
||||||
if (closed || mine !== seq) return;
|
if (closed || mine !== seq) return;
|
||||||
// 비운 칸을 물었을 때 서버가 준 값 — 그 칸의 진짜 기본값으로 기억해 둔다.
|
// 비운 칸을 물었을 때 서버가 준 값 — 그 칸의 진짜 기본값으로 기억해 둔다.
|
||||||
@@ -617,6 +642,10 @@ export function openRecommendModal(params: RecommendParams): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
rerun.addEventListener("click", () => void run(true));
|
rerun.addEventListener("click", () => void run(true));
|
||||||
|
terrainSelect.addEventListener("change", () => {
|
||||||
|
terrain = terrainSelect.value === "sheet" ? "sheet" : "las";
|
||||||
|
void run(); // 그 지형 저장본 — 없으면 안내 · [재계산](509-5)
|
||||||
|
});
|
||||||
applyButton.addEventListener("click", () => {
|
applyButton.addEventListener("click", () => {
|
||||||
const picked = preview;
|
const picked = preview;
|
||||||
if (!picked) return;
|
if (!picked) return;
|
||||||
|
|||||||
@@ -372,6 +372,19 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 지형 고르기(509-5) — 이름 · 고르기 한 줄. */
|
||||||
|
.b05-routerec__terrain {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 7em;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-8);
|
||||||
|
color: var(--color-text-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.b05-routerec__terrain[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* 공통 제약 네 칸 — 한 줄에 하나(이름 · 입력 · 단위). 비우면 placeholder 가 기본값. */
|
/* 공통 제약 네 칸 — 한 줄에 하나(이름 · 입력 · 단위). 비우면 placeholder 가 기본값. */
|
||||||
.b05-routerec__criteria {
|
.b05-routerec__criteria {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ const local = { grid, design: "las" };
|
|||||||
|
|
||||||
let remoteCalls = 0;
|
let remoteCalls = 0;
|
||||||
STUBS["B05_Profile_Api_Replan"] = {
|
STUBS["B05_Profile_Api_Replan"] = {
|
||||||
analysisTerrain: () => "las",
|
|
||||||
fetchRoutePlan: async () => ({ planned: job.line, curves: [], min_radius_m: job.minRadius }),
|
fetchRoutePlan: async () => ({ planned: job.line, curves: [], min_radius_m: job.minRadius }),
|
||||||
};
|
};
|
||||||
STUBS["B05_Profile_Api_Structures"] = {
|
STUBS["B05_Profile_Api_Structures"] = {
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ const STUBS = {
|
|||||||
"./B05_Profile_Api_Fetch": {
|
"./B05_Profile_Api_Fetch": {
|
||||||
clearRouteLatestCache: () => calls.push("latest:clear"),
|
clearRouteLatestCache: () => calls.push("latest:clear"),
|
||||||
},
|
},
|
||||||
|
// 509-2 — 추천 [적용] 뒤 받아 둔 설계 지형 격자 버림
|
||||||
|
"./B05_Profile_Api_TerrainGrid": {
|
||||||
|
forgetDesignTerrainGrid: () => calls.push("grid:forget"),
|
||||||
|
},
|
||||||
"./B05_Profile_Api_Replan": {
|
"./B05_Profile_Api_Replan": {
|
||||||
replanRoute: async (_p, body) => {
|
replanRoute: async (_p, body) => {
|
||||||
bodies.push(JSON.parse(JSON.stringify(body)));
|
bodies.push(JSON.parse(JSON.stringify(body)));
|
||||||
@@ -115,7 +119,6 @@ const apply = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Ap
|
|||||||
curveRadius: [null, 25],
|
curveRadius: [null, 25],
|
||||||
}),
|
}),
|
||||||
changed: () => changed,
|
changed: () => changed,
|
||||||
terrain: () => "sheet",
|
|
||||||
snap: () => ({ stationSnap: true, stationSnapStep: 20 }), // 390-5 — 켬끔 · 간격 한 벌
|
snap: () => ({ stationSnap: true, stationSnapStep: 20 }), // 390-5 — 켬끔 · 간격 한 벌
|
||||||
initial: { current: () => ({ mode: "follow" }), keep: () => (kept += 1) },
|
initial: { current: () => ({ mode: "follow" }), keep: () => (kept += 1) },
|
||||||
onApplied: () => (rebuilt += 1),
|
onApplied: () => (rebuilt += 1),
|
||||||
@@ -158,7 +161,13 @@ const apply = loadTs(path.join(ROOT, "B05_Profile", "B05_Profile_UI_RouteEdit_Ap
|
|||||||
curveOn: [true, true],
|
curveOn: [true, true],
|
||||||
curveRadius: [null, null],
|
curveRadius: [null, null],
|
||||||
};
|
};
|
||||||
out.recommendOk = await apply.saveRecommendedRoute("p1", snapshot, { mode: "earthwork" }, true);
|
out.recommendOk = await apply.saveRecommendedRoute(
|
||||||
|
"p1",
|
||||||
|
snapshot,
|
||||||
|
{ mode: "earthwork" },
|
||||||
|
true,
|
||||||
|
"sheet", // 509-5 — 창에서 고른 지형
|
||||||
|
);
|
||||||
out.recommendBody = bodies[bodies.length - 1];
|
out.recommendBody = bodies[bodies.length - 1];
|
||||||
out.recommendCalls = calls.filter((c) => !c.startsWith("toast:"));
|
out.recommendCalls = calls.filter((c) => !c.startsWith("toast:"));
|
||||||
out.recommendDraftsLeft = {
|
out.recommendDraftsLeft = {
|
||||||
|
|||||||
@@ -164,14 +164,10 @@ def test_화면_고르기_배선():
|
|||||||
main = (page / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
main = (page / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||||||
api = (page / "B05_Profile_Api_Replan.ts").read_text(encoding="utf-8")
|
api = (page / "B05_Profile_Api_Replan.ts").read_text(encoding="utf-8")
|
||||||
initial_api = (page / "B05_Profile_Api_RouteInitial.ts").read_text(encoding="utf-8")
|
initial_api = (page / "B05_Profile_Api_RouteInitial.ts").read_text(encoding="utf-8")
|
||||||
# 기본 숨김 · 둘 다 있을 때만 보임 · 기본 라이다
|
# 509-5 — 평면은 설계 지형 이름만(둘 다 있을 때) · 고르기 · 지형 실음 = 추천 창(test_509_5)
|
||||||
assert '<label class="b05-routeedit__terrain" hidden' in chrome
|
assert 'data-slot="terrain-name" hidden' in chrome and 'data-act="terrain"' not in chrome
|
||||||
assert '<option value="las">라이다</option>' in chrome
|
assert 'sources?.sources.includes("las") && sources.sources.includes("sheet")' in main
|
||||||
assert '<option value="sheet">수치지형도</option>' in chrome
|
assert "analysisTerrain" not in api and "analysisTerrain" not in initial_api
|
||||||
assert 'sources?.sources.includes("las") && !!sources?.sources.includes("sheet")' in main
|
assert "body: JSON.stringify(request)," in initial_api
|
||||||
assert 'saved === "sheet" || saved === "las" ? saved : (context?.design ?? fallback)' in api
|
|
||||||
# 모달 지반고가 고른 지형을 실어 보냄 · 횡단 미리보기 = 묶음(365-3) · [확인] 재계산은 안 실음
|
|
||||||
assert api.count("terrain: analysisTerrain(projectId)") == 1
|
|
||||||
assert "terrain: analysisTerrain(projectId)" in initial_api
|
|
||||||
replan_fn = api.split("export async function replanRoute")[1].split("export ")[0]
|
replan_fn = api.split("export async function replanRoute")[1].split("export ")[0]
|
||||||
assert "terrain" not in replan_fn
|
assert "terrain" not in replan_fn
|
||||||
|
|||||||
@@ -291,9 +291,8 @@ def test_화면_확인이_고른_지형을_실음():
|
|||||||
page = ROOT / "B05_Profile"
|
page = ROOT / "B05_Profile"
|
||||||
apply = (page / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
apply = (page / "B05_Profile_UI_RouteEdit_Apply.ts").read_text(encoding="utf-8")
|
||||||
main = (page / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
main = (page / "B05_Profile_UI_RouteEdit.ts").read_text(encoding="utf-8")
|
||||||
# 346-3 — 노선 초안(draft.terrain)에 실림 · [저장]이 보냄
|
# 509-5 — 지형 바꿈 = 추천 [적용]만(창 지형) · 손 [저장] 초안 = 설계 지형(null)
|
||||||
assert "terrain: params.terrain() })" in apply and "terrain: settings.terrain," in apply
|
assert "terrain: settings.terrain," in apply
|
||||||
assert "pickedTerrain() !== designTerrain" in main # 지형만 바꿔도 [확인]이 다시 셈
|
assert "const settings = () => ({ ...params.snap(), terrain: null });" in apply
|
||||||
assert (
|
assert "stationSnapStep: null,\n terrain,\n" in apply # 창 지형(509-5)
|
||||||
"setTerrainContext(projectId, choosable, designTerrain)" in main
|
assert "pickedTerrain" not in main and "terrainSelect" not in main
|
||||||
) # 고른 값 없으면 설계 지형
|
|
||||||
|
|||||||
@@ -86,11 +86,10 @@ def test_체인이_만들면_화면이_꺼냄(wired) -> None: # noqa: F811
|
|||||||
|
|
||||||
|
|
||||||
def test_화면이_실은_분석_지형이_설계_지형이면_기본_갈래(wired) -> None: # noqa: F811
|
def test_화면이_실은_분석_지형이_설계_지형이면_기본_갈래(wired) -> None: # noqa: F811
|
||||||
"""화면은 늘 분석 지형을 실음 — 설계 지형(가짜 세계 = 라이다)과 같으면 저장본만 · 다르면 셈."""
|
"""창 지형 — 설계 지형이든 다른 지형이든 기본 갈래는 저장본만(509-5 · 지형마다 미리 셈)."""
|
||||||
assert _ask(False, mode="gentle", terrain="las").status_code == 409
|
assert _ask(False, mode="gentle", terrain="las").status_code == 409
|
||||||
|
assert _ask(False, mode="gentle", terrain="sheet").status_code == 409
|
||||||
assert wired.count == 0
|
assert wired.count == 0
|
||||||
assert _ask(False, mode="gentle", terrain="sheet")["stored"] is False
|
|
||||||
assert wired.count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_사용자가_바꾼_값_재계산은_셈(wired) -> None: # noqa: F811
|
def test_사용자가_바꾼_값_재계산은_셈(wired) -> None: # noqa: F811
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ def test_페이지_묶음_노선_초안_캐시_칸(r):
|
|||||||
assert p["before"] == {"saved": None, "pipe": True}
|
assert p["before"] == {"saved": None, "pipe": True}
|
||||||
assert (
|
assert (
|
||||||
p["cached"]["rev"] == 1
|
p["cached"]["rev"] == 1
|
||||||
and p["cached"]["terrain"] == "las"
|
and p["cached"]["terrain"] is None # 설계 지형(509-5)
|
||||||
and p["cached"]["nodes"] == r["n"]
|
and p["cached"]["nodes"] == r["n"]
|
||||||
)
|
)
|
||||||
assert p["sameRev"] == 1 # 같은 노선 = 판 번호 그대로
|
assert p["sameRev"] == 1 # 같은 노선 = 판 번호 그대로
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def test_초안은_계약_RouteDraft_꼴(out):
|
|||||||
len(draft[name]) == n for name in ("curveOn", "curveRadius", "curveLock", "curveArc")
|
len(draft[name]) == n for name in ("curveOn", "curveRadius", "curveLock", "curveArc")
|
||||||
)
|
)
|
||||||
assert draft["curveOn"] == [True, False, True] and draft["curveRadius"] == [None, 25, None]
|
assert draft["curveOn"] == [True, False, True] and draft["curveRadius"] == [None, 25, None]
|
||||||
assert draft["stationSnap"] is True and draft["terrain"] == "sheet"
|
assert draft["stationSnap"] is True and draft["terrain"] is None # 손 저장 = 설계 지형(509-5)
|
||||||
assert draft["stationSnapStep"] == 20 # 390-5 간격도 함께 실림
|
assert draft["stationSnapStep"] == 20 # 390-5 간격도 함께 실림
|
||||||
assert out["body"]["initial"] == {"mode": "follow"}
|
assert out["body"]["initial"] == {"mode": "follow"}
|
||||||
assert "balanced_profile" not in out["body"] # 손으로 고친 노선 = 계획고 옮김만(349-0v)
|
assert "balanced_profile" not in out["body"] # 손으로 고친 노선 = 계획고 옮김만(349-0v)
|
||||||
@@ -90,8 +90,15 @@ def test_추천_적용은_as_initial_저장_뒤_다시_세움(out):
|
|||||||
body = out["recommendBody"]
|
body = out["recommendBody"]
|
||||||
assert body["as_initial"] is True and body["initial"] == {"mode": "earthwork"}
|
assert body["as_initial"] is True and body["initial"] == {"mode": "earthwork"}
|
||||||
assert body["draft"]["nodes"] == [[1, 1], [9, 9]]
|
assert body["draft"]["nodes"] == [[1, 1], [9, 9]]
|
||||||
|
assert body["draft"]["terrain"] == "sheet" # 창 지형 = 설계 지형(509-5)
|
||||||
# 잠금 막 하나(472-6 6-B — 저장 직전 저장 관 떠 두기는 서버 한 본문 몫 · 추천은 초안을 비우니 안 받음)
|
# 잠금 막 하나(472-6 6-B — 저장 직전 저장 관 떠 두기는 서버 한 본문 몫 · 추천은 초안을 비우니 안 받음)
|
||||||
assert out["recommendCalls"] == ["overlay:on", "replan", "commit", "overlay:off"] # 초안 전 시설 버림
|
assert out["recommendCalls"] == [
|
||||||
|
"overlay:on",
|
||||||
|
"replan",
|
||||||
|
"commit",
|
||||||
|
"grid:forget", # 새 설계 지형 격자로(509-2)
|
||||||
|
"overlay:off",
|
||||||
|
] # 초안 전 시설 버림
|
||||||
assert out["recommendDraftsLeft"] == {"route": None, "pipes": None} # 새 초기값에서 다시
|
assert out["recommendDraftsLeft"] == {"route": None, "pipes": None} # 새 초기값에서 다시
|
||||||
assert out["rebuilt"] == 1
|
assert out["rebuilt"] == 1
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ def test_기본값_판정() -> None:
|
|||||||
}
|
}
|
||||||
assert is_default("gentle", None, None, None)
|
assert is_default("gentle", None, None, None)
|
||||||
assert is_default("gentle", None, {"min_radius_m": None}, None)
|
assert is_default("gentle", None, {"min_radius_m": None}, None)
|
||||||
assert not is_default("gentle", None, None, "las")
|
assert is_default("gentle", None, None, "las") # 지형 안 따짐(509-5 · 지형마다 미리 셈)
|
||||||
assert not is_default("gentle", None, {"min_radius_m": 15.0}, None)
|
assert not is_default("gentle", None, {"min_radius_m": 15.0}, None)
|
||||||
assert is_default("gentle", None, {"min_radius_m": 15.0}, None, saved)
|
assert is_default("gentle", None, {"min_radius_m": 15.0}, None, saved)
|
||||||
assert not is_default("gentle", None, {"min_radius_m": 16.0}, None, saved)
|
assert not is_default("gentle", None, {"min_radius_m": 16.0}, None, saved)
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
# 509-5 — 평면 분석 지형 고르기(화면 취향 기억)를 걷음 · 지형 고르기 = 추천 창 안(test_509_5_*)
|
||||||
|
pytestmark = pytest.mark.skip(
|
||||||
|
reason="509-5 평면 분석 지형 고르기 걷음 — test_509_5_recommend_terrain"
|
||||||
|
)
|
||||||
|
|
||||||
HERE = Path(__file__).resolve().parent
|
HERE = Path(__file__).resolve().parent
|
||||||
ROOT = HERE.parents[1]
|
ROOT = HERE.parents[1]
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,11 @@ def test_모달이_여는_갈래_먼저(monkeypatch) -> None:
|
|||||||
lambda _root: {"logic": "gentle", "weights": None, "criteria": None},
|
lambda _root: {"logic": "gentle", "weights": None, "criteria": None},
|
||||||
)
|
)
|
||||||
monkeypatch.setattr(router, "compute_initial", compute)
|
monkeypatch.setattr(router, "compute_initial", compute)
|
||||||
|
|
||||||
|
async def terrains(_pid, _root):
|
||||||
|
return ["las"] # 지형 하나(509-5 지형마다 = test_509_5)
|
||||||
|
|
||||||
|
monkeypatch.setattr(router, "precompute_terrains", terrains)
|
||||||
asyncio.run(router.precompute_defaults(uuid4()))
|
asyncio.run(router.precompute_defaults(uuid4()))
|
||||||
assert asked[0] == "gentle" and asked[-1] == "follow"
|
assert asked[0] == "gentle" and asked[-1] == "follow"
|
||||||
assert sorted(asked) == sorted(DEFAULT_MODES)
|
assert sorted(asked) == sorted(DEFAULT_MODES)
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ def test_편집기_배선() -> None:
|
|||||||
"lineChanged(history) || otherChanged()" in main.split("function tellDraft()", 1)[1][:400]
|
"lineChanged(history) || otherChanged()" in main.split("function tellDraft()", 1)[1][:400]
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
"const otherChanged = (): boolean => pickedTerrain() !== designTerrain || snap.changed();"
|
"const otherChanged = (): boolean => snap.changed();" # 지형 고르기 = 추천 창(509-5)
|
||||||
in main
|
in main
|
||||||
) # 433-14 ②
|
) # 433-14 ②
|
||||||
assert "plan.node_edits" in main
|
assert "plan.node_edits" in main
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
"""509-2 수치지형도 격자 브라우저로 — 적용 지형(설계 지형) = 수치지형도일 때만 그 격자를 받음 · 노드
|
||||||
|
끌기 지반고 서버 묻기 걷음 · 횡단 비탈 넓힘도 같은 격자(509-4 G2).
|
||||||
|
|
||||||
|
./venv/Scripts/python.exe -m pytest resources/tester/test_509_2_sheet_grid.py -q
|
||||||
|
|
||||||
|
분석 지형 = 설계 지형 하나(509-5) — 받는 격자는 늘 설계 지형 것 · 추천 [적용]이 지형을 바꾸면 버림.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from B05_Profile.B05_Profile_Router_Terrain import _design_grid
|
||||||
|
from common_util.common_util_surface_confirmation import SHEET_ANALYSIS_FILTER
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
SHEET = {"source_filter": SHEET_ANALYSIS_FILTER, "method": "dtm", "smooth": True}
|
||||||
|
MODEL = f"B04_PreProcess/models/dtm_{SHEET_ANALYSIS_FILTER}.npz"
|
||||||
|
|
||||||
|
|
||||||
|
def test_설계_지형_수치지형도면_그_격자_자리(tmp_path) -> None:
|
||||||
|
grid = _design_grid(SHEET, tmp_path, (41, "dtm", MODEL))
|
||||||
|
assert grid == {"model_id": 41, "smooth": True, "signature": "."}
|
||||||
|
|
||||||
|
|
||||||
|
def test_확정_모델이_다른_파일이면_격자_없음(tmp_path) -> None:
|
||||||
|
las = "B04_PreProcess/models/dtm_classification.npz"
|
||||||
|
assert _design_grid(SHEET, tmp_path, (41, "dtm", las)) is None
|
||||||
|
assert _design_grid(SHEET, tmp_path, None) is None
|
||||||
|
|
||||||
|
|
||||||
|
def _ts(path: str) -> str:
|
||||||
|
return (ROOT / path).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_지반고는_설계_지형_격자_서버_묻기_없음() -> None:
|
||||||
|
replan = _ts("B05_Profile/B05_Profile_Api_Replan.ts")
|
||||||
|
elevations = replan.split("export async function fetchRouteElevations", 1)[1][:700]
|
||||||
|
assert "if (local) {" in elevations # 받아 둔 격자면 늘 브라우저 표본
|
||||||
|
assert "JSON.stringify({ points })" in elevations # 서버 길도 설계 지형
|
||||||
|
# 계산 묶음 초안 지형 = null(설계) → ground 단계가 격자를 씀(terrain === null)
|
||||||
|
ground = _ts("B05_Profile/logic/B05_Profile_Pipeline_RouteGround.ts")
|
||||||
|
assert "(terrain === null || terrain === local.design)" in ground
|
||||||
|
pipeline = _ts("A00_Common/design/design_pipeline_page.ts")
|
||||||
|
assert "terrain: null };" in pipeline
|
||||||
|
# 횡단 비탈 넓힘 = ground 와 같은 격자(509-4 G2 · 적용 지형 것)
|
||||||
|
assert "createCrossStage({ grid: groundDeps.grid })" in pipeline
|
||||||
|
|
||||||
|
|
||||||
|
def test_적용으로_지형이_바뀌면_격자_버림() -> None:
|
||||||
|
grid = _ts("B05_Profile/B05_Profile_Api_TerrainGrid.ts")
|
||||||
|
forget = grid.split("export function forgetDesignTerrainGrid", 1)[1][:200]
|
||||||
|
assert "loaded.delete(projectId)" in forget and "ready.delete(projectId)" in forget
|
||||||
|
apply = _ts("B05_Profile/B05_Profile_UI_RouteEdit_Apply.ts")
|
||||||
|
recommend = apply.split("export async function saveRecommendedRoute", 1)[1]
|
||||||
|
assert recommend.index("forgetDesignTerrainGrid(projectId)") < recommend.index(
|
||||||
|
"await rebuildAfterRouteSave(projectId)"
|
||||||
|
)
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""509-5 분석 지형 고르기 = 추천 노선 창 안 — 지형마다 추천 미리 셈 · 창 지형 바꿈 = 그 지형 저장본 ·
|
||||||
|
없으면 안내 + [재계산] · [적용] = 창 지형이 설계 지형 · 평면 고르기 없음.
|
||||||
|
|
||||||
|
./venv/Scripts/python.exe -m pytest resources/tester/test_509_5_recommend_terrain.py -q
|
||||||
|
|
||||||
|
가짜 세계(`test_224_3` wired) — 지형마다 다른 격자(수치지형도 = 1 m 높임)라 저장 키가 갈림.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
|
from pathlib import Path
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from test_224_3_recommend_store import STREAMS, _ask, _terrain, wired # noqa: F401 — 픽스처
|
||||||
|
|
||||||
|
from B05_Profile import B05_Profile_Router_RouteInitial as router
|
||||||
|
from B05_Profile import B05_Profile_Router_Terrain as terrain_router
|
||||||
|
from B05_Profile.B05_Profile_Engine_RouteInitial_Store import DEFAULT_MODES, store_dir
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def two(wired, monkeypatch): # noqa: F811
|
||||||
|
"""라이다 설계 · 수치지형도 고를 수 있음 — 지형마다 다른 격자."""
|
||||||
|
import B05_Profile.B05_Profile_Engine_RouteInitial as engine
|
||||||
|
import common_util.common_util_surface_confirmation as surface
|
||||||
|
|
||||||
|
async def terrains(_pid, _root):
|
||||||
|
return ["las", "sheet"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(router, "precompute_terrains", terrains)
|
||||||
|
monkeypatch.setattr(surface, "analysis_terrain_params", lambda s, t: {**s, "t": t})
|
||||||
|
monkeypatch.setattr(
|
||||||
|
engine,
|
||||||
|
"load_terrain_and_streams",
|
||||||
|
lambda _root, selection: (_terrain(1.0 if selection.get("t") == "sheet" else 0.0), STREAMS),
|
||||||
|
)
|
||||||
|
return wired
|
||||||
|
|
||||||
|
|
||||||
|
def _saved(root: Path) -> list[dict]:
|
||||||
|
return [json.loads(p.read_text(encoding="utf-8")) for p in store_dir(root).glob("*.json")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_미리_셈이_지형마다_추천_다섯(two) -> None:
|
||||||
|
asyncio.run(router.precompute_defaults(uuid4()))
|
||||||
|
assert two.count == 2 * len(DEFAULT_MODES)
|
||||||
|
saved = _saved(two.root)
|
||||||
|
assert sorted({item["request"]["terrain"] for item in saved}) == ["las", "sheet"]
|
||||||
|
assert all(item["default"] for item in saved)
|
||||||
|
# 창 지형 바꿈 = 그 지형 저장본(셈 없음) · 두 지형 답이 따로
|
||||||
|
for mode in DEFAULT_MODES:
|
||||||
|
assert _ask(False, mode=mode, terrain="sheet")["stored"] is True
|
||||||
|
assert _ask(False, mode=mode, terrain="las")["stored"] is True
|
||||||
|
assert two.count == 2 * len(DEFAULT_MODES)
|
||||||
|
|
||||||
|
|
||||||
|
def test_미리_셈_없는_지형은_안내_재계산은_셈(two) -> None:
|
||||||
|
"""기존 프로젝트(수치지형도 미리 셈 없음) — 안내 한 줄 · [재계산]이면 그 자리 셈."""
|
||||||
|
missing = _ask(False, mode="gentle", terrain="sheet")
|
||||||
|
assert missing.status_code == 409
|
||||||
|
message = json.loads(missing.body)["message"]
|
||||||
|
assert "전처리를 다시 하면" in message and "[재계산]" in message
|
||||||
|
assert two.count == 0
|
||||||
|
assert _ask(False, mode="gentle", terrain="sheet", recompute=True)["stored"] is False
|
||||||
|
assert two.count == 1
|
||||||
|
assert _ask(False, mode="gentle", terrain="sheet")["stored"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_미리_셈_지형_차례_설계_먼저(monkeypatch) -> None:
|
||||||
|
import common_util.common_util_surface_confirmation as surface
|
||||||
|
|
||||||
|
class _Pool:
|
||||||
|
@asynccontextmanager
|
||||||
|
async def acquire(self):
|
||||||
|
yield None
|
||||||
|
|
||||||
|
async def params(_c, _pid):
|
||||||
|
return {"source_filter": surface.SHEET_ANALYSIS_FILTER}
|
||||||
|
|
||||||
|
async def choosable(_c, _pid, _params, _dir):
|
||||||
|
return ["las", "sheet"]
|
||||||
|
|
||||||
|
monkeypatch.setattr(router, "get_db_pool", lambda: _Pool())
|
||||||
|
monkeypatch.setattr(surface, "get_surface_confirmation_params", params)
|
||||||
|
monkeypatch.setattr(terrain_router, "choosable_terrains", choosable)
|
||||||
|
assert asyncio.run(router.precompute_terrains(uuid4(), ROOT)) == ["sheet", "las"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_고를_지형은_DB_모델_있을_때만(tmp_path, monkeypatch) -> None:
|
||||||
|
import B04_PreProcess.B04_PreProcess_Service as service
|
||||||
|
from common_util.common_util_surface_confirmation import SHEET_ANALYSIS_FILTER
|
||||||
|
|
||||||
|
(tmp_path / f"dtm_{SHEET_ANALYSIS_FILTER}.npz").write_bytes(b"")
|
||||||
|
params = {"source_filter": "classification", "method": "dtm", "smooth": False}
|
||||||
|
found: list[str] = []
|
||||||
|
|
||||||
|
async def missing(_c, _pid, selection):
|
||||||
|
found.append(selection["source_filter"])
|
||||||
|
raise LookupError
|
||||||
|
|
||||||
|
async def present(_c, _pid, _selection):
|
||||||
|
return 7
|
||||||
|
|
||||||
|
monkeypatch.setattr(service, "find_surface_model_for_selection", missing)
|
||||||
|
pick = terrain_router.choosable_terrains
|
||||||
|
assert asyncio.run(pick(None, uuid4(), params, tmp_path)) == ["las"]
|
||||||
|
assert found == [SHEET_ANALYSIS_FILTER]
|
||||||
|
monkeypatch.setattr(service, "find_surface_model_for_selection", present)
|
||||||
|
assert asyncio.run(pick(None, uuid4(), params, tmp_path)) == ["las", "sheet"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_지형_바꿈은_추천_적용만_초기값_다시_세움() -> None:
|
||||||
|
replan = (ROOT / "B05_Profile/B05_Profile_Router_Replan.py").read_text(encoding="utf-8")
|
||||||
|
save = replan.split("async def _save_route(", 1)[1]
|
||||||
|
assert "request.terrain if as_initial else None," in save # 손 [저장]은 지형 안 바꿈
|
||||||
|
assert save.index("await _recompute(") < save.index("refresh_initial_snapshot(project_id")
|
||||||
|
recompute = replan.split("async def _recompute(", 1)[1].split("\nasync def ")[0]
|
||||||
|
assert "switch_design_terrain(connection, project_id, terrain)" in recompute
|
||||||
|
|
||||||
|
|
||||||
|
def _ts(name: str) -> str:
|
||||||
|
return (ROOT / "B05_Profile" / name).read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_창_지형_고르기_배선() -> None:
|
||||||
|
modal = _ts("B05_Profile_UI_RouteEdit_Recommend.ts")
|
||||||
|
assert 'data-act="terrain"' in modal and "data-terrain-row hidden" in modal
|
||||||
|
assert 'params.terrains.includes("las") && params.terrains.includes("sheet")' in modal
|
||||||
|
change = modal.split('terrainSelect.addEventListener("change"', 1)[1][:300]
|
||||||
|
assert "void run();" in change # 바꾸면 그 지형 저장본(없으면 안내 · [재계산])
|
||||||
|
assert "recompute,\n terrain,\n" in modal # 서버에 창 지형
|
||||||
|
initial = _ts("B05_Profile_UI_RouteEdit_Initial.ts")
|
||||||
|
assert "terrain: params.terrains()?.design" in initial # 처음 = 설계 지형
|
||||||
|
assert "return { ...normalizeRouteInitial(" in initial and "terrain };" in initial
|
||||||
|
assert "picked.terrain ?? null," in initial # [적용] = 창 지형
|
||||||
|
api = _ts("B05_Profile_Api_RouteInitial.ts")
|
||||||
|
assert "terrain?: AnalysisTerrain;" in api and "body: JSON.stringify(request)," in api
|
||||||
|
|
||||||
|
|
||||||
|
def test_평면_지형_고르기_없음_이름만() -> None:
|
||||||
|
chrome = _ts("B05_Profile_UI_RouteEdit_Chrome.ts")
|
||||||
|
main = _ts("B05_Profile_UI_RouteEdit.ts")
|
||||||
|
assert 'data-act="terrain"' not in chrome and 'data-slot="terrain-name"' in chrome
|
||||||
|
assert "terrainName.textContent = `지형 ${" in main
|
||||||
|
assert "terrainSelect" not in main and "setAnalysisTerrain" not in main
|
||||||
|
assert "routeedit-terrain" not in (ROOT / "A00_Common/b_page_state.ts").read_text(
|
||||||
|
encoding="utf-8"
|
||||||
|
)
|
||||||
|
pipeline = (ROOT / "A00_Common/design/design_pipeline_page.ts").read_text(encoding="utf-8")
|
||||||
|
assert "terrain: null }; // 설계 지형(509-5)" in pipeline
|
||||||
|
assert len(pipeline.splitlines()) <= 700 and len(main.splitlines()) <= 700
|
||||||
@@ -231,8 +231,8 @@ def test_노선_추천_다듬기_23_3():
|
|||||||
"|",
|
"|",
|
||||||
'data-act="station-snap"',
|
'data-act="station-snap"',
|
||||||
'data-act="snap-step"', # 390-2 스냅 간격 = [측점 스냅] 오른쪽
|
'data-act="snap-step"', # 390-2 스냅 간격 = [측점 스냅] 오른쪽
|
||||||
"|", # 390-1 [측점 스냅] | 분석 지형
|
"|", # 390-1 [측점 스냅] | 설계 지형 이름(509-5)
|
||||||
'class="b05-routeedit__terrain" hidden',
|
'class="b05-routeedit__terrain" data-slot="terrain-name" hidden',
|
||||||
]
|
]
|
||||||
at = 0
|
at = 0
|
||||||
for mark in order:
|
for mark in order:
|
||||||
|
|||||||
Reference in New Issue
Block a user