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:
@@ -51,6 +51,105 @@ export function indexLayerLod(layer: PreparedLayer): PreparedLayer {
|
||||
return layer;
|
||||
}
|
||||
|
||||
/** 경위도 → 정규화(0~1) — 고를 칸 `toNorm` 이 있으면 그것(TM 자리) · 없으면 같은 네모 식. */
|
||||
export function normOfLonLat(normalizer: Normalizer, lon: number, lat: number): [number, number] {
|
||||
if (normalizer.toNorm) return normalizer.toNorm(lon, lat);
|
||||
return [
|
||||
(lon - normalizer.lonMin) / normalizer.lonRange,
|
||||
1 - (lat - normalizer.latMin) / normalizer.latRange,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 경위도 → TM 정규화(589-1) — 서버 `tm_grid`(그림 상자 경위도 고른 마디의 TM 값) 쌍선형 →
|
||||
* `((x − x_min)/W, 1 − (y − y_min)/H)` = `metricToScreen` 과 같은 자. 상자 밖은 가장자리 칸을 늘려 씀.
|
||||
* 격자가 없거나 모양이 틀리면 null.
|
||||
*/
|
||||
export function tmGridNormOf(
|
||||
meta: VWorldMeta,
|
||||
): ((lon: number, lat: number) => [number, number]) | null {
|
||||
const grid = meta.tm_grid;
|
||||
const n = grid?.n ?? 0;
|
||||
if (!grid || n < 2 || grid.x.length !== n * n || grid.y.length !== n * n) return null;
|
||||
const { x: gx, y: gy } = grid;
|
||||
const cells = n - 1;
|
||||
const lonRange = meta.lon_max - meta.lon_min || 1;
|
||||
const latRange = meta.lat_max - meta.lat_min || 1;
|
||||
const spanX = meta.width_meters || 1;
|
||||
const spanY = meta.height_meters || 1;
|
||||
return (lon, lat) => {
|
||||
const u = ((lon - meta.lon_min) / lonRange) * cells;
|
||||
const v = ((lat - meta.lat_min) / latRange) * cells;
|
||||
const i = Math.min(Math.max(Math.floor(u), 0), cells - 1);
|
||||
const j = Math.min(Math.max(Math.floor(v), 0), cells - 1);
|
||||
const fu = u - i;
|
||||
const fv = v - j;
|
||||
const k = j * n + i;
|
||||
const w00 = (1 - fu) * (1 - fv);
|
||||
const w10 = fu * (1 - fv);
|
||||
const w01 = (1 - fu) * fv;
|
||||
const w11 = fu * fv;
|
||||
const x = w00 * gx[k] + w10 * gx[k + 1] + w01 * gx[k + n] + w11 * gx[k + n + 1];
|
||||
const y = w00 * gy[k] + w10 * gy[k + 1] + w01 * gy[k + n] + w11 * gy[k + n + 1];
|
||||
return [(x - meta.x_min) / spanX, 1 - (y - meta.y_min) / spanY];
|
||||
};
|
||||
}
|
||||
|
||||
const mercatorOf = (lat: number): number => Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360));
|
||||
const latOfMercator = (m: number): number => (Math.atan(Math.exp(m)) * 360) / Math.PI - 90;
|
||||
|
||||
/**
|
||||
* 위성 그림 px → 화면 CSS 행렬 `[a, b, c, d, e, f]`(원점 그림 왼위 · 589-1).
|
||||
* 그림 = 웹 메르카토르 타일 이음(가로 경도 선형 · 세로 메르카토르 y 선형) → 경위도 → `normalizer` →
|
||||
* `toScreen`. 이 길을 화면 (`centerX`, `centerY`) 에 닿는 그림 px 에서 1차로 맞춤 — 옮김 · 확대마다 다시 셈.
|
||||
*/
|
||||
export function imageScreenMatrix(
|
||||
meta: VWorldMeta,
|
||||
normalizer: Normalizer,
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
toScreen: (nx: number, ny: number) => [number, number],
|
||||
centerX: number,
|
||||
centerY: number,
|
||||
): [number, number, number, number, number, number] {
|
||||
const lonRange = meta.lon_max - meta.lon_min;
|
||||
const mTop = mercatorOf(meta.lat_max);
|
||||
const mSpan = mTop - mercatorOf(meta.lat_min);
|
||||
const screenOf = (u: number, v: number): [number, number] => {
|
||||
const lon = meta.lon_min + (u / imageWidth) * lonRange;
|
||||
const lat = latOfMercator(mTop - (v / imageHeight) * mSpan);
|
||||
const [nx, ny] = normOfLonLat(normalizer, lon, lat);
|
||||
return toScreen(nx, ny);
|
||||
};
|
||||
let u = imageWidth / 2;
|
||||
let v = imageHeight / 2;
|
||||
let a = 1;
|
||||
let b = 0;
|
||||
let c = 0;
|
||||
let d = 1;
|
||||
let at = screenOf(u, v);
|
||||
// 뉴턴 셋 — 길이 거의 1차라 한두 번에 화면 가운데 그림 px 가 잡힘.
|
||||
for (let step = 0; step < 3; step += 1) {
|
||||
const h = 0.5;
|
||||
const du0 = screenOf(u - h, v);
|
||||
const du1 = screenOf(u + h, v);
|
||||
const dv0 = screenOf(u, v - h);
|
||||
const dv1 = screenOf(u, v + h);
|
||||
a = (du1[0] - du0[0]) / (2 * h);
|
||||
b = (du1[1] - du0[1]) / (2 * h);
|
||||
c = (dv1[0] - dv0[0]) / (2 * h);
|
||||
d = (dv1[1] - dv0[1]) / (2 * h);
|
||||
const det = a * d - b * c;
|
||||
if (!Number.isFinite(det) || Math.abs(det) < 1e-12) break;
|
||||
const ex = centerX - at[0];
|
||||
const ey = centerY - at[1];
|
||||
u += (d * ex - c * ey) / det;
|
||||
v += (a * ey - b * ex) / det;
|
||||
at = screenOf(u, v);
|
||||
}
|
||||
return [a, b, c, d, at[0] - a * u - c * v, at[1] - b * u - d * v];
|
||||
}
|
||||
|
||||
function isPoint(value: unknown): value is [number, number] {
|
||||
return Array.isArray(value) && typeof value[0] === "number" && typeof value[1] === "number";
|
||||
}
|
||||
@@ -62,8 +161,9 @@ function projectRing(ring: unknown, normalizer: Normalizer): Float64Array | null
|
||||
let count = 0;
|
||||
for (const point of ring) {
|
||||
if (!isPoint(point)) continue;
|
||||
coords[count * 2] = (point[0] - normalizer.lonMin) / normalizer.lonRange;
|
||||
coords[count * 2 + 1] = 1 - (point[1] - normalizer.latMin) / normalizer.latRange;
|
||||
const [nx, ny] = normOfLonLat(normalizer, point[0], point[1]);
|
||||
coords[count * 2] = nx;
|
||||
coords[count * 2 + 1] = ny;
|
||||
count += 1;
|
||||
}
|
||||
if (count === 0) return null;
|
||||
@@ -166,10 +266,7 @@ function labelAnchorOf(geometry: GeoJsonGeometry, normalizer: Normalizer): [numb
|
||||
if (!Array.isArray(line) || line.length === 0) return null;
|
||||
const mid = line[Math.floor(line.length / 2)];
|
||||
if (!isPoint(mid)) return null;
|
||||
return [
|
||||
(mid[0] - normalizer.lonMin) / normalizer.lonRange,
|
||||
1 - (mid[1] - normalizer.latMin) / normalizer.latRange,
|
||||
];
|
||||
return normOfLonLat(normalizer, mid[0], mid[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user