height correction, new defalt
This commit is contained in:
1 parent
0891cf12a5
commit
b692f94122
12 files changed
+792
-205
No files matched your search
+60
-24
@@ -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.")
|
||||
|
||||
Reference in new issue
Block a user