import math
import json
from shapely.geometry import Polygon, LineString
from shapely.affinity import rotate
from pyproj import Transformer
[docs]
def latlon_to_utm(polygon_latlon, epsg="32632"):
lats, lons = zip(*polygon_latlon)
transformer = Transformer.from_crs(
"epsg:4326", f"epsg:{epsg}", always_xy=True)
utm_coords = [transformer.transform(lon, lat)
for lat, lon in polygon_latlon]
return Polygon(utm_coords), transformer
[docs]
def utm_to_latlon(coords, transformer):
result = []
for x, y in coords:
if not math.isfinite(x) or not math.isfinite(y):
print(f"🚨 Ungültige UTM-Koordinate: {(x, y)}")
continue # oder raise
lat, lon = transformer.transform(x, y, direction='INVERSE')
result.append((lat, lon))
return result
[docs]
def generate_scan_lines(polygon, spacing, angle_deg=0):
# Rotate the polygon to simplify horizontal line generation
rotated = rotate(polygon, -angle_deg, origin='centroid', use_radians=False)
minx, miny, maxx, maxy = rotated.bounds
y = miny + spacing / 2
lines = []
reverse = False # To alternate direction for U-shaped pattern
while y <= maxy:
scan_line = LineString([(minx, y), (maxx, y)])
intersect = scan_line.intersection(rotated)
if not intersect.is_empty:
segments = []
if intersect.geom_type == "MultiLineString":
for seg in intersect.geoms:
coords = list(seg.coords)
segments.append(coords)
elif intersect.geom_type == "LineString":
coords = list(intersect.coords)
segments = [coords]
for coords in segments:
if reverse:
coords = list(reversed(coords))
lines.append(coords)
reverse = not reverse # Alternate direction
y += spacing
# Rotate lines back to original orientation
return [rotate(LineString(coords), angle_deg,
origin=polygon.centroid, use_radians=False)
for coords in lines]
[docs]
def calculate_heading(lat1, lon1, lat2, lon2, transformer):
x1, y1 = transformer.transform(lon1, lat1)
x2, y2 = transformer.transform(lon2, lat2)
dx = x2 - x1
dy = y2 - y1
angle_rad = math.atan2(dx, dy) # East is X, North is Y
heading_deg = math.degrees(angle_rad)
heading_deg = (heading_deg + 360) % 360 # Normalize to [0, 360)
return heading_deg
[docs]
def plan_scan(polygon_latlon, start_latlon, end_latlon, altitude,
fov_deg, overlap_ratio, epsg="32632"):
footprint = fov_to_ground_footprint(altitude, fov_deg)
spacing = footprint * (1 - overlap_ratio)
polygon_utm, transformer = latlon_to_utm(polygon_latlon, epsg)
# Rotate to minimize scan distance
min_len = float('inf')
best_angle = 0
for angle in range(0, 180, 5):
lines = generate_scan_lines(polygon_utm, spacing, angle)
total_len = sum(i.length for i in lines)
if total_len < min_len:
min_len = total_len
best_angle = angle
scan_lines = generate_scan_lines(polygon_utm, spacing, best_angle)
scan_latlon = [utm_to_latlon(list(line.coords), transformer)
for line in scan_lines]
# Build mission route
route = [start_latlon] + \
[pt[::-1] for line in scan_latlon for pt in line] + [end_latlon]
route_with_heading = []
for i in range(len(route) - 1):
heading = calculate_heading(*route[i], *route[i + 1], transformer)
route_with_heading.append((*route[i+1], heading))
return {
"scan_polygon": polygon_latlon,
"scan_lines": scan_latlon,
"route": route_with_heading
}
[docs]
def export_geojson(scan_data, filename="scan_mission.geojson"):
features = []
# Scan area
poly = scan_data["scan_polygon"] + [scan_data["scan_polygon"][0]]
features.append({
"type": "Feature",
"properties": {"type": "scan_area"},
"geometry": {
"type": "Polygon",
"coordinates": [[(lon, lat) for lat, lon in poly]]
}
})
# Flight path
features.append({
"type": "Feature",
"properties": {"type": "flight_path"},
"geometry": {
"type": "LineString",
"coordinates": [(lon, lat) for lat, lon, _ in scan_data["route"]]
}
})
# Waypoints
for i, (lat, lon, heading) in enumerate(scan_data["route"]):
features.append({
"type": "Feature",
"properties": {
"type": "waypoint",
"index": i,
"heading": heading
},
"geometry": {
"type": "Point",
"coordinates": [lon, lat]
}
})
geojson = {
"type": "FeatureCollection",
"features": features
}
with open(filename, "w") as f:
json.dump(geojson, f, indent=2)
print(f"✔ GeoJSON saved as: {filename}")