From 7a50540dbafb241534ae8b967d2230dc52c6ba9b Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 20 Jul 2026 19:58:55 +0900 Subject: [PATCH] =?UTF-8?q?B06=5F=EA=B3=84=ED=9A=8D=EB=85=B8=EC=84=A0=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../B06_wf3_ProfileCross_UI_Section_View.ts | 51 +++++++++++++++++-- .../B06_wf3_ProfileCross_UI_Style.css | 5 ++ 2 files changed, 51 insertions(+), 5 deletions(-) diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts index 704f7af5..415a73c3 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Section_View.ts @@ -48,12 +48,42 @@ function validElevation(sample: SectionSample): sample is SectionSample & { elev ); } +/** + * 측점 chainage 위치의 계획고를 계획선 샘플에서 선형보간한다. + * 범위를 벗어나면 양 끝값으로 클램프하며, 계획선이 없으면 undefined를 반환해 지반고 폴백을 유도한다. + */ +function designElevationAt( + designProfiles: DesignProfile[] | undefined, + chainageM: number, +): number | undefined { + const samples = designProfiles?.[0]?.samples?.filter((sample) => + Number.isFinite(sample.elevation_m), + ); + if (!samples?.length) return undefined; + if (chainageM <= samples[0].chainage_m) return samples[0].elevation_m; + const last = samples[samples.length - 1]; + if (chainageM >= last.chainage_m) return last.elevation_m; + for (let index = 1; index < samples.length; index += 1) { + const previous = samples[index - 1]; + const current = samples[index]; + if (chainageM > current.chainage_m) continue; + const span = current.chainage_m - previous.chainage_m; + if (span <= 0) return current.elevation_m; + const ratio = (chainageM - previous.chainage_m) / span; + return previous.elevation_m + (current.elevation_m - previous.elevation_m) * ratio; + } + return last.elevation_m; +} + function calculateYScale(detail: SectionDetailResponse): YScaleOptions | undefined { const elevations = [ ...detail.longitudinal.samples.map((sample) => sample.elevation_m), ...detail.cross_sections.flatMap((section) => section.samples.map((sample) => sample.elevation_m), ), + ...(detail.longitudinal.design_profiles ?? []).flatMap((profile) => + profile.samples.map((sample) => sample.elevation_m), + ), ].filter((value): value is number => typeof value === "number" && Number.isFinite(value)); if (!elevations.length) return undefined; const globalMinElevation = Math.min(...elevations); @@ -348,6 +378,7 @@ export function createCrossSectionCard( crossHalfWidth?: number, widthPx = CROSS_WIDTH, heightPx = CROSS_HEIGHT, + designElevation?: number, ): HTMLElement { const card = document.createElement("article"); card.id = `cross-${section.station_id}`; @@ -386,7 +417,9 @@ export function createCrossSectionCard( const offsets = sourceSamples.map((sample) => sample.offset_m ?? 0); const minOffset = Math.min(...offsets, -1); const maxOffset = Math.max(...offsets, 1); + const hasDesign = designElevation !== undefined && Number.isFinite(designElevation); const elevations = valid.map((sample) => sample.elevation_m); + if (hasDesign) elevations.push(designElevation as number); const rawMin = Math.min(...elevations); const rawMax = Math.max(...elevations); const elevationMid = (rawMin + rawMax) / 2; @@ -478,9 +511,15 @@ export function createCrossSectionCard( return nearest; }, null); const centerX = x(0); - const centerY = centerSample - ? y(elevationMid + (centerSample.elevation_m - elevationMid) * exaggeration) - : heightPx / 2; + // 십자선은 계획 노선(계획고) 위치를 가리킨다. 계획선이 없는 구 데이터는 지반고로 폴백한다. + const centerElevation = hasDesign + ? (designElevation as number) + : (centerSample?.elevation_m ?? null); + const centerY = + centerElevation !== null + ? y(elevationMid + (centerElevation - elevationMid) * exaggeration) + : heightPx / 2; + const centerMarkerClass = `b06-chart__center-marker${hasDesign ? " b06-chart__center-marker--design" : ""}`; svg.append( svgElement("line", { x1: CROSS_PAD.left, @@ -501,14 +540,14 @@ export function createCrossSectionCard( y1: centerY - 18, x2: centerX, y2: centerY + 18, - class: "b06-chart__center-marker", + class: centerMarkerClass, }), svgElement("line", { x1: centerX - 18, y1: centerY, x2: centerX + 18, y2: centerY, - class: "b06-chart__center-marker", + class: centerMarkerClass, }), svgText(L("B06_Profile_View_CrossXAxis"), { x: widthPx / 2, @@ -599,6 +638,7 @@ export function createSectionView(): SectionViewController { Math.max(renderWidth, longitudinalMinWidth), LONG_HEIGHT, longitudinalMinWidth, + detail.longitudinal.design_profiles ?? [], ), ); @@ -629,6 +669,7 @@ export function createSectionView(): SectionViewController { currentCrossHalfWidth, cardWidth, CROSS_HEIGHT, + designElevationAt(detail.longitudinal.design_profiles, section.chainage_m), ), ), ); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css index 6dac0848..71655d4b 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style.css @@ -356,6 +356,11 @@ stroke-width: 2.5; } +/* 십자선이 계획 노선(계획고) 위치를 가리킬 때: 종단 계획선과 동일 색으로 대응시킨다 */ +.b06-chart__center-marker--design { + stroke: var(--color-royal-amethyst); +} + .b06-chart__station--selected .b06-chart__station-label { fill: var(--color-danger); font-weight: var(--font-weight-bold);