fix(B04·B05): 지도 경위도 겹 · 위성 그림을 TM 겹 자리로 맞춤 (PLAN 589-1 · 요구 447 · 448)

- 서버 위성 메타 응답에 그림 상자 9×9 TM 격자(tm_grid · pyproj · 응답 때 셈 · 귀퉁이 1 cm 넘게 다르면 안 줌)
- 공용 createTmNormalizer(고를 칸 toNorm · 격자 쌍선형) · normOfLonLat · imageScreenMatrix · placeMapImage
- B04 하단 지도 · B05 배수유역도 · B05 평면 노선 편집이 TM 자리로 · 위성 그림 = 화면 가운데 1차 행렬(contain 뺌)
- Watershed 손 베낌 식 넷 · 줌아웃 한계 = 공용 식 · D03 길(createNormalizer) 바뀜 없음 · TM 길 손 안 댐
- 지킴 시험 test_589_tm_grid(Bessel · GRS80 · 격자 = pyproj 0.02 m · 그림 행렬 0.5 px · 옛 꼴 어긋남)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 20:21:39 +09:00
co-authored by Claude Opus 5.5
parent 7beca06a06
commit ba18b0f96d
14 changed files with 475 additions and 79 deletions
+43 -5
View File
@@ -1,6 +1,11 @@
import { themeColor } from "@ui/ui_template_palette";
import type { VWorldMeta } from "./B04_PreProcess_Api_Fetch";
import { pickLodList } from "./logic/B04_PreProcess_UI_MapRender_Prepare";
import {
imageScreenMatrix,
normOfLonLat,
pickLodList,
tmGridNormOf,
} from "./logic/B04_PreProcess_UI_MapRender_Prepare";
// 2D 지도 벡터 레이어 렌더 엔진.
// GeoJSON 좌표를 로드 시 1회만 정규화 맵 좌표(0~1)로 사전 투영해 두고,
@@ -84,6 +89,8 @@ export type Normalizer = {
lonRange: number;
latRange: number;
aspect: number;
/** 경위도 → 정규화 고를 칸(589-1 · `createTmNormalizer`) — 있으면 TM 자리 · 없으면 같은 네모 식. */
toNorm?: (lon: number, lat: number) => [number, number];
};
/** 뷰포트 안에서 지도 이미지가 차지하는 사각형(기존 getMapRect와 동일 계산). */
@@ -114,6 +121,13 @@ export function createNormalizer(meta: VWorldMeta): Normalizer {
};
}
/** 경위도 겹을 TM 겹 자리로 놓는 변환 계수(589-1 · B04 · B05) — 메타에 `tm_grid` 가 없으면 `createNormalizer` 그대로. */
export function createTmNormalizer(meta: VWorldMeta): Normalizer {
const base = createNormalizer(meta);
const toNorm = tmGridNormOf(meta);
return toNorm ? { ...base, toNorm } : base;
}
export function computeMapRect(meta: VWorldMeta | null, width: number, height: number): MapRect {
if (!meta) return { x: 0, y: 0, width, height };
const mapRatio = meta.width_meters / Math.max(meta.height_meters, 1);
@@ -338,11 +352,35 @@ export function lonLatToScreen(
lon: number,
lat: number,
): [x: number, y: number] {
return normalizedToScreen(
view,
(lon - normalizer.lonMin) / normalizer.lonRange,
1 - (lat - normalizer.latMin) / normalizer.latRange,
const [nx, ny] = normOfLonLat(normalizer, lon, lat);
return normalizedToScreen(view, nx, ny);
}
/**
* 위성 그림(`<img>` 제 크기 · 원점 왼위)을 화면에 놓음(589-1) — 그림 px → 경위도 → `normalizer` → 화면을
* 화면 가운데에서 1차로 맞춘 행렬. 그림 · 메타가 아직 없으면 그대로 둠.
*/
export function placeMapImage(
image: HTMLImageElement,
meta: VWorldMeta | null,
normalizer: Normalizer | null,
view: ViewState,
): void {
if (!meta || !normalizer || !image.naturalWidth || !image.naturalHeight) return;
const affine = affineOf(view);
const matrix = imageScreenMatrix(
meta,
normalizer,
image.naturalWidth,
image.naturalHeight,
(nx, ny) => [nx * affine.ax + affine.bx, ny * affine.ay + affine.by],
view.width / 2,
view.height / 2,
);
image.style.transform = `matrix(${matrix.join(", ")})`;
// 크게 당기면 배경 그림이 뭉개진다 — 흐림 보간을 끄고 픽셀을 그대로 보인다
// (2026-09-04 사용자 지시). 실제 크기는 축척 막대로 읽는다.
image.style.imageRendering = view.scale > 4 ? "pixelated" : "auto";
}
/**