Skip to content

Commit e5ca295

Browse files
committed
wip
1 parent 9ebfd2c commit e5ca295

4 files changed

Lines changed: 120 additions & 11 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,4 @@ Thumbs.db
7777
*.ac$
7878
*.dwl
7979
*.dwl2
80+
*.pdf

app.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import math
2+
13
from flask import Flask, request, jsonify
24

35
from dxf import SurveyDXFManager
46
from models.plan import PlanProps
7+
from utils import polygon_orientation, line_normals, line_direction
58

69
app = Flask(__name__)
710
app.config["SECRET_KEY"] = "secret"
@@ -17,29 +20,76 @@ def generate_cadastral_plan():
1720

1821
plan = PlanProps(**data)
1922
extent = plan.get_extent()
20-
beacon_size = extent * 0.05
23+
beacon_size = extent * 0.02
2124

2225
drawer = SurveyDXFManager(plan_name=plan.name, scale=plan.scale)
2326
drawer.setup_beacon_style(type_=plan.beacon_type, size=beacon_size or 1.0)
2427
drawer.setup_font(plan.font)
2528

29+
label_height = extent * 0.015 if extent > 0 else 1.0
30+
2631
# Draw beacon and labels
2732
for coord in plan.coordinates:
2833
drawer.add_beacon(coord.easting, coord.northing, 0, beacon_size * 0.5, coord.id)
2934

3035
# create a dictionary of coordinates for easy lookup
3136
coord_dict = {coord.id: coord for coord in plan.coordinates}
37+
parcel_dict = {}
3238

3339
# Draw parcels
3440
for parcel in plan.parcels:
3541
parcel_points = []
3642
for point_id in parcel.ids:
3743
if point_id in coord_dict:
3844
coord = coord_dict[point_id]
39-
parcel_points.append((coord.easting, coord.northing, coord.elevation))
45+
parcel_points.append((coord.easting, coord.northing))
4046
if parcel_points:
4147
drawer.add_parcel(parcel.name, parcel_points)
4248

49+
# add bearing and distance text
50+
orientation = polygon_orientation(parcel_points)
51+
for leg in parcel.legs:
52+
# compute rotational angle for text
53+
angle_rad = math.atan2(leg.to.northing - leg.from_.northing, leg.to.easting - leg.from_.easting)
54+
angle_deg = math.degrees(angle_rad)
55+
56+
first_x = leg.from_.easting + (0.2 * (leg.to.easting - leg.from_.easting))
57+
first_y = leg.from_.northing + (0.2 * (leg.to.northing - leg.from_.northing))
58+
last_x = leg.from_.easting + (0.8 * (leg.to.easting - leg.from_.easting))
59+
last_y = leg.from_.northing + (0.8 * (leg.to.northing - leg.from_.northing))
60+
mid_x = (leg.from_.easting + leg.to.easting) / 2
61+
mid_y = (leg.from_.northing + leg.to.northing) / 2
62+
63+
# Offset text above/below the line
64+
normals = line_normals((leg.from_.easting, leg.from_.northing), (leg.to.easting, leg.to.northing), orientation)
65+
offset_distance = extent * 0.02
66+
offset_inside_x = (normals[0][0] / math.hypot(*normals[0])) * offset_distance
67+
offset_inside_y = (normals[0][1] / math.hypot(*normals[0])) * offset_distance
68+
offset_outside_x = (normals[1][0] / math.hypot(*normals[1])) * offset_distance
69+
offset_outside_y = (normals[1][1] / math.hypot(*normals[1])) * offset_distance
70+
first_x += offset_outside_x
71+
first_y += offset_outside_y
72+
last_x += offset_outside_x
73+
last_y += offset_outside_y
74+
mid_x += offset_inside_x
75+
mid_y += offset_inside_y
76+
77+
# add texts
78+
text_angle = angle_deg
79+
if text_angle > 90 or text_angle < -90:
80+
text_angle += 180
81+
82+
drawer.add_text(f"{leg.distance:.2f} m", mid_x, mid_y, angle=text_angle, height=label_height)
83+
ld = line_direction(angle_deg)
84+
print(leg.from_.id, leg.to.id, ld)
85+
if ld == "left → right":
86+
drawer.add_text(f"{leg.bearing.degrees}°", first_x, first_y, angle=text_angle, height=label_height)
87+
drawer.add_text(f"{leg.bearing.minutes}'", last_x, last_y, angle=text_angle, height=label_height)
88+
else:
89+
drawer.add_text(f"{leg.bearing.degrees}°", last_x, last_y, angle=text_angle, height=label_height)
90+
drawer.add_text(f"{leg.bearing.minutes}'", first_x, first_y, angle=text_angle, height=label_height)
91+
parcel_dict[parcel.name] = parcel_points
92+
4393
# Compute extent sizes
4494
min_x, min_y, max_x, max_y = plan.get_bounding_box()
4595
width = max_x - min_x
@@ -55,9 +105,14 @@ def generate_cadastral_plan():
55105
offset_y = max(height, width) * 0.78
56106
drawer.draw_frame(min_x - offset_x, min_y - offset_y, max_x + offset_x, max_y + offset_y)
57107

