feat(E01·E04·E06·E07): 측점 글 = No.+거리 소수 한 자리 · 간격 = 측점 목록 추정 (PLAN 566-4)
- 공용 stationLabel · inferStationInterval 로 한 벌(예 61+14.5) · 서버 label(STA. 꼴) 안 읽음 · 화면만 - E07 — 측점 표 · 지도 측점 글 · 셀 툴팁 · 지도 글 솎음이 20 m 고정 대신 추정 간격 - E04 — 구간 목록 · 위치(가까운 측점) · 지도 측점 글 · 툴팁 · 솎음 간격 - E06 — 위험 띠 · 측점 표 · 상세 제목 · 스테레오넷 제목 · 띠 양 끝 글 - E01 빠진 측점 띠 — interval 인자(기본 20 m · E02 · E03 이 측점 간격을 넘기면 그 값) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -8,15 +8,17 @@
|
||||
import { createBanner } from "@ui/ui_template_banner";
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { stationLabel } from "../common_util/common_util_station";
|
||||
|
||||
/** 빠진 측점이 없으면 빈 칸(띠 없음) */
|
||||
export function buildMissingBanner(missing: number[] | undefined): HTMLElement {
|
||||
/** 빠진 측점이 없으면 빈 칸(띠 없음) · 측점 글 = No.+거리 — interval = 측점 간격(부르는 쪽이
|
||||
* `inferStationInterval(측점 목록)` 으로 넘김 · 없으면 20 m) */
|
||||
export function buildMissingBanner(missing: number[] | undefined, interval = 20): HTMLElement {
|
||||
const list = missing ?? [];
|
||||
if (!list.length) return el("div");
|
||||
const box = createBanner({
|
||||
tone: "warning",
|
||||
text: t("E_Missing_Head").replace("{n}", String(list.length)),
|
||||
details: el("p", { text: list.map((m) => `${Math.round(m * 100) / 100} m`).join(" · ") }),
|
||||
details: el("p", { text: list.map((m) => stationLabel(m, interval, 1)).join(" · ") }),
|
||||
});
|
||||
box.classList.add("e-missing");
|
||||
box.dataset.role = "missing-stations";
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { tooltipBox } from "@ui/ui_template_hover_info";
|
||||
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { cutCells, footprintCenter } from "./logic/E04_Stab3D_Mesh";
|
||||
import {
|
||||
@@ -43,8 +44,6 @@ interface View {
|
||||
|
||||
const RANGE_COLOR = "#2563eb"; // 고른 구간 — 3D 노선 관과 같은 색 · 안전율 색 단계와 겹치지 않음
|
||||
|
||||
const shortLabel = (label: string): string => label.replace(/^STA\./, "").replace(/\.0+$/, "");
|
||||
|
||||
const css = (name: string, fallback: string): string =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
||||
|
||||
@@ -60,6 +59,7 @@ export function createPlanMap(): PlanMap {
|
||||
|
||||
let line: number[][] = [];
|
||||
let stations: StationPoint[] = [];
|
||||
let interval = 20; // 측점 간격 — setRoute 때 측점 목록에서 추정
|
||||
let range: [number, number] | null = null;
|
||||
let result: Result3d | null = null;
|
||||
let layer: MapLayer = "fs";
|
||||
@@ -140,8 +140,8 @@ export function createPlanMap(): PlanMap {
|
||||
ctx.stroke();
|
||||
}
|
||||
// 측점 — 화면 간격이 좁으면 다섯 개마다 이름
|
||||
// 이름 = "STA.0+020.000" → "0+020" · 이름끼리 70 px 이상 떨어지게 1 · 2 · 5 · 10 … 개마다
|
||||
const gap = stations.length > 1 ? 20 * view.scale : 999;
|
||||
// 이름 = No.+거리(예 61+14.5) · 이름끼리 70 px 이상 떨어지게 1 · 2 · 5 · 10 … 개마다
|
||||
const gap = stations.length > 1 ? interval * view.scale : 999;
|
||||
const every = [1, 2, 5, 10, 25, 50, 100].find((n) => n * gap >= 70) ?? 100;
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textBaseline = "middle";
|
||||
@@ -152,7 +152,7 @@ export function createPlanMap(): PlanMap {
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, i % every === 0 ? 3 : 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (i % every === 0 && s.label) ctx.fillText(shortLabel(s.label), px + 6, py - 8);
|
||||
if (i % every === 0) ctx.fillText(stationLabel(s.chainage_m, interval, 1), px + 6, py - 8);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -311,7 +311,7 @@ export function createPlanMap(): PlanMap {
|
||||
tip.textContent = [
|
||||
`${t("E04_Layer_FS")} ${f.toFixed(3)}`,
|
||||
`${t("E04_Layer_Volume")} ${v === null ? "-" : `${Math.round(v).toLocaleString()} m³`}`,
|
||||
near ? `${t("E04_Tip_Station")} ${near.label ?? near.chainage_m.toFixed(1)}` : "",
|
||||
near ? `${t("E04_Tip_Station")} ${stationLabel(near.chainage_m, interval, 1)}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
@@ -351,6 +351,7 @@ export function createPlanMap(): PlanMap {
|
||||
setRoute(nextLine, nextStations) {
|
||||
line = nextLine;
|
||||
stations = nextStations;
|
||||
interval = inferStationInterval(stations);
|
||||
fit();
|
||||
draw();
|
||||
},
|
||||
|
||||
@@ -24,6 +24,7 @@ import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
||||
import { createPlanMap, type MapLayer, type StationPoint } from "./E04_Stab3D_UI_Map";
|
||||
import { createView3D } from "./E04_Stab3D_UI_View3D";
|
||||
import { buildSettingsCards, type Settings } from "./E04_Stab3D_UI_Settings";
|
||||
@@ -246,6 +247,9 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
let result: Result3d | null = null;
|
||||
let stations2d: Station2d[] = [];
|
||||
let running = false;
|
||||
/** 측점 글 = No.+거리(소수 한 자리) · 간격 = 측점 목록 추정 */
|
||||
const station = (chainage: number): string =>
|
||||
stationLabel(chainage, inferStationInterval(overview?.stations ?? []), 1);
|
||||
|
||||
const remember = (): void =>
|
||||
sessionStorage.setItem(cacheKey(projectId), JSON.stringify(settings.read()));
|
||||
@@ -278,7 +282,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
const byCondition = CONDITIONS.map((c) => savedOf.get(`${key}_${c}`));
|
||||
const saved = savedOf.get(`${key}_${condition}`);
|
||||
const row = createListRow({
|
||||
label: `${section.start_m.toFixed(0)} – ${section.end_m.toFixed(0)} m`,
|
||||
label: `${station(section.start_m)} – ${station(section.end_m)}`,
|
||||
trailing: byCondition
|
||||
.flatMap((x) =>
|
||||
x
|
||||
@@ -313,7 +317,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
s.end_m,
|
||||
);
|
||||
const ratio = ratio3d2d(s.f_min, f2d);
|
||||
const station = overview?.stations.reduce<StationPoint | null>(
|
||||
const nearest = overview?.stations.reduce<StationPoint | null>(
|
||||
(best, x) =>
|
||||
!best || Math.abs(x.chainage_m - s.chainage_m) < Math.abs(best.chainage_m - s.chainage_m)
|
||||
? x
|
||||
@@ -335,7 +339,7 @@ export function renderE04Stab3D(root: HTMLElement): void {
|
||||
["E04_Result_Bishop", s.by_method.bishop.toFixed(3)],
|
||||
["E04_Result_Janbu", s.by_method.janbu_corrected.toFixed(3)],
|
||||
["E04_Result_Hovland", s.by_method.hovland.toFixed(3)],
|
||||
["E04_Result_Where", station?.label ?? `${s.chainage_m.toFixed(1)} m`],
|
||||
["E04_Result_Where", station(nearest?.chainage_m ?? s.chainage_m)],
|
||||
["E04_Result_Cells", s.cells.toLocaleString()],
|
||||
["E04_Result_Trials", s.trials.toLocaleString()],
|
||||
["E04_Result_Time", `${result.elapsed_s.toFixed(1)} s`],
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "@ui/ui_template_elements";
|
||||
import { t } from "@ui/ui_template_locale";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import {
|
||||
buildJointCards,
|
||||
@@ -158,6 +159,11 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
let result: RockResult | null = null;
|
||||
let active: string | null = null;
|
||||
let shown: StationResponse | null = null;
|
||||
let interval = 20; // 측점 간격 — 처음 GET 의 측점 목록에서 추정
|
||||
/** 측점 글 = No.+거리(소수 한 자리) — 서버 label(STA. 꼴) 대신 */
|
||||
const relabel = (e: StationEntry): void => {
|
||||
e.label = stationLabel(e.chainage_m, interval, 1);
|
||||
};
|
||||
|
||||
const invalidCount = (): number => cards.querySelectorAll('[aria-invalid="true"]').length;
|
||||
const paintStatus = (): void => {
|
||||
@@ -241,6 +247,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
shown = await api<StationResponse>(
|
||||
`/projects/${projectId}/stab/rock/result?station=${encodeURIComponent(key)}&side=${sideName}`,
|
||||
);
|
||||
relabel(shown.entry);
|
||||
} catch (error) {
|
||||
shown = null;
|
||||
showToast(error instanceof Error ? error.message : t("Common_Status_Error"), "error");
|
||||
@@ -251,6 +258,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
async function loadResult(): Promise<void> {
|
||||
const body = await api<ResultResponse>(`/projects/${projectId}/stab/rock/result`);
|
||||
result = body.result;
|
||||
result?.stations.forEach(relabel);
|
||||
staleBar.hidden = !body.stale;
|
||||
const stations = result?.stations ?? [];
|
||||
const keep = stations.find((e) => entryId(e) === active);
|
||||
@@ -334,6 +342,7 @@ export function renderE06StabRock(root: HTMLElement): void {
|
||||
try {
|
||||
const body = await api<RockResponse>(`/projects/${projectId}/stab/rock`);
|
||||
const stations = body.stations ?? [];
|
||||
interval = inferStationInterval(stations);
|
||||
projection = body.joints.settings.projection;
|
||||
paintProjection();
|
||||
view = {
|
||||
|
||||
@@ -113,8 +113,8 @@ export function buildBand(
|
||||
});
|
||||
const ticks = el("div", { className: "e06-band__ticks" });
|
||||
ticks.append(
|
||||
el("span", { text: `${lo.toFixed(0)} m` }),
|
||||
el("span", { text: `${hi.toFixed(0)} m` }),
|
||||
el("span", { text: list[0].label ?? `${lo.toFixed(0)} m` }),
|
||||
el("span", { text: list[list.length - 1].label ?? `${hi.toFixed(0)} m` }),
|
||||
);
|
||||
box.append(legend, strip, ticks);
|
||||
} else box.append(legend, note(t("E06_NoRock")));
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { el } from "@ui/ui_template_elements";
|
||||
import { tooltipBox } from "@ui/ui_template_hover_info";
|
||||
import { t, type LocaleKey } from "@ui/ui_template_locale";
|
||||
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
||||
import { CLASS_KEYS, cellIndex, rasterRgba, type Decoded } from "./logic/E07_StabArea_Raster";
|
||||
|
||||
export type MapLayer = "cls" | "fs";
|
||||
@@ -34,8 +35,6 @@ interface View {
|
||||
scale: number; // px / m
|
||||
}
|
||||
|
||||
const shortLabel = (label: string): string => label.replace(/^STA\./, "").replace(/\.0+$/, "");
|
||||
|
||||
const css = (name: string, fallback: string): string =>
|
||||
getComputedStyle(document.documentElement).getPropertyValue(name).trim() || fallback;
|
||||
|
||||
@@ -51,6 +50,7 @@ export function createAreaMap(): AreaMap {
|
||||
|
||||
let line: number[][] = [];
|
||||
let stations: StationPoint[] = [];
|
||||
let interval = 20; // 측점 간격 — setRoute 때 측점 목록에서 추정
|
||||
let band = 100;
|
||||
let data: Decoded | null = null;
|
||||
let layer: MapLayer = "cls";
|
||||
@@ -112,7 +112,7 @@ export function createAreaMap(): AreaMap {
|
||||
ctx.lineWidth = 2;
|
||||
path();
|
||||
ctx.stroke();
|
||||
const gap = stations.length > 1 ? 20 * view.scale : 999;
|
||||
const gap = stations.length > 1 ? interval * view.scale : 999;
|
||||
const every = [1, 2, 5, 10, 25, 50, 100].find((n) => n * gap >= 70) ?? 100;
|
||||
ctx.font = "11px sans-serif";
|
||||
ctx.textBaseline = "middle";
|
||||
@@ -124,7 +124,8 @@ export function createAreaMap(): AreaMap {
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, on ? 6 : i % every === 0 ? 3 : 1.5, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if ((on || i % every === 0) && s.label) ctx.fillText(shortLabel(s.label), px + 7, py - 9);
|
||||
if (on || i % every === 0)
|
||||
ctx.fillText(stationLabel(s.chainage_m, interval, 1), px + 7, py - 9);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -227,7 +228,7 @@ export function createAreaMap(): AreaMap {
|
||||
t(`E07_Class_${CLASS_KEYS[data!.cls[i] - 1]}` as LocaleKey),
|
||||
`SI ${data!.si[i].toFixed(3)} · Fs ${data!.fs[i].toFixed(3)}`,
|
||||
`${t("E07_Tip_Slope")} ${data!.slope[i].toFixed(1)}°${Number.isNaN(m) ? "" : ` · m ${m.toFixed(2)}`}`,
|
||||
near ? `${t("E07_Tip_Station")} ${near.label ?? near.chainage_m.toFixed(1)}` : "",
|
||||
near ? `${t("E07_Tip_Station")} ${stationLabel(near.chainage_m, interval, 1)}` : "",
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
@@ -265,6 +266,7 @@ export function createAreaMap(): AreaMap {
|
||||
setRoute(nextLine, nextStations) {
|
||||
line = nextLine;
|
||||
stations = nextStations;
|
||||
interval = inferStationInterval(stations);
|
||||
fit();
|
||||
draw();
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import { createSegment } from "@ui/ui_template_segment";
|
||||
import { createTable } from "@ui/ui_template_table";
|
||||
import { createSidePanels } from "@ui/ui_template_side_panels";
|
||||
import { createText } from "@ui/ui_template_text";
|
||||
import { inferStationInterval, stationLabel } from "../common_util/common_util_station";
|
||||
import { mountGroupStep } from "../A00_Common/group_workflow";
|
||||
import { createAreaMap, type MapLayer, type StationPoint } from "./E07_StabArea_UI_Map";
|
||||
import {
|
||||
@@ -87,8 +88,6 @@ const NUMBER_FIELDS: [string, LocaleKey, "soil" | "rain" | "band"][] = [
|
||||
];
|
||||
|
||||
const cacheKey = (projectId: string): string => `e07_settings:${projectId}`;
|
||||
const shortLabel = (label: string | null, ch: number): string =>
|
||||
(label ?? `${ch.toFixed(1)} m`).replace(/^STA\./, "").replace(/\.0+$/, "");
|
||||
|
||||
async function api<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
@@ -379,7 +378,7 @@ export function renderE07StabArea(root: HTMLElement): void {
|
||||
{ label: t("E07_Col_RiskArea"), align: "end" },
|
||||
],
|
||||
result.stations.map((r) => [
|
||||
shortLabel(r.label, r.chainage_m),
|
||||
stationLabel(r.chainage_m, inferStationInterval(overview?.stations ?? []), 1),
|
||||
r.fs_min === undefined ? "-" : r.fs_min.toFixed(2),
|
||||
r.risk_ratio === undefined ? "-" : (r.risk_ratio * 100).toFixed(1),
|
||||
r.level ? chipCell(LEVEL_COLORS[r.level], t(`E07_Level_${r.level}` as LocaleKey)) : "",
|
||||
|
||||
Reference in New Issue
Block a user