- D03 평면 = 바탕(위성 · 바탕 겹 다섯) / 위(그은 선 · 재기) 두 장 · 마우스 움직임 · 누름 = 위만 · 화면 바뀜 = 둘 다 · rAF 한 화면 한 번 - PlanMap.redraw = 위 겹만 · redrawBase = 바탕까지(겹 켬끔 · 받음) - 공용 그리기에 선택 칸만 더함 — 조각 색인(part.lod · indexLayerLod 를 부른 레이어만) · 글 폭 기억(widths 인자) · B04 · B05 는 안 켬 - 한 판 한 번 긋기(d)는 뺌 — 반투명 선 겹침 색이 바뀌고(픽셀 14만 차) 래스터가 더 느림(36 → 60 ms) - 지킴 시험 test_582_contour_lod — 색인 그린 명령 = 지금 명령(27 줌 · 자리 · 닫힌 고리 · 짧은 조각) · 글 폭 기억 같은 글 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
190 lines
6.6 KiB
JavaScript
190 lines
6.6 KiB
JavaScript
/* 582 등고선 줌 단계 색인 헬퍼 — `B04_PreProcess_UI_MapRender_Prepare.ts` 의 `indexLayerLod` 를 붙인
|
|
* 레이어와 안 붙인 레이어를 같은 `drawPreparedLayer` 로 가짜 캔버스에 그려 그리기 명령(차례 · 좌표)을
|
|
* 줌 · 자리마다 맞대고 JSON 으로 낸다. 글 폭 기억(`drawPreparedLabels` widths)도 같은 방식.
|
|
*/
|
|
const path = require("path");
|
|
const { makeLoader } = require("./helper_ts_stub_loader.cjs");
|
|
|
|
const ROOT = path.join(__dirname, "..", "..");
|
|
const { load } = makeLoader({ root: ROOT, ui: () => ({ themeColor: () => "" }) });
|
|
const render = load("B04_PreProcess", "B04_PreProcess_UI_MapRender.ts");
|
|
const prep = load("B04_PreProcess", "logic", "B04_PreProcess_UI_MapRender_Prepare.ts");
|
|
|
|
const W = 1400;
|
|
const H = 900;
|
|
const META = { width_meters: 10000, height_meters: 8000 };
|
|
const NORMALIZER = { lonMin: 127, latMin: 36, lonRange: 0.1, latRange: 0.08, aspect: 1.25 };
|
|
|
|
/** 그리기 명령을 글로 적는 가짜 캔버스. */
|
|
function recorder() {
|
|
const log = [];
|
|
const rec =
|
|
(name) =>
|
|
(...args) =>
|
|
log.push(`${name}(${args.map((v) => (typeof v === "number" ? v.toFixed(6) : v)).join(",")})`);
|
|
let measured = 0;
|
|
return {
|
|
log,
|
|
measured: () => measured,
|
|
beginPath: rec("beginPath"),
|
|
moveTo: rec("moveTo"),
|
|
lineTo: rec("lineTo"),
|
|
closePath: rec("closePath"),
|
|
stroke: rec("stroke"),
|
|
arc: rec("arc"),
|
|
fill: rec("fill"),
|
|
save: rec("save"),
|
|
restore: rec("restore"),
|
|
translate: rec("translate"),
|
|
rotate: rec("rotate"),
|
|
strokeText: rec("strokeText"),
|
|
fillText: rec("fillText"),
|
|
measureText: (text) => {
|
|
measured += 1;
|
|
return { width: text.length * 6.1 };
|
|
},
|
|
};
|
|
}
|
|
|
|
/** 정해진 씨앗의 난수(결과가 매번 같게). */
|
|
function random(seed) {
|
|
let s = seed >>> 0;
|
|
return () => {
|
|
s = (s * 1664525 + 1013904223) >>> 0;
|
|
return s / 2 ** 32;
|
|
};
|
|
}
|
|
|
|
/** 등고선 비슷한 가닥 — 구불구불한 열린 선 · 닫힌 고리 · 짧은 조각. */
|
|
function collection() {
|
|
const rnd = random(582);
|
|
const features = [];
|
|
const lonLat = (x, y) => [127 + x * 0.1, 36 + (1 - y) * 0.08];
|
|
for (let f = 0; f < 60; f += 1) {
|
|
const kind = f % 3; // 0 열린 긴 선 · 1 닫힌 고리 · 2 짧은 조각
|
|
const n = kind === 2 ? 3 + Math.floor(rnd() * 40) : 200 + Math.floor(rnd() * 1800);
|
|
const cx = rnd();
|
|
const cy = rnd();
|
|
const pts = [];
|
|
if (kind === 1) {
|
|
const r = 0.02 + rnd() * 0.2;
|
|
for (let i = 0; i < n; i += 1) {
|
|
const a = (i / n) * Math.PI * 2;
|
|
const wobble = 1 + 0.15 * Math.sin(a * 7 + f) + 0.01 * (rnd() - 0.5);
|
|
pts.push(lonLat(cx + r * wobble * Math.cos(a), cy + r * wobble * Math.sin(a)));
|
|
}
|
|
pts.push(pts[0]);
|
|
} else {
|
|
let x = cx;
|
|
let y = cy;
|
|
let heading = rnd() * Math.PI * 2;
|
|
for (let i = 0; i < n; i += 1) {
|
|
heading += (rnd() - 0.5) * 0.6;
|
|
const step = 0.0004 + rnd() * 0.0008;
|
|
x += Math.cos(heading) * step;
|
|
y += Math.sin(heading) * step;
|
|
pts.push(lonLat(x, y));
|
|
}
|
|
}
|
|
const geometry =
|
|
kind === 1
|
|
? { type: "Polygon", coordinates: [pts] }
|
|
: f % 5 === 0
|
|
? { type: "MultiLineString", coordinates: [pts, pts.slice(0, Math.max(2, n >> 1))] }
|
|
: { type: "LineString", coordinates: pts };
|
|
features.push({ geometry, properties: { 등고수치: 400 + 5 * f } });
|
|
}
|
|
return { features };
|
|
}
|
|
|
|
const plain = prep.prepareLayer(collection(), NORMALIZER, ["등고수치"]);
|
|
const indexed = prep.indexLayerLod(prep.prepareLayer(collection(), NORMALIZER, ["등고수치"]));
|
|
|
|
let lodParts = 0;
|
|
let shortParts = 0;
|
|
for (const feature of indexed.features)
|
|
for (const part of feature.parts) {
|
|
if (part.lod) lodParts += 1;
|
|
else shortParts += 1;
|
|
}
|
|
|
|
const SCALES = [1, 1.7, 3, 7, 13, 50, 200, 1000, 2000];
|
|
const OFFSETS = [
|
|
[0, 0],
|
|
[300, -200],
|
|
[-900, 500],
|
|
];
|
|
const mapRect = render.computeMapRect(
|
|
{ width_meters: META.width_meters, height_meters: META.height_meters },
|
|
W,
|
|
H,
|
|
);
|
|
const cases = [];
|
|
for (const scale of SCALES)
|
|
for (const [ox, oy] of OFFSETS) {
|
|
const view = { width: W, height: H, scale, offsetX: ox, offsetY: oy, mapRect };
|
|
const a = recorder();
|
|
const b = recorder();
|
|
for (const everyM of [undefined, 25]) {
|
|
render.drawPreparedLayer(a, plain, view, "dot", everyM);
|
|
render.drawPreparedLayer(b, indexed, view, "dot", everyM);
|
|
}
|
|
const widths = new Map();
|
|
const la = recorder();
|
|
const lb = recorder();
|
|
render.drawPreparedLabels(la, plain, view, "#fff", 5, 0, "#000");
|
|
render.drawPreparedLabels(lb, indexed, view, "#fff", 5, 0, "#000", widths);
|
|
const measuredFirst = lb.measured();
|
|
render.drawPreparedLabels(lb, indexed, view, "#fff", 5, 0, "#000", widths); // 두 번째 = 기억만 씀
|
|
const measuredSecond = lb.measured() - measuredFirst;
|
|
const half = lb.log.length / 2;
|
|
cases.push({
|
|
scale,
|
|
offset: [ox, oy],
|
|
commands: a.log.length,
|
|
same: a.log.join("\n") === b.log.join("\n"),
|
|
vertices: a.log.filter((line) => line.startsWith("lineTo")).length,
|
|
labelsSame: la.log.join("\n") === lb.log.slice(0, half).join("\n"),
|
|
labelsRepeatSame: lb.log.slice(0, half).join("\n") === lb.log.slice(half).join("\n"),
|
|
labelMeasuredPlain: la.measured(),
|
|
labelMeasuredCached: measuredSecond,
|
|
});
|
|
}
|
|
|
|
// 색인이 훑는 정점 수 — 다 줌아웃(배율 1)의 허용 오차에서 전부 대비 몇 %
|
|
const tolerance = 0.75 / (mapRect.width * 1);
|
|
let all = 0;
|
|
let scanned = 0;
|
|
for (const feature of indexed.features)
|
|
for (const part of feature.parts) {
|
|
all += part.coords.length / 2;
|
|
const list = part.lod ? prep.pickLodList(part.lod, tolerance) : null;
|
|
scanned += list ? list.length : part.coords.length / 2;
|
|
}
|
|
|
|
// 고른 단 = 「무게 ≥ 오차」 정점을 모두 담음(여러 오차)
|
|
let supersetOk = true;
|
|
for (const tol of [1e-2, 1e-3, 3e-4, 1e-4, 2e-5, 5e-6, 1e-6, 1e-7])
|
|
for (const feature of indexed.features)
|
|
for (const part of feature.parts) {
|
|
if (!part.lod) continue;
|
|
const list = prep.pickLodList(part.lod, tol);
|
|
if (!list) continue;
|
|
const inList = new Set(list);
|
|
for (let v = 0; v < part.weights.length; v += 1)
|
|
if (part.weights[v] >= tol && !inList.has(v)) supersetOk = false;
|
|
for (let k = 1; k < list.length; k += 1) if (list[k] <= list[k - 1]) supersetOk = false;
|
|
}
|
|
|
|
process.stdout.write(
|
|
JSON.stringify({
|
|
lodParts,
|
|
shortParts,
|
|
closedParts: indexed.features.filter((f) => f.parts.some((p) => p.closed)).length,
|
|
cases,
|
|
scannedRatio: scanned / all,
|
|
supersetOk,
|
|
noLodFinest: prep.pickLodList(indexed.features[0].parts[0].lod, 1e-9) === null,
|
|
}),
|
|
);
|