- GL 겹 스물: 위성 조각 · 음영 · 급경사 · 산사태위험(image) · 산림입지 · 보호구역 · 띠 · 물 면 · 건물(fill + 테두리) · 지적 · 등고선 · 도로 · 임도 짐작 · 올린 임도망도 · 세류 · 단층 · 지질구조선 · 0차곡 · 산사태취약 필지(line · 점선 px 그대로) · 산사태취약 점 · 지명 점(circle) — 색 · 굵기 · 투명도 · 차례 = 옛 캔버스 그대로 - 음영 = 위성 위 「곱하기 0.75」와 같은 셈의 검은 반투명 그림(밝기 g → 투명도 0.75·(1−g)) · 위성 끔 = 그대로 - 등고 수치 · 지명 = symbol(겹치면 뺌 · 등고 글은 선 따라) · 글꼴 = 화면 Noto Sans KR(OFL · 자체 보관)을 그 자리에서 그림 (꼴에 글꼴 주소 없음 = 글꼴 조각 파일 없음 · 받기 전에 그 글자 글꼴 미리 받음) - 카메라 주인 = D03 화면 view · 돌림 하나 — 휠 · 가운데 끌기 · 돌림 · 맞춤 · 단축키 모두 draw() → glCamera - 처음 = GL(aislo:pref:d03.canvas = "1" 이면 옛 캔버스) · 겹 단추줄 끝 단추로 바꿈 · WebGL 못 열거나 잃음 = 저절로 옛 캔버스 + 알림 - GL 이면 캔버스 바탕 겹은 쉼(등고 주곡선 간격 셈만) · GL 이면 위성 한 장 그림 안 받음 - 산사태위험 그림도 경위도 귀퉁이(lonlat_corners 한 곳) · 묶음 꼴 표시 없는 합친 자료도 GL 이 받게 다시 쌈 - 캔버스 그리기 목록 = 새 D03_PlanAnalysis_UI_LayerPaint.ts(700줄 규칙) · 검증 창구 layers().gl = GL 겹별 그린 지물 수 - 잼(headless Edge 실 GPU · 검증_sub2_D · 겹 18 켬 = 자료 있는 겹 전부): 줌아웃 화면 간격 중앙 58.4 → 4.2 ms · 끊김 27 → 0/30 · 끌기 62.5 → 4.2 ms · 끊김 37 → 0/40 · 마우스 4.2 ms 끊김 0 그대로 · 그은 선 대 GL 0.019 px(돌림 30° 0.019 · 확대 0.077) - 시험: 캔버스 그리기를 옮긴 자리로 지킴 시험 넷 고침(581-2 · 582 · 584-1 · 584-3 — 그리기 차례 · 색 값이 LayerPaint 로) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
259 lines
9.0 KiB
TypeScript
259 lines
9.0 KiB
TypeScript
/* =============================================================================
|
|
* D03_PlanAnalysis_UI_LayerPaint.ts
|
|
* D03 바탕 겹 옛 캔버스 그리기(PLAN 569-2 · 581 · 584 · 586-2) — 그리기 차례 · 색 · 굵기 · 투명도.
|
|
* GL 바탕(586-2)이 같은 꼴을 MapLibre 로 그림(`D03_PlanAnalysis_UI_MapGl`) · 이것은 GL 을 못 쓰거나 끈 때.
|
|
* 부르는 곳 = `D03_PlanAnalysis_UI_Layers.ts`(받기 · 켬끔 · GL 이면 쉼).
|
|
* ========================================================================== */
|
|
|
|
import { themeColor } from "@ui/ui_template_palette";
|
|
import type { VWorldMeta } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
|
|
import {
|
|
createNormalizer,
|
|
drawPreparedLabels,
|
|
lonLatToScreen,
|
|
pickLevelStep,
|
|
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
|
|
import {
|
|
LINE_KEYS,
|
|
LINE_LAYERS,
|
|
ZONE_COLORS,
|
|
drawRaster,
|
|
fillLayer,
|
|
stroke,
|
|
type PlanLayerKey,
|
|
} from "./D03_PlanAnalysis_UI_LayerDefs";
|
|
import type { Prepared } from "./D03_PlanAnalysis_UI_LayerLegend";
|
|
import type { PlanMapLayer } from "./D03_PlanAnalysis_UI_Map";
|
|
|
|
/** 바탕 겹(아래 → 위) — 켬끔 · 받은 자료 · 위성 틀 · 위성 그림(음영 곱하기)을 그때그때 읽음. */
|
|
export function createCanvasLayers(opts: {
|
|
on: Record<PlanLayerKey, boolean>;
|
|
data: Partial<Prepared>;
|
|
meta: () => VWorldMeta | null;
|
|
satellite: () => HTMLImageElement | null;
|
|
}): PlanMapLayer[] {
|
|
const { on, data } = opts;
|
|
/** 등고 글 폭 기억(글꼴 하나 · 582). */
|
|
const labelWidths = new Map<string, number>();
|
|
return [
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.hillshade || !data.hillshade) return;
|
|
// 위성 위 = 곱하기(밝은 곳은 그대로 · 그늘만 짙게) · 위성 없으면 그대로
|
|
if (on.satellite && opts.satellite()) {
|
|
context.globalCompositeOperation = "multiply";
|
|
context.globalAlpha = 0.75;
|
|
}
|
|
drawRaster(context, map, data.hillshade);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.slope || !data.slope) return;
|
|
context.globalAlpha = 0.55;
|
|
drawRaster(context, map, data.slope);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.landslide || !data.landslide) return;
|
|
context.globalAlpha = 0.6;
|
|
drawRaster(context, map, data.landslide);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.soils || !data.soils) return;
|
|
for (const { layer, color } of data.soils.kinds) {
|
|
const fill = themeColor(color, "#facc15");
|
|
fillLayer(context, layer, map.view, fill, 0.35, { color: fill, width: 1.4 });
|
|
}
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.zones || !data.zones) return;
|
|
for (const [kind, layer] of data.zones.kinds) {
|
|
const color = themeColor(ZONE_COLORS[kind] ?? "--draw-warning", "#eab308");
|
|
fillLayer(context, layer, map.view, color, 0.22, { color, width: 1.6 });
|
|
}
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.bands || !data.bands) return;
|
|
const road = themeColor("--draw-map-band-road", "#facc15");
|
|
const stream = themeColor("--draw-map-band-stream", "#22d3ee");
|
|
fillLayer(context, data.bands.road, map.view, road, 0.16, {
|
|
color: road,
|
|
width: 1.2,
|
|
dash: [6, 4],
|
|
});
|
|
fillLayer(context, data.bands.stream, map.view, stream, 0.18, {
|
|
color: stream,
|
|
width: 1.2,
|
|
dash: [6, 4],
|
|
});
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.cadastral || !data.cadastral) return;
|
|
context.globalAlpha = 0.8;
|
|
stroke(context, map, data.cadastral, themeColor("--draw-map-parcel", "#f0abfc"), 0.8);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.water || !data.water) return;
|
|
const color = themeColor("--draw-map-water", "#0ea5e9");
|
|
fillLayer(context, data.water, map.view, color, 0.45, { color, width: 1 });
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.building || !data.building) return;
|
|
const color = themeColor("--draw-map-building", "#fb7185");
|
|
fillLayer(context, data.building, map.view, color, 0.7, {
|
|
color: "rgba(0, 0, 0, 0.55)",
|
|
width: 0.6,
|
|
});
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
const contour = on.contour && data.contour;
|
|
if (!contour) return;
|
|
const color = themeColor("--draw-map-contour", "#ffd68c");
|
|
const step = pickLevelStep(contour, map.view, 5);
|
|
context.globalAlpha = 0.7;
|
|
stroke(context, map, contour, color, 0.8, step);
|
|
stroke(context, map, contour, color, 1.6, 25); // 계곡선
|
|
context.globalAlpha = 1;
|
|
context.font = "10px system-ui, sans-serif";
|
|
context.textAlign = "center";
|
|
context.textBaseline = "middle";
|
|
drawPreparedLabels(
|
|
context,
|
|
contour,
|
|
map.view,
|
|
color,
|
|
25,
|
|
map.uprightRad,
|
|
"rgba(0, 0, 0, 0.6)",
|
|
labelWidths,
|
|
);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.road || !data.road) return;
|
|
stroke(context, map, data.road.all, "rgba(0, 0, 0, 0.55)", 3.2);
|
|
stroke(context, map, data.road.all, themeColor("--draw-map-road", "#f8fafc"), 1.6);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.forestRoad || !data.road) return;
|
|
context.setLineDash([7, 4]);
|
|
stroke(context, map, data.road.small, "rgba(0, 0, 0, 0.55)", 3.6);
|
|
stroke(context, map, data.road.small, themeColor("--draw-map-forest-road", "#f59e0b"), 2);
|
|
context.setLineDash([]);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
const got = on.forestRoadFile && data.forestRoadFile;
|
|
if (!got) return;
|
|
stroke(context, map, got.layer, "rgba(0, 0, 0, 0.55)", 4);
|
|
stroke(context, map, got.layer, themeColor("--draw-map-forest-road-file", "#ea580c"), 2.4);
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
if (!on.stream || !data.stream) return;
|
|
const color = themeColor("--draw-map-stream", "#38bdf8");
|
|
stroke(context, map, data.stream[0], color, 1);
|
|
stroke(context, map, data.stream[1], color, 2.4);
|
|
},
|
|
},
|
|
...LINE_KEYS.map((key): PlanMapLayer => ({
|
|
base: true,
|
|
draw: (context, map) => {
|
|
const got = on[key] && data[key];
|
|
if (!got) return;
|
|
const { color, width, dash } = LINE_LAYERS[key];
|
|
context.setLineDash(dash);
|
|
stroke(context, map, got.layer, "rgba(0, 0, 0, 0.55)", width + 1.6);
|
|
stroke(context, map, got.layer, themeColor(color, "#f43f5e"), width);
|
|
context.setLineDash([]);
|
|
},
|
|
})),
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
const got = on.landslideZones && data.landslideZones;
|
|
const meta = opts.meta();
|
|
if (!got || !meta) return;
|
|
const color = themeColor("--draw-map-landslide-zone", "#e11d48");
|
|
// 필지 = 「이 필지 안 어딘가」 테두리만(지정 넓이 ≪ 필지 · 칠하지 않음)
|
|
context.setLineDash([6, 3]);
|
|
stroke(context, map, got.parcels, "rgba(255, 255, 255, 0.7)", 3.2);
|
|
stroke(context, map, got.parcels, color, 1.8);
|
|
context.setLineDash([]);
|
|
const normalizer = createNormalizer(meta);
|
|
context.fillStyle = color;
|
|
context.strokeStyle = "#ffffff";
|
|
context.lineWidth = 1.5;
|
|
for (const point of got.info.points) {
|
|
const [x, y] = lonLatToScreen(normalizer, map.view, point.lon, point.lat);
|
|
if (x < -10 || y < -10 || x > map.view.width + 10 || y > map.view.height + 10) continue;
|
|
context.beginPath();
|
|
context.arc(x, y, 5, 0, Math.PI * 2);
|
|
context.fill();
|
|
context.stroke();
|
|
}
|
|
},
|
|
},
|
|
{
|
|
base: true,
|
|
draw: (context, map) => {
|
|
const meta = opts.meta();
|
|
if (!on.place || !data.place || !meta) return;
|
|
const normalizer = createNormalizer(meta);
|
|
context.font = "600 11px system-ui, sans-serif";
|
|
context.textAlign = "center";
|
|
context.textBaseline = "bottom";
|
|
context.lineWidth = 3;
|
|
context.strokeStyle = "rgba(0, 0, 0, 0.7)";
|
|
context.fillStyle = themeColor("--draw-map-place", "#f8fafc");
|
|
for (const place of data.place) {
|
|
const [x, y] = lonLatToScreen(normalizer, map.view, place.lon, place.lat);
|
|
if (x < -60 || y < -20 || x > map.view.width + 60 || y > map.view.height + 20) continue;
|
|
map.upright(context, x, y, () => {
|
|
context.beginPath();
|
|
context.arc(x, y, 2.5, 0, Math.PI * 2);
|
|
context.fill();
|
|
context.strokeText(place.name, x, y - 4);
|
|
context.fillText(place.name, x, y - 4);
|
|
});
|
|
}
|
|
},
|
|
},
|
|
];
|
|
}
|