height correction, new defalt

This commit is contained in:
Olaf committed 2026-10-21 09:24:12 +02:00
1 parent 0891cf12a5
commit b692f94122
12 files changed
+792 -205

No files matched your search

+60 -24
View File
@@ -12,10 +12,11 @@ from shapely.affinity import affine_transform
from shapely.geometry import LineString, box
from shapely.geometry.base import BaseGeometry
from flight_map.ahn import terrain_data
from flight_map.curves import Cubic
from flight_map.geo import Projection
from flight_map.lettering import lettering
from flight_map.maps import map_data, map_lines
from flight_map.maps import WaterMaskError, map_data, map_lines, water_areas
from flight_map.storage import database, get_session, load_observations
from flight_map.terrain import Elevation
from flight_map.tracks import make_tracks
@@ -119,18 +120,24 @@ def render(
local_map: Path | None = None,
terrain: Path | None = None,
terrain_reference: str = "unknown",
geoid: Path | None = None,
confirm_geometric_ellipsoid: bool = False,
force: bool = False,
refresh_map: bool = False,
no_terrain: bool = False,
refresh_terrain: bool = False,
) -> int:
options = options or PlotOptions()
options.validate()
if no_terrain and (terrain is not None or refresh_terrain):
raise ValueError("--no-terrain cannot be combined with --terrain or --refresh-terrain.")
if terrain is not None and refresh_terrain:
raise ValueError("--refresh-terrain applies to automatic AHN data, not --terrain.")
if terrain is None and terrain_reference != "unknown":
raise ValueError("--terrain-reference requires a local --terrain raster.")
if output.exists() and not force:
raise ValueError(f"Output already exists: {output}. Use --force to replace it.")
if any(
output.resolve() == source.resolve()
for source in (db, local_map, terrain, geoid)
for source in (db, local_map, terrain)
if source is not None
):
raise ValueError("SVG output must not overwrite the recording database or input data.")
@@ -150,26 +157,45 @@ def render(
if not observations:
raise ValueError("No recorded observations in the selected interval.")
projection = Projection(session.bounds)
with Elevation(terrain, terrain_reference, geoid, confirm_geometric_ellipsoid) as elevation:
ground = elevation.mean_ground(session.bounds)
tracks = make_tracks(observations, projection, elevation, ground, options.max_gap)
data = map_data(session.bounds, cache, local_map, refresh=refresh_map)
try:
water = water_areas(data)
except WaterMaskError as exc:
LOGGER.warning(
"Land elevation unavailable: %s. Reported flight altitudes are unaffected.", exc
)
water = None
terrain_source = "disabled" if no_terrain else "local terrain"
if terrain is None and not no_terrain:
terrain = terrain_data(session.bounds, cache / "terrain", refresh=refresh_terrain)
terrain_reference = "nap" if terrain else "unknown"
terrain_source = "PDOK AHN DTM dtm_05m; NAP; CC0"
if terrain is not None and terrain.resolve() == output.resolve():
raise ValueError("SVG output must not overwrite cached terrain data.")
with Elevation(terrain, terrain_reference) as elevation:
ground = elevation.mean_ground(session.bounds, water=water) if water is not None else None
land_coverage = elevation.land_coverage
land_samples = elevation.land_samples
valid_land_samples = elevation.valid_land_samples
water_samples = elevation.water_samples
tracks = make_tracks(observations, projection, options.max_gap)
if not tracks:
raise ValueError("No usable airborne tracks cross the map in the selected interval.")
if ground is None:
LOGGER.warning(
"Average ground elevation unavailable: supply a complete terrain raster "
"and its vertical reference."
"Average land elevation unavailable: no usable height map or land coverage. "
"Reported flight altitudes are unaffected."
)
missing = sum(track.average_height is None for track in tracks)
missing = sum(track.average_altitude is None for track in tracks)
if missing:
LOGGER.warning(
"%d track height(s) unavailable: compatible geometric altitude, terrain "
"and vertical-reference conversion are required.",
"%d track altitude(s) unavailable: reported barometric altitude is missing "
"on a contributing segment; no geometric-altitude fallback is used.",
missing,
)
data = map_data(session.bounds, cache, local_map, refresh=refresh_map)
row_height = options.font_height * 1.8
legend_height = (len(tracks) + 4) * row_height
terrain_attribution = terrain_source.startswith("PDOK")
legend_height = (len(tracks) + 5 + int(terrain_attribution)) * row_height
available_width = options.width - 2 * (options.margin + options.extension)
available_height = options.height - 2 * (options.margin + options.extension) - legend_height
if available_height < 25:
@@ -205,11 +231,15 @@ def render(
ET.SubElement(root, tag("desc")).text = (
f"Session {session.id}; bounds W,S,E,N={session.bounds.values}; "
f"{utc(start)} to {utc(end)} UTC. Local equirectangular projection. "
"Height is time-weighted aircraft altitude within the map minus the "
"area-sampled mean ground elevation, not terrain clearance. "
"Flight altitude is the time-weighted mean reported barometric pressure altitude "
"within the map, in metres, without terrain subtraction or vertical corrections. "
"It is not height above ground or terrain clearance. "
"Mean land elevation is a separate summary excluding mapped OSM water. "
f"Terrain source: {terrain_source}; "
f"Terrain reference: {terrain_reference}; "
f"valid land samples: {valid_land_samples}/{land_samples}; "
f"land coverage: {100 * land_coverage:.1f}%; excluded water samples: {water_samples}; "
f"mean ground elevation in metres: {ground}; "
f"geometric ellipsoid interpretation confirmed: {confirm_geometric_ellipsoid}. "
"Map data: OpenStreetMap contributors, ODbL, https://www.openstreetmap.org/copyright. "
"Flight data: The OpenSky Network, https://opensky-network.org. "
"Schaefer et al., Bringing Up OpenSky: A Large-scale ADS-B Sensor Network "
@@ -245,9 +275,10 @@ def render(
"data-callsign": track.observations[0].callsign or "unavailable",
"data-start-utc": utc(track.start),
"data-end-utc": utc(track.end),
"data-average-height-metres": (
f"{track.average_height:.3f}"
if track.average_height is not None
"data-altitude-source": "barometric",
"data-average-barometric-altitude-metres": (
f"{track.average_altitude:.3f}"
if track.average_altitude is not None
else "unavailable"
),
},
@@ -282,15 +313,18 @@ def render(
legend_y = top + map_height + options.extension + row_height
ground_label = "N/A" if ground is None else f"{ground:.1f} M"
legend = [
"FLIGHT TIMES UTC - HEIGHT ABOVE MAP AVERAGE GROUND, NOT TERRAIN CLEARANCE",
f"MAP AVERAGE GROUND ELEVATION: {ground_label} - N/A MEANS UNAVAILABLE",
"FLIGHT TIMES UTC - REPORTED BAROMETRIC ALTITUDE, NOT HEIGHT ABOVE GROUND",
f"MAP AVERAGE LAND ELEVATION: {ground_label} - N/A MEANS UNAVAILABLE",
f"LAND ELEVATION COVERAGE: {100 * land_coverage:.1f} PERCENT - MAPPED WATER EXCLUDED",
]
for index, track in enumerate(tracks, 1):
first = track.observations[0]
height_label = "N/A" if track.average_height is None else f"{track.average_height:.0f} M"
altitude_label = (
"N/A" if track.average_altitude is None else f"{track.average_altitude:.0f} M"
)
legend.append(
f"{index}: {utc(track.start)} TO {utc(track.end)} "
f"{first.callsign or 'CALLSIGN N/A'} ({first.icao24.upper()}) HEIGHT {height_label}"
f"{first.callsign or 'CALLSIGN N/A'} ({first.icao24.upper()}) BARO ALT {altitude_label}"
)
legend.extend(
[
@@ -298,6 +332,8 @@ def render(
"FLIGHTS: THE OPENSKY NETWORK - OPENSKY-NETWORK.ORG",
]
)
if terrain_attribution:
legend.append("TERRAIN: AHN DTM VIA PDOK (CC0) - ELEVATIONS RELATIVE TO NAP")
for index, text in enumerate(legend):
if len(text) * options.font_height > options.width - 2 * options.margin:
raise ValueError("Legend is too wide; reduce --font-height or increase page width.")