58-
# add bearing and distance text
108+
# add title block
109+
box_width = (max_x + margin_x) - (min_x - margin_x) * 0.6
110+
title_x = ((min_x - margin_x) + (max_x + margin_x)) / 2
111+
title_y = (max_y + margin_y) - (margin_y * 0.15)
112+
text_height = plan.font_size or ((max_y + margin_y) - (min_y - margin_y)) * 0.02
113+
drawer.add_title(plan.title.upper(), title_x, title_y, width=box_width, height=text_height)
59114

60-
drawer.dxf_to_pdf()
115+
drawer.save_dxf()
61116
return jsonify({"message": "Cadastral plan generated", "filename": plan.name}), 200
62117

63118
if __name__ == '__main__':

dxf.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import ezdxf
22
import re
3-
from ezdxf.enums import TextEntityAlignment
3+
from ezdxf.enums import TextEntityAlignment, MTextParagraphAlignment
44
import matplotlib
55
matplotlib.use("Agg") # no GUI
66
import matplotlib.pyplot as plt
@@ -125,6 +125,31 @@ def add_parcel(self, parcel_id: str, points: list):
125125
align=TextEntityAlignment.MIDDLE_CENTER
126126
)
127127

128+
def add_text(self, text: str, x: float, y: float, angle: float = 0.0, height: float = 1.0):
129+
"""Add arbitrary text at given coordinates with optional rotation"""
130+
self.msp.add_text(
131+
text,
132+
dxfattribs={
133+
'layer': 'LABELS',
134+
'height': height * self.scale,
135+
'style': 'SURVEY_TEXT',
136+
'rotation': angle
137+
}
138+
).set_placement(
139+
(x * self.scale, y * self.scale),
140+
align=TextEntityAlignment.MIDDLE_CENTER
141+
)
142+
143+
def add_title(self, text: str, x: float, y: float, width: float, height: float = 1.0):
144+
print(width)
145+
"""Add title text at given coordinates with optional rotation"""
146+
mtext = self.msp.add_mtext(text, dxfattribs={'layer': 'TITLE_BLOCK', 'style': 'SURVEY_TEXT'})
147+
mtext.set_location((x * self.scale, y * self.scale))
148+
mtext.dxf.char_height = height * self.scale
149+
mtext.dxf.width = 60
150+
mtext.dxf.attachment_point = 2 # top center
151+
# mtext.dxf.paragraphs = MTextParagraphAlignment.JUSTIFIED
152+
128153
def draw_frame(self, min_x, min_y, max_x, max_y):
129154
"""Draw a rectangle given min and max coordinates"""
130155
self.msp.add_lwpolyline([
@@ -149,14 +174,15 @@ def save_dxf(self):
149174

150175

151176
def dxf_to_pdf(self, margin_ratio: float = 0.05):
177+
self.save_dxf()
152178

153-
154-
# doc = ezdxf.readfile(dxf_path)
155-
# msp = doc.modelspace()
179+
dxf_path = f"{self.get_filename()}.dxf"
180+
doc = ezdxf.readfile(dxf_path)
181+
msp = doc.modelspace()
156182

157183
# compute bounding box
158184
xs, ys = [], []
159-
for e in self.msp:
185+
for e in msp:
160186
if e.dxftype() == "LINE":
161187
xs += [e.dxf.start[0], e.dxf.end[0]]
162188
ys += [e.dxf.start[1], e.dxf.end[1]]
@@ -178,9 +204,11 @@ def dxf_to_pdf(self, margin_ratio: float = 0.05):
178204
ax.set_ylim(min_y, max_y)
179205
ax.axis("off")
180206

181-
ctx = RenderContext(self.doc)
207+
ctx = RenderContext(doc)
182208
out = MatplotlibBackend(ax)
183-
Frontend(ctx, out).draw_layout(self.msp, finalize=True)
209+
out.set_background("black")
210+
# out.set_color_mapper(lambda entity: 'black' if entity.dxf.color == 7 else None)
211+
Frontend(ctx, out).draw_layout(msp, finalize=True)
184212

185213
pdf_path = f"{self.get_filename()}.pdf"
186214
fig.savefig(pdf_path, bbox_inches="tight", pad_inches=0)

utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
def polygon_orientation(coords):
2+
# coords = [(x1,y1), (x2,y2), ...]
3+
area = 0
4+
for i in range(len(coords)):
5+
x1, y1 = coords[i]
6+
x2, y2 = coords[(i + 1) % len(coords)]
7+
area += (x2 - x1) * (y2 + y1)
8+
return "CW" if area > 0 else "CCW"
9+
10+
def line_normals(p1, p2, orientation="CCW"):
11+
dx, dy = p2[0]-p1[0], p2[1]-p1[1]
12+
if orientation == "CCW": # inside = left normal
13+
inside = (-dy, dx)
14+
outside = (dy, -dx)
15+
else: # CW polygon
16+
inside = (dy, -dx)
17+
outside = (-dy, dx)
18+
return inside, outside
19+
20+
def line_direction(angle) -> str:
21+
# Normalize angle between -180 and 180
22+
if -90 <= angle <= 90:
23+
return "left → right"
24+
else:
25+
return "right → left"

0 commit comments

Comments
 (0)