260723_0
This commit is contained in:
@@ -124,41 +124,61 @@ function normalizedLongitudinal(data: LongitudinalSection): LongitudinalSection
|
||||
};
|
||||
}
|
||||
|
||||
/** 종단면도 렌더러와 **같은** chainage → x(px) 매핑을 만든다 (테이블·버튼 정렬 기준). */
|
||||
function chainageMapper(data: LongitudinalSection, width: number): (chainage: number) => number {
|
||||
const samples = normalizedLongitudinal(data).samples.filter(
|
||||
(sample) => sample.valid !== false && Number.isFinite(sample.elevation_m ?? NaN),
|
||||
);
|
||||
const maxChainage = Math.max(data.length_m, samples[samples.length - 1]?.chainage_m ?? 1, 1);
|
||||
const plotWidth = width - LONG_PAD.left - LONG_PAD.right;
|
||||
return (chainage: number) => LONG_PAD.left + (chainage / maxChainage) * plotWidth;
|
||||
}
|
||||
|
||||
/** 측점 사이 여백 — 이웃 셀끼리 붙어 보이지 않게 띄운다. */
|
||||
const CELL_GAP_PX = 2;
|
||||
|
||||
/** 이웃 측점과 겹치지 않는 테이블 셀 폭 (글자 크기를 정하는 기준이기도 하다). */
|
||||
function stationCellWidth(data: LongitudinalSection, x: (chainage: number) => number): number {
|
||||
const stations = data.stations;
|
||||
if (stations.length < 2) return 72;
|
||||
const span = x(stations[stations.length - 1].chainage_m) - x(stations[0].chainage_m);
|
||||
return Math.max(30, Math.min(span / (stations.length - 1) - CELL_GAP_PX, 96));
|
||||
/**
|
||||
* **측점 간격 기본값(px)** — 측점 한 칸의 고정 폭.
|
||||
*
|
||||
* 목표 글자 크기(12px)로 7자리 값(`3000.00`)이 잘리지 않는 크기다. 브라우저 폭에 맞춰
|
||||
* 늘였다 줄였다 하지 않고 이 값을 항상 유지하며, 남는 가로는 스크롤로 훑는다. 규칙 측점은
|
||||
* 정확히 이 간격으로 찍히고, 셀 폭·글자 크기도 이 값에서 나온다.
|
||||
*
|
||||
* 다음 세션의 **비정규 측점**(`+18`, `+15` 등)은 이 간격을 한 칸으로 보고 그 안에서
|
||||
* chainage 비례로 자리 잡는다 — 값 자체는 chainage×(간격/측점간격)로 매핑되므로 규칙 칸을
|
||||
* 넓히면 비정규 측점이 겹칠 여유도 함께 늘어난다. 그래서 기본값을 넉넉히 잡아 둔다.
|
||||
*/
|
||||
const STATION_SPACING_PX = tableCellWidthFor(TABLE_TARGET_FONT_PX) + CELL_GAP_PX;
|
||||
|
||||
/**
|
||||
* 0측점·종점을 좌우 축 프레임 안쪽으로 반 칸 밀어넣는 여백(px).
|
||||
*
|
||||
* 0측점을 이름표 열(labelWidth = LONG_PAD.left) 바로 위에 두면 셀의 왼쪽 절반이 이름표에
|
||||
* 가려진다. 반 칸을 밀어 0측점 셀 전체가 이름표 오른쪽으로 나오게 하고, 종점도 대칭으로
|
||||
* 오른쪽 끝에서 반 칸 띄운다. 그래프도 같은 오프셋(`originOffsetPx`)을 받아 X축을 맞춘다.
|
||||
*/
|
||||
const PROFILE_ORIGIN_OFFSET_PX = STATION_SPACING_PX / 2;
|
||||
|
||||
/** 유효 표고 샘플 기준의 노선 최대 chainage(m). 그래프·테이블이 같은 값을 써야 X축이 맞물린다. */
|
||||
function maxChainageOf(data: LongitudinalSection): number {
|
||||
const samples = normalizedLongitudinal(data).samples.filter(
|
||||
(sample) => sample.valid !== false && Number.isFinite(sample.elevation_m ?? NaN),
|
||||
);
|
||||
return Math.max(data.length_m, samples[samples.length - 1]?.chainage_m ?? 1, 1);
|
||||
}
|
||||
|
||||
/** 종단면도 렌더러와 **같은** chainage → x(px) 매핑을 만든다 (테이블·버튼 정렬 기준). */
|
||||
function chainageMapper(
|
||||
data: LongitudinalSection,
|
||||
width: number,
|
||||
originOffset: number,
|
||||
): (chainage: number) => number {
|
||||
const maxChainage = maxChainageOf(data);
|
||||
const plotWidth = width - LONG_PAD.left - LONG_PAD.right - 2 * originOffset;
|
||||
return (chainage: number) => LONG_PAD.left + originOffset + (chainage / maxChainage) * plotWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 측점 한 칸의 고정 폭. 목표 글자 크기(12px)로 7자리 값이 잘리지 않는 크기다.
|
||||
* 화면 폭에 맞춰 늘였다 줄였다 하지 않고 이 값을 유지하고, 남는 가로는 스크롤로 훑는다.
|
||||
*/
|
||||
const STATION_COLUMN_PX = tableCellWidthFor(TABLE_TARGET_FONT_PX) + CELL_GAP_PX;
|
||||
|
||||
/**
|
||||
* 측점 칸 폭을 고정했을 때 필요한 캔버스 폭.
|
||||
* 측점 간격을 `STATION_SPACING_PX`로 고정했을 때의 캔버스 폭.
|
||||
*
|
||||
* 종단면도 렌더러의 최소 폭(`longitudinalMinimumWidth`)은 그래프 아래 **측점 라벨**만
|
||||
* 안 겹치면 되는 기준이라 열이 48px밖에 안 돼, 7자리 값(`3000.00`)을 담기엔 좁다.
|
||||
* 브라우저 폭에 맞춰 늘리지 않는다 — 노선 전체를 `STATION_SPACING_PX / 측점간격(m)`의
|
||||
* 고정 배율(px/m)로 펼치고, 화면보다 길면 스크롤로 훑는다. 이렇게 해야 측점 간격·셀 폭·
|
||||
* 글자 크기가 브라우저 크기와 무관하게 일정하게 유지된다.
|
||||
*/
|
||||
function tableMinimumWidth(stationCount: number): number {
|
||||
return LONG_PAD.left + LONG_PAD.right + Math.max(1, stationCount) * STATION_COLUMN_PX;
|
||||
function fixedProfileWidth(data: LongitudinalSection, stationIntervalM: number): number {
|
||||
const pxPerMeter = STATION_SPACING_PX / Math.max(stationIntervalM, 1e-6);
|
||||
// 좌우 반 칸씩(= STATION_SPACING_PX) 여백을 더해 0측점·종점 셀이 프레임에 붙지 않게 한다.
|
||||
return LONG_PAD.left + LONG_PAD.right + STATION_SPACING_PX + maxChainageOf(data) * pxPerMeter;
|
||||
}
|
||||
|
||||
export function createRouteProfilePanel(
|
||||
@@ -291,16 +311,22 @@ export function createRouteProfilePanel(
|
||||
renderBalance();
|
||||
|
||||
const longitudinal = detail.longitudinal;
|
||||
const minimumWidth = Math.max(
|
||||
longitudinalMinimumWidth(longitudinal, stationInterval),
|
||||
alignment ? tableMinimumWidth(longitudinal.stations.length) : 0,
|
||||
);
|
||||
const width = Math.max(Math.max(1, body.clientWidth - 30), minimumWidth);
|
||||
// 계획선(편집 가능) 상태에서는 측점 간격을 고정한다. 그 외(구버전·플레인 뷰)는
|
||||
// 예전처럼 화면 폭에 맞춰 펼친다.
|
||||
const stationIntervalM = stationInterval ?? alignment?.policy.station_interval_m;
|
||||
const originOffset = alignment ? PROFILE_ORIGIN_OFFSET_PX : 0;
|
||||
const width =
|
||||
alignment && stationIntervalM
|
||||
? fixedProfileWidth(longitudinal, stationIntervalM)
|
||||
: Math.max(
|
||||
Math.max(1, body.clientWidth - 15),
|
||||
longitudinalMinimumWidth(longitudinal, stationInterval),
|
||||
);
|
||||
const canvas = document.createElement("div");
|
||||
canvas.className = "b05-profile__canvas";
|
||||
canvas.style.width = `${width}px`;
|
||||
|
||||
const x = chainageMapper(longitudinal, width);
|
||||
const x = chainageMapper(longitudinal, width, originOffset);
|
||||
// 그래프 30% : 테이블 70% (상단 정보 라인은 본문 밖이라 애초에 빠져 있다).
|
||||
// 가로 스크롤바를 `overflow-x: scroll`로 항상 띄우므로 clientHeight에서 이미 빠져 있다.
|
||||
const available = Math.max(120, body.clientHeight);
|
||||
@@ -315,7 +341,8 @@ export function createRouteProfilePanel(
|
||||
stationInterval: stationInterval ?? alignment.policy.station_interval_m,
|
||||
width,
|
||||
height: tableHeight,
|
||||
cellWidth: stationCellWidth(longitudinal, x),
|
||||
// 측점 간격을 고정했으므로 셀 폭도 고정값으로 둔다(브라우저 폭과 무관하게 유지).
|
||||
cellWidth: STATION_SPACING_PX - CELL_GAP_PX,
|
||||
// 이름표 열을 그래프 좌측 여백과 같은 폭으로 맞춰야 그래프 시작점이 가려지지 않는다.
|
||||
labelWidth: LONG_PAD.left,
|
||||
rowCount: TABLE_ROW_COUNT,
|
||||
@@ -343,6 +370,7 @@ export function createRouteProfilePanel(
|
||||
chartHeight,
|
||||
width,
|
||||
designProfiles,
|
||||
originOffset,
|
||||
),
|
||||
);
|
||||
if (alignment) {
|
||||
|
||||
@@ -60,7 +60,10 @@
|
||||
min-height: 0;
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
padding-inline: 15px;
|
||||
/* 좌측 여백을 0으로 둔다. 여백이 있으면 sticky 이름표 열이 그 여백까지 못 덮어,
|
||||
가로 스크롤 시 셀 값이 이름표 왼쪽 틈으로 새어 보인다. 우측만 숨 돌릴 여백을 준다. */
|
||||
padding-left: 0;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/* 종단면도는 노선 전체 길이만큼 가로로 길어 스크롤바가 유일한 이동 수단이다.
|
||||
|
||||
@@ -86,6 +86,11 @@ export function createLongitudinalProfile(
|
||||
heightPx = LONG_HEIGHT,
|
||||
minimumWidthPx = widthPx,
|
||||
designProfiles: DesignProfile[] = [],
|
||||
/**
|
||||
* 데이터(측점선·프로파일)를 그래프 축 프레임 안쪽으로 더 들여쓰는 좌우 여백(px).
|
||||
* B05 도면 테이블과 정렬할 때 0측점을 이름표 열 바깥으로 밀어내는 데 쓴다(기본 0).
|
||||
*/
|
||||
originOffsetPx = 0,
|
||||
): HTMLElement {
|
||||
const samples = data.samples.filter(validElevation);
|
||||
if (samples.length < 2) return emptyView(L("B06_Profile_View_NoLongitudinal"));
|
||||
@@ -113,12 +118,15 @@ export function createLongitudinalProfile(
|
||||
const rawMax = yScaleOptions?.globalMaxElevation ?? Math.max(...elevations);
|
||||
const elevationMid = (rawMin + rawMax) / 2;
|
||||
const exaggeration = Math.max(verticalExaggeration, 0.1);
|
||||
const plotWidth = widthPx - LONG_PAD.left - LONG_PAD.right;
|
||||
// 데이터 영역은 축 프레임(LONG_PAD)보다 originOffsetPx만큼 더 좁게 잡아,
|
||||
// 0측점·종점이 좌우 끝에 딱 붙지 않고 반 칸씩 안으로 들어오게 한다.
|
||||
const plotWidth = widthPx - LONG_PAD.left - LONG_PAD.right - 2 * originOffsetPx;
|
||||
const plotHeight = heightPx - LONG_PAD.top - LONG_PAD.bottom;
|
||||
const elevationSpan = yScaleOptions
|
||||
? plotHeight / yScaleOptions.pixelsPerMeter
|
||||
: Math.max(rawMax - rawMin, 1);
|
||||
const x = (chainage: number) => LONG_PAD.left + (chainage / maxChainage) * plotWidth;
|
||||
const x = (chainage: number) =>
|
||||
LONG_PAD.left + originOffsetPx + (chainage / maxChainage) * plotWidth;
|
||||
const y = (elevation: number) =>
|
||||
LONG_PAD.top + ((elevationMid + elevationSpan / 2 - elevation) / elevationSpan) * plotHeight;
|
||||
const stationInterval = configuredStationInterval ?? inferStationInterval(data.stations);
|
||||
|
||||
Reference in New Issue
Block a user