Merge remote-tracking branch 'origin/sub_laptop_4' into main_laptop_1
This commit is contained in:
@@ -18,6 +18,8 @@ import math
|
||||
from typing import Any
|
||||
from uuid import uuid5
|
||||
|
||||
from shapely.geometry import LineString
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad import (
|
||||
_ENTITY_NS,
|
||||
DRAWING_FORMAT,
|
||||
@@ -74,7 +76,9 @@ _FONT_SIZE = 2.2
|
||||
# 유역 번호는 해칭 위에 얹히므로 흰 원판을 깔고 그 안에 적는다(2026-08-30 사용자 지시).
|
||||
_NUMBER_FONT_SIZE = 4.5
|
||||
_BADGE_RADIUS_MM = 3.6
|
||||
_BADGE_FILL = "#ffffff"
|
||||
# 흰색(#ffffff)은 CAD 자동색이라 흰 바탕에서 검정으로 바뀌어 숫자가 묻혔다. 원판은 CAD 가
|
||||
# 밝기를 손대지 않는 회색(단순 휘도 0.58 이하)으로 둔다 — 흰·검은 바탕 모두 숫자 대비 5.9:1.
|
||||
_BADGE_FILL = "#909090"
|
||||
_BADGE_TEXT_COLOR = "#111111"
|
||||
_BADGE_SEGMENTS = 24
|
||||
_TITLE_FONT_SIZE = 7.0
|
||||
@@ -94,6 +98,9 @@ _COMPASS_SIZE = 26.0 # 방위표 긴 변(mm)
|
||||
_COMPASS_GAP = 6.0 # 방위표와 첫 정보표 사이(mm)
|
||||
# 노선은 지형·유역 위에 얹혀야 보인다 — 굵기도 배경선보다 굵게 한다.
|
||||
_ROUTE_WIDTH = 3
|
||||
# 배경선(등고선·세류선)은 종이에서 이보다 작게 꺾이는 점을 뺀다 — 펜 굵기보다 가늘어
|
||||
# 안 보이는 점이다. 그대로 두면 수십만 선분이 서버 응답·브라우저 메모리를 다 먹는다.
|
||||
_BACKGROUND_SIMPLIFY_MM = 0.1
|
||||
|
||||
|
||||
def map_area_mm() -> tuple[float, float]:
|
||||
@@ -109,6 +116,13 @@ def map_area_mm() -> tuple[float, float]:
|
||||
)
|
||||
|
||||
|
||||
def _thinned(points: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
||||
"""종이 mm 점열에서 `_BACKGROUND_SIMPLIFY_MM` 아래 굴곡을 뺀다(양 끝점은 남는다)."""
|
||||
if len(points) < 3:
|
||||
return points
|
||||
return list(LineString(points).simplify(_BACKGROUND_SIMPLIFY_MM).coords)
|
||||
|
||||
|
||||
def _number(value: Any, fallback: float = 0.0) -> float:
|
||||
return float(value) if isinstance(value, (int, float)) else fallback
|
||||
|
||||
@@ -169,7 +183,7 @@ def _hatch_entity(
|
||||
def _number_badge_entities(
|
||||
drawing_id: str, number: int, center: tuple[float, float], color: str
|
||||
) -> list[dict[str, Any]]:
|
||||
"""유역 번호를 동그라미 배지로 그린다 — 흰 원판 + 유역색 테두리 + 검은 숫자.
|
||||
"""유역 번호를 동그라미 배지로 그린다 — 회색 원판 + 유역색 테두리 + 검은 숫자.
|
||||
|
||||
해칭 위에 그냥 문자만 올리면 무늬에 묻혀 읽히지 않는다. 원판은 solid 해치라
|
||||
화면(어두운 배경)과 인쇄(흰 배경) 어느 쪽에서도 숫자가 남는다.
|
||||
@@ -298,7 +312,7 @@ def build_watershed_drawing(
|
||||
for index, line in enumerate(contours):
|
||||
contour = polyline_entity(
|
||||
drawing_id,
|
||||
[paper(point) for point in line],
|
||||
_thinned([paper(point) for point in line]),
|
||||
CONTOUR_LAYER_ID,
|
||||
CONTOUR_COLOR,
|
||||
suffix=f":contour:{index}",
|
||||
@@ -308,7 +322,7 @@ def build_watershed_drawing(
|
||||
for index, line in enumerate(streams):
|
||||
stream = polyline_entity(
|
||||
drawing_id,
|
||||
[paper(point) for point in line],
|
||||
_thinned([paper(point) for point in line]),
|
||||
STREAM_LAYER_ID,
|
||||
STREAM_COLOR,
|
||||
suffix=f":stream:{index}",
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
넣을 것. 연속지적도에 지번 정보가 있는지 확인하고, 없으면 일단 그림만」.
|
||||
|
||||
지번은 있다(2026-09-04 실측: 저장된 연속지적도 GeoJSON 필지마다 `jibun`·`jimok`·
|
||||
`parea`·`owner_nm` + 시도·시군구·읍면동·리 이름). 이번 판은 **지번만** 적는다 —
|
||||
지목·면적·소유 구분은 용지 조서(표)에서 쓸 값이라 도면에는 넣지 않는다.
|
||||
`parea`·`owner_nm` + 시도·시군구·읍면동·리 이름). 필지마다 **지번·지목·면적·소유자**
|
||||
네 줄을 적는다(사용자: 「연속지적도에 해당 정보를 띄우고 판단」) — 빈 값은 그 줄만 비운다.
|
||||
|
||||
축척·도곽·장 나눔은 계획평면도와 **같다**(1/1,200 고정). 배경도 같은 창구를 쓴다.
|
||||
|
||||
@@ -65,10 +65,8 @@ _ROUTE_WIDTH = 3
|
||||
_SGG_WIDTH = 3
|
||||
_EMD_WIDTH = 2
|
||||
_JIBUN_FONT_SIZE = 1.8
|
||||
_INFO_LINE_GAP = _JIBUN_FONT_SIZE * 1.3 # 필지 정보 줄 간격(mm)
|
||||
_LEGEND_FONT_SIZE = 2.4
|
||||
# 지번을 적을 최소 필지 크기(종이 mm) — 이보다 작으면 글자가 겹쳐 읽히지 않는다.
|
||||
_JIBUN_MIN_W_MM = 6.0
|
||||
_JIBUN_MIN_H_MM = 3.0
|
||||
|
||||
_LEGEND_ROW_H = 6.0
|
||||
_LEGEND_SAMPLE_W = 12.0
|
||||
@@ -92,6 +90,25 @@ def _ring_center(ring: list[tuple[float, float]]) -> tuple[float, float]:
|
||||
return ((min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0)
|
||||
|
||||
|
||||
def _parcel_info_lines(props: dict[str, Any]) -> list[str]:
|
||||
"""지번·지목·면적·소유자 네 줄 — 값이 없으면 그 줄은 빈 문자열."""
|
||||
|
||||
def text(value: Any) -> str:
|
||||
return value.strip() if isinstance(value, str) else ""
|
||||
|
||||
area = props.get("parea")
|
||||
try:
|
||||
area_text = f"{float(area):,.0f}㎡" if area not in (None, "") else ""
|
||||
except (TypeError, ValueError):
|
||||
area_text = text(area)
|
||||
return [
|
||||
text(props.get("jibun")),
|
||||
text(props.get("jimok")),
|
||||
area_text,
|
||||
text(props.get("owner_nm")),
|
||||
]
|
||||
|
||||
|
||||
def _legend_entities(drawing_id: str, origin: tuple[float, float]) -> list[dict[str, Any]]:
|
||||
"""범례 — 유역도에서 유역 정보표가 있던 자리(오른쪽 칸)에 놓는다."""
|
||||
entities: list[dict[str, Any]] = []
|
||||
@@ -197,48 +214,48 @@ def build_landuse_drawing(
|
||||
drawing_id, contours, CONTOUR_LAYER_ID, CONTOUR_COLOR, 1, None, paper, "contour"
|
||||
)
|
||||
)
|
||||
# 필지 경계 + 지번.
|
||||
jibun: list[dict[str, Any]] = []
|
||||
# 필지 경계 + 필지 정보. 도곽에 잘려 조각이 여럿인 필지도 정보는 한 번만 —
|
||||
# 가장 큰 조각 가운데에 적는다(구역 안 필지 수 = 정보 묶음 수).
|
||||
spots: dict[str, tuple[float, tuple[float, float], dict[str, Any]]] = {}
|
||||
for index, parcel in enumerate(parcels):
|
||||
ring = parcel.get("ring") or []
|
||||
props = parcel.get("props") or {}
|
||||
label_at = parcel.get("label_at")
|
||||
if len(ring) >= 2:
|
||||
paper_ring = [paper(point) for point in ring]
|
||||
outline = polyline_entity(
|
||||
drawing_id,
|
||||
[paper(point) for point in ring],
|
||||
PARCEL_LAYER_ID,
|
||||
PARCEL_COLOR,
|
||||
suffix=f":parcel:{index}",
|
||||
drawing_id, paper_ring, PARCEL_LAYER_ID, PARCEL_COLOR, suffix=f":parcel:{index}"
|
||||
)
|
||||
if outline:
|
||||
entities.append(outline)
|
||||
elif label_at is None:
|
||||
continue
|
||||
if label_at is not None:
|
||||
# 도곽을 통째로 감싼 필지 — 경계선이 없으니 지정된 자리에 지번만 적는다.
|
||||
center = paper(tuple(label_at))
|
||||
else:
|
||||
paper_ring = [paper(point) for point in ring]
|
||||
width = max(x for x, _y in paper_ring) - min(x for x, _y in paper_ring)
|
||||
height = max(y for _x, y in paper_ring) - min(y for _x, y in paper_ring)
|
||||
# 작은 필지는 지번을 솎는다 — 글자가 겹치면 큰 필지 것까지 못 읽는다.
|
||||
if width < _JIBUN_MIN_W_MM or height < _JIBUN_MIN_H_MM:
|
||||
continue
|
||||
center = _ring_center(paper_ring)
|
||||
text = (parcel.get("props") or {}).get("jibun")
|
||||
if not isinstance(text, str) or not text:
|
||||
size, center = width * height, _ring_center(paper_ring)
|
||||
elif label_at is not None:
|
||||
# 도곽을 통째로 감싼 필지 — 경계선이 없으니 지정된 자리에 정보만 적는다.
|
||||
size, center = 0.0, paper(tuple(label_at))
|
||||
else:
|
||||
continue
|
||||
jibun.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:jibun:{index}",
|
||||
text,
|
||||
center[0],
|
||||
center[1],
|
||||
JIBUN_LAYER_ID,
|
||||
_JIBUN_FONT_SIZE,
|
||||
JIBUN_COLOR,
|
||||
)
|
||||
)
|
||||
key = str(props.get("pnu") or f"#{index}")
|
||||
if key not in spots or size > spots[key][0]:
|
||||
spots[key] = (size, center, props)
|
||||
jibun: list[dict[str, Any]] = []
|
||||
for key, (_size, center, props) in spots.items():
|
||||
lines = _parcel_info_lines(props)
|
||||
top = center[1] + (len(lines) - 1) / 2.0 * _INFO_LINE_GAP
|
||||
for row, text in enumerate(lines):
|
||||
if text:
|
||||
jibun.append(
|
||||
_text_entity(
|
||||
f"{drawing_id}:parcel-info:{key}:{row}",
|
||||
text,
|
||||
center[0],
|
||||
top - row * _INFO_LINE_GAP,
|
||||
JIBUN_LAYER_ID,
|
||||
_JIBUN_FONT_SIZE,
|
||||
JIBUN_COLOR,
|
||||
)
|
||||
)
|
||||
# 행정 경계는 필지 위에, 노선은 그 위에 — 아래에 깔리면 필지 선에 묻힌다.
|
||||
entities.extend(
|
||||
_boundary_entities(
|
||||
@@ -267,7 +284,7 @@ def build_landuse_drawing(
|
||||
)
|
||||
if route:
|
||||
entities.append(route)
|
||||
entities.extend(jibun) # 지번은 가장 위 — 선에 가리면 못 읽는다.
|
||||
entities.extend(jibun) # 필지 정보는 가장 위 — 선에 가리면 못 읽는다.
|
||||
|
||||
# 오른쪽 칸: 방위표가 맨 위, 그 아래로 범례(유역도에서 유역 정보표가 있던 자리).
|
||||
if map_bbox:
|
||||
@@ -323,7 +340,7 @@ def build_landuse_drawing(
|
||||
"layers": [
|
||||
_layer(CONTOUR_LAYER_ID, "등고선", locked=True),
|
||||
_layer(PARCEL_LAYER_ID, "필지 경계"),
|
||||
_layer(JIBUN_LAYER_ID, "지번"),
|
||||
_layer(JIBUN_LAYER_ID, "필지 정보(지번·지목·면적·소유자)"),
|
||||
_layer(EMD_LAYER_ID, "읍면동·리 경계"),
|
||||
_layer(SGG_LAYER_ID, "시군구 경계"),
|
||||
_layer(ROUTE_LAYER_ID, "계획노선"),
|
||||
|
||||
@@ -11,6 +11,7 @@ from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
from pyproj import Transformer
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Router_Watershed import CONTOUR_FILE, STREAM_FILE
|
||||
@@ -186,32 +187,50 @@ def _too_far_from_route(
|
||||
@lru_cache(maxsize=8)
|
||||
def _metric_lines_cached(
|
||||
path_str: str, mtime_ns: int, crs: str
|
||||
) -> tuple[tuple[tuple[float, float], ...], ...]:
|
||||
) -> tuple[tuple[np.ndarray, ...], np.ndarray]:
|
||||
"""도엽 GeoJSON 한 벌을 사업지 좌표계(m) 선 목록으로 돌려 **캐시**한다.
|
||||
|
||||
같은 배경을 유역도와 계획평면도가 나눠 쓴다 — 도면마다 다시 읽고 다시 투영하면
|
||||
한 장 여는 데 수 초가 걸린다(등고선 수만 점). 파일이 바뀌면 mtime 이 달라져
|
||||
캐시가 저절로 갈린다(`_read_template` 와 같은 방식).
|
||||
|
||||
선은 (n, 2) 실수 배열로 쥔다 — 파이썬 튜플이면 등고선 한 벌이 수백 MB 를 붙든다.
|
||||
둘째 값은 선마다 (min_x, min_y, max_x, max_y) — 도곽 밖 선을 자르기 전에 거른다.
|
||||
"""
|
||||
to_metric = Transformer.from_crs("EPSG:4326", crs, always_xy=True)
|
||||
lines: list[tuple[tuple[float, float], ...]] = []
|
||||
lines: list[np.ndarray] = []
|
||||
for feature in _geojson_features(Path(path_str)):
|
||||
for part in _geometry_lines(feature.get("geometry")):
|
||||
converted = tuple(
|
||||
(float(x), float(y))
|
||||
for x, y in (to_metric.transform(point[0], point[1]) for point in part)
|
||||
)
|
||||
if len(converted) >= 2:
|
||||
lines.append(converted)
|
||||
return tuple(lines)
|
||||
if len(part) < 2:
|
||||
continue
|
||||
lonlat = np.array([(point[0], point[1]) for point in part], dtype=float)
|
||||
x, y = to_metric.transform(lonlat[:, 0], lonlat[:, 1])
|
||||
lines.append(np.column_stack((x, y)))
|
||||
bounds = np.array(
|
||||
[(*line.min(axis=0), *line.max(axis=0)) for line in lines], dtype=float
|
||||
).reshape(-1, 4)
|
||||
return tuple(lines), bounds
|
||||
|
||||
|
||||
def _metric_lines(path: Path, crs: str) -> list[list[tuple[float, float]]]:
|
||||
"""캐시된 배경 선을 쓰기 좋은 형태로 낸다. 파일이 없으면 빈 목록."""
|
||||
def _metric_lines(
|
||||
path: Path, crs: str, box: tuple[float, float, float, float] | None = None
|
||||
) -> list[list[tuple[float, float]]]:
|
||||
"""캐시된 배경 선 중 box 에 걸치는 것만 낸다. 파일이 없으면 빈 목록."""
|
||||
if not path.is_file():
|
||||
return []
|
||||
cached = _metric_lines_cached(str(path), path.stat().st_mtime_ns, crs)
|
||||
return [list(line) for line in cached]
|
||||
lines, bounds = _metric_lines_cached(str(path), path.stat().st_mtime_ns, crs)
|
||||
if box is None:
|
||||
picked = range(len(lines))
|
||||
else:
|
||||
min_x, min_y, max_x, max_y = box
|
||||
hit = (
|
||||
(bounds[:, 2] >= min_x)
|
||||
& (bounds[:, 0] <= max_x)
|
||||
& (bounds[:, 3] >= min_y)
|
||||
& (bounds[:, 1] <= max_y)
|
||||
)
|
||||
picked = np.flatnonzero(hit)
|
||||
return [list(map(tuple, lines[index].tolist())) for index in picked]
|
||||
|
||||
|
||||
def map_background(
|
||||
@@ -248,7 +267,7 @@ def map_background(
|
||||
background: dict[str, list[list[tuple[float, float]]]] = {}
|
||||
for key, filename in (("contours", CONTOUR_FILE), ("streams", STREAM_FILE)):
|
||||
lines: list[list[tuple[float, float]]] = []
|
||||
for line in _metric_lines(sheet_dir / filename, crs):
|
||||
for line in _metric_lines(sheet_dir / filename, crs, box):
|
||||
lines.extend(clip_line_to_box(line, box))
|
||||
background[key] = lines
|
||||
return background
|
||||
@@ -360,7 +379,7 @@ def _clip_rings(
|
||||
) -> list[list[tuple[float, float]]]:
|
||||
"""행정구역 경계를 사업지 좌표계로 돌려 도곽 범위로 절취한다(속성은 안 씀)."""
|
||||
rings: list[list[tuple[float, float]]] = []
|
||||
for ring in _metric_lines(path, crs):
|
||||
for ring in _metric_lines(path, crs, box):
|
||||
rings.extend(clip_line_to_box(ring, box))
|
||||
return rings
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Box, Point, Polygon, Segment } from '@flatten-js/core';
|
||||
import type { Shape, SnapPoint } from '../App.types';
|
||||
import { SnapPointType } from '../App.types';
|
||||
import type { DrawController } from '../drawControllers/DrawController';
|
||||
import { hatchSpans } from '../helpers/geometry/hatch-lines';
|
||||
import { type Span, hatchSpans } from '../helpers/geometry/hatch-lines';
|
||||
import { getExportColor } from '../helpers/get-export-color';
|
||||
import { mirrorPointOverAxis } from '../helpers/mirror-point-over-axis';
|
||||
import { scalePoint } from '../helpers/scale-point';
|
||||
@@ -50,6 +50,8 @@ export class HatchEntity implements Entity {
|
||||
|
||||
private points: Point[];
|
||||
public options: HatchOptions;
|
||||
/** 패턴 선분 — 줌·이동마다 다시 그리므로 경계·무늬가 그대로면 지난 계산을 쓴다. */
|
||||
private spanCache?: { key: string; points: Point[]; spans: Span[] };
|
||||
|
||||
constructor(layerId: string, points: Point[], options?: Partial<HatchOptions>) {
|
||||
this.layerId = layerId;
|
||||
@@ -77,14 +79,8 @@ export class HatchEntity implements Entity {
|
||||
this.drawGradient(drawController);
|
||||
} else {
|
||||
drawController.setLineStyles(highlighted, selected, this.options.color, this.lineWidth);
|
||||
const angles =
|
||||
this.options.style === 'cross'
|
||||
? [this.options.angle, this.options.angle + Math.PI / 2]
|
||||
: [this.options.angle];
|
||||
for (const angle of angles) {
|
||||
for (const [start, end] of hatchSpans(this.points, angle, this.options.spacing)) {
|
||||
drawController.drawLine(start, end);
|
||||
}
|
||||
for (const [start, end] of this.patternSpans()) {
|
||||
drawController.drawLine(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +97,18 @@ export class HatchEntity implements Entity {
|
||||
}
|
||||
}
|
||||
|
||||
private patternSpans(): Span[] {
|
||||
const { style, angle, spacing } = this.options;
|
||||
const key = `${style}:${angle}:${spacing}`;
|
||||
if (this.spanCache?.points === this.points && this.spanCache.key === key) {
|
||||
return this.spanCache.spans;
|
||||
}
|
||||
const angles = style === 'cross' ? [angle, angle + Math.PI / 2] : [angle];
|
||||
const spans = angles.flatMap((value) => hatchSpans(this.points, value, spacing));
|
||||
this.spanCache = { key, points: this.points, spans };
|
||||
return spans;
|
||||
}
|
||||
|
||||
/** 그라데이션 — 촘촘한 스캔선의 색을 조금씩 바꿔 표현한다 */
|
||||
private drawGradient(drawController: DrawController): void {
|
||||
const box = this.getBoundingBox();
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
"""B07 유역도 풍선 — 흰·검은 바탕 어느 쪽에서도 숫자가 읽히는 색 (59-5).
|
||||
|
||||
CAD 그리기 규칙(openwebcad `theme.ts` paintColor): 자동색(#000000·#ffffff·#f5f7fa)은
|
||||
바탕 따라 흑백이 뒤집히고, 흰 바탕에서는 단순 휘도 0.58 초과 색을 어둡게 누른다.
|
||||
"""
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Basin import _BADGE_FILL, _BADGE_TEXT_COLOR
|
||||
|
||||
_AUTO = {"#000000", "#ffffff", "#f5f7fa"}
|
||||
|
||||
|
||||
def _rgb(color: str) -> tuple[int, int, int]:
|
||||
return tuple(int(color[index : index + 2], 16) for index in (1, 3, 5))
|
||||
|
||||
|
||||
def _wcag(color: str) -> float:
|
||||
def channel(value: int) -> float:
|
||||
value /= 255
|
||||
return value / 12.92 if value <= 0.03928 else ((value + 0.055) / 1.055) ** 2.4
|
||||
|
||||
r, g, b = (channel(v) for v in _rgb(color))
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
|
||||
def test_badge_colors_survive_both_themes() -> None:
|
||||
for color in (_BADGE_FILL, _BADGE_TEXT_COLOR):
|
||||
assert color.lower() not in _AUTO
|
||||
r, g, b = _rgb(color)
|
||||
assert (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 <= 0.58
|
||||
fill, text = _wcag(_BADGE_FILL), _wcag(_BADGE_TEXT_COLOR)
|
||||
assert (max(fill, text) + 0.05) / (min(fill, text) + 0.05) >= 4.5
|
||||
@@ -0,0 +1,37 @@
|
||||
"""B07 유역도 메모리 — 배경선 배열 캐시 · 도곽 밖 선 거름 · 종이 0.1 mm 솎기 (59-4)."""
|
||||
|
||||
import json
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Basin import _thinned
|
||||
from B07_DesignDetail.B07_DesignDetail_Router_Support_Basin import _metric_lines
|
||||
|
||||
|
||||
def test_metric_lines_filters_by_box(tmp_path) -> None:
|
||||
path = tmp_path / "lines.geojson"
|
||||
inside = [[0.0, 0.0], [1.0, 1.0], [2.0, 0.5]]
|
||||
outside = [[50.0, 50.0], [51.0, 51.0]]
|
||||
path.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{"geometry": {"type": "LineString", "coordinates": inside}},
|
||||
{"geometry": {"type": "LineString", "coordinates": outside}},
|
||||
],
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
every = _metric_lines(path, "EPSG:4326")
|
||||
assert every == [[tuple(p) for p in inside], [tuple(p) for p in outside]]
|
||||
assert _metric_lines(path, "EPSG:4326", (-1.0, -1.0, 5.0, 5.0)) == every[:1]
|
||||
assert _metric_lines(tmp_path / "none.geojson", "EPSG:4326") == []
|
||||
|
||||
|
||||
def test_thinned_drops_sub_pen_wiggles() -> None:
|
||||
points = [(x * 0.5, 0.02 if x % 2 else 0.0) for x in range(21)]
|
||||
thinned = _thinned(points)
|
||||
assert thinned[0] == points[0] and thinned[-1] == points[-1]
|
||||
assert len(thinned) == 2
|
||||
bend = [(0.0, 0.0), (5.0, 1.0), (10.0, 0.0)]
|
||||
assert _thinned(bend) == bend
|
||||
@@ -0,0 +1,37 @@
|
||||
"""B07 용지도 — 필지마다 지번·지목·면적·소유자 한 묶음 · 빈 값은 빈 줄 (59-6)."""
|
||||
|
||||
import pytest
|
||||
|
||||
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Landuse import (
|
||||
JIBUN_LAYER_ID,
|
||||
MM,
|
||||
build_landuse_drawing,
|
||||
)
|
||||
|
||||
|
||||
def _square(x0: float, size: float) -> list[tuple[float, float]]:
|
||||
return [(x0, 0.0), (x0 + size, 0.0), (x0 + size, size), (x0, size), (x0, 0.0)]
|
||||
|
||||
|
||||
def test_one_label_group_per_parcel() -> None:
|
||||
full = {"pnu": "A", "jibun": "95전", "jimok": "전", "parea": 4916, "owner_nm": "개인"}
|
||||
split = {"pnu": "B", "jibun": "산1임", "jimok": "임야", "parea": 12.4, "owner_nm": ""}
|
||||
parcels = [
|
||||
{"ring": _square(0.0, 40.0), "props": full},
|
||||
# 도곽에 잘려 두 조각이 된 필지 — 정보는 큰 조각에 한 번만
|
||||
{"ring": _square(100.0, 10.0), "props": split},
|
||||
{"ring": _square(200.0, 30.0), "props": split},
|
||||
# 도곽을 통째로 감싼 필지 — 경계 없이 정보만
|
||||
{"ring": [], "props": {"pnu": "C", "jibun": "산77"}, "label_at": (500.0, 500.0)},
|
||||
]
|
||||
drawing = build_landuse_drawing("landuse", "용지도", [], [], parcels, [], [])
|
||||
texts = [e for e in drawing["entities"] if e.get("layerId") == JIBUN_LAYER_ID]
|
||||
labels = [e["shapeData"]["label"] for e in texts]
|
||||
assert labels == ["95전", "전", "4,916㎡", "개인", "산1임", "임야", "12㎡", "산77"]
|
||||
groups = {round(e["shapeData"]["basePoint"]["x"], 6) for e in texts}
|
||||
assert len(groups) == 3
|
||||
# 쪼개진 필지는 큰 조각(200~230 m) 가운데에
|
||||
split_x = {
|
||||
e["shapeData"]["basePoint"]["x"] for e in texts if e["shapeData"]["label"] == "산1임"
|
||||
}
|
||||
assert split_x.pop() == pytest.approx(215.0 * MM)
|
||||
Reference in New Issue
Block a user