diff --git a/.devcontainer/requirements.txt b/.devcontainer/requirements.txt index b27db6e..b46eef5 100644 --- a/.devcontainer/requirements.txt +++ b/.devcontainer/requirements.txt @@ -20,4 +20,4 @@ textalloc==1.1.6 shapely==2.0.6 types-shapely==2.0.0.20241221 orjson==3.10.7 -requests==2.32.3 +requests==2.32.4 diff --git a/pretty_gpx/common/drawing/components/elevation_profile.py b/pretty_gpx/common/drawing/components/elevation_profile.py index d9f0a75..7167098 100644 --- a/pretty_gpx/common/drawing/components/elevation_profile.py +++ b/pretty_gpx/common/drawing/components/elevation_profile.py @@ -18,12 +18,51 @@ from pretty_gpx.common.gpx.gpx_bounds import GpxBounds from pretty_gpx.common.gpx.gpx_distance import get_pairwise_distance_m from pretty_gpx.common.gpx.gpx_track import GpxTrack +from pretty_gpx.common.gpx.multi_gpx_track import MultiGpxTrack from pretty_gpx.common.layout.paper_size import PaperSize from pretty_gpx.common.utils.asserts import assert_in from pretty_gpx.common.utils.asserts import assert_same_len from pretty_gpx.common.utils.utils import get +def handle_flat_elevation_profile(track: GpxTrack | MultiGpxTrack, + bot_ratio: float, + ele_ratio: float) -> tuple[float, float]: + """For nearly flat tracks with gradients below 1%, reduce vertical scaling accordingly. + + 1 ┌───────────────────────x────────────┐ ▲ + │ xxxxxxx │ │ ele_ratio + │ xxxxxxxxxxx xxxx xxxxxxxxxx│ │ + xxx xxxxxx xx ▼ + x x + x x + x x + x x + x x + 0 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 1 + """ + if isinstance(track, GpxTrack): + uphill_m = track.uphill_m + dist_km = track.dist_km + else: + uphill_m = sum(t.uphill_m for t in track.tracks) + dist_km = sum(t.dist_km for t in track.tracks) + + avg_gradient = uphill_m*1e-3/dist_km + ref_gradient = 0.01 # 1% + + if 0 <= avg_gradient < ref_gradient: + downscale = avg_gradient / ref_gradient + + new_total_height = 1.0 - ele_ratio*(1.-downscale) + new_ele_height = ele_ratio * downscale + + bot_ratio *= new_total_height + ele_ratio = new_ele_height / new_total_height + + return bot_ratio, ele_ratio + + def downsample(x: np.ndarray, y: np.ndarray, n: int) -> tuple[np.ndarray, np.ndarray]: """Downsample the signal Y evaluated at X to N points, applying a simple moving average smoothing beforehand.""" assert_same_len([x, y], msg="Downsampling arrays should be the same length") diff --git a/pretty_gpx/rendering_modes/city/drawing/city_drawer.py b/pretty_gpx/rendering_modes/city/drawing/city_drawer.py index f752775..7acd606 100644 --- a/pretty_gpx/rendering_modes/city/drawing/city_drawer.py +++ b/pretty_gpx/rendering_modes/city/drawing/city_drawer.py @@ -9,6 +9,7 @@ from pretty_gpx.common.drawing.components.annotated_scatter import AnnotatedScatterAll from pretty_gpx.common.drawing.components.centered_title import CenteredTitle from pretty_gpx.common.drawing.components.elevation_profile import ElevationProfile +from pretty_gpx.common.drawing.components.elevation_profile import handle_flat_elevation_profile from pretty_gpx.common.drawing.components.track_data import TrackData from pretty_gpx.common.drawing.utils.drawer import DrawerSingleTrack from pretty_gpx.common.drawing.utils.drawing_figure import DrawingFigure @@ -53,9 +54,12 @@ class CityDrawer(DrawerSingleTrack): def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None: """Load a single GPX file to create a City Poster.""" gpx_track = GpxTrack.load(gpx_path) + ele_ratio = 0.45 + bot_ratio, ele_ratio = handle_flat_elevation_profile(gpx_track, self.bot_ratio, ele_ratio) + layouts = VerticalLayoutUnion.from_track(gpx_track, top_ratio=self.top_ratio, - bot_ratio=self.bot_ratio, + bot_ratio=bot_ratio, margin_ratio=self.margin_ratio) total_query = OverpassQuery() @@ -72,7 +76,7 @@ def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None: layout = layouts.layouts[paper] background.change_papersize(paper, layout.background_bounds) - ele_pofile = ElevationProfile.from_track(layout.bot_bounds, gpx_track, scatter_points, ele_ratio=0.45) + ele_profile = ElevationProfile.from_track(layout.bot_bounds, gpx_track, scatter_points, ele_ratio=ele_ratio) title = CenteredTitle(bounds=layout.top_bounds) scatter_all = AnnotatedScatterAll.from_scatter(paper, layout.background_bounds, layout.mid_bounds, scatter_points, self.params) @@ -80,7 +84,7 @@ def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None: self.data = CityLayout(layouts=layouts, background=background, - bot=ele_pofile, + bot=ele_profile, top=title, mid_scatter=scatter_all, mid_track=track_data, diff --git a/pretty_gpx/rendering_modes/mountain/drawing/mountain_drawer.py b/pretty_gpx/rendering_modes/mountain/drawing/mountain_drawer.py index f6f5b5c..5f1df09 100644 --- a/pretty_gpx/rendering_modes/mountain/drawing/mountain_drawer.py +++ b/pretty_gpx/rendering_modes/mountain/drawing/mountain_drawer.py @@ -9,6 +9,7 @@ from pretty_gpx.common.drawing.components.annotated_scatter import AnnotatedScatterAll from pretty_gpx.common.drawing.components.centered_title import CenteredTitle from pretty_gpx.common.drawing.components.elevation_profile import ElevationProfile +from pretty_gpx.common.drawing.components.elevation_profile import handle_flat_elevation_profile from pretty_gpx.common.drawing.components.track_data import TrackData from pretty_gpx.common.drawing.utils.drawer import DrawerSingleTrack from pretty_gpx.common.drawing.utils.drawing_figure import DrawingFigure @@ -51,9 +52,12 @@ class MountainDrawer(DrawerSingleTrack): def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None: """Load a single GPX file to create a Mountain Poster.""" gpx_track = GpxTrack.load(gpx_path) + ele_ratio = 0.45 + bot_ratio, ele_ratio = handle_flat_elevation_profile(gpx_track, self.bot_ratio, ele_ratio) + layouts = VerticalLayoutUnion.from_track(gpx_track, top_ratio=self.top_ratio, - bot_ratio=self.bot_ratio, + bot_ratio=bot_ratio, margin_ratio=self.margin_ratio) total_query = OverpassQuery() @@ -66,7 +70,7 @@ def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None: layout = layouts.layouts[paper] background.change_papersize(paper, layout.background_bounds) - ele_pofile = ElevationProfile.from_track(layout.bot_bounds, gpx_track, scatter_points, ele_ratio=0.45) + ele_profile = ElevationProfile.from_track(layout.bot_bounds, gpx_track, scatter_points, ele_ratio=ele_ratio) title = CenteredTitle(bounds=layout.top_bounds) scatter_all = AnnotatedScatterAll.from_scatter(paper, layout.background_bounds, layout.mid_bounds, scatter_points, self.params) @@ -74,7 +78,7 @@ def change_gpx(self, gpx_path: str | bytes, paper: PaperSize) -> None: self.data = MountainLayout(layouts=layouts, background=background, - bot=ele_pofile, + bot=ele_profile, top=title, mid_scatter=scatter_all, mid_track=track_data, diff --git a/pretty_gpx/rendering_modes/multi_mountain/drawing/multi_mountain_drawer.py b/pretty_gpx/rendering_modes/multi_mountain/drawing/multi_mountain_drawer.py index 4b971b3..bf6c794 100644 --- a/pretty_gpx/rendering_modes/multi_mountain/drawing/multi_mountain_drawer.py +++ b/pretty_gpx/rendering_modes/multi_mountain/drawing/multi_mountain_drawer.py @@ -9,6 +9,7 @@ from pretty_gpx.common.drawing.components.annotated_scatter import AnnotatedScatterAll from pretty_gpx.common.drawing.components.centered_title import CenteredTitle from pretty_gpx.common.drawing.components.elevation_profile import ElevationProfile +from pretty_gpx.common.drawing.components.elevation_profile import handle_flat_elevation_profile from pretty_gpx.common.drawing.components.track_data import TrackData from pretty_gpx.common.drawing.utils.drawer import DrawerMultiTrack from pretty_gpx.common.drawing.utils.drawing_figure import DrawingFigure @@ -51,9 +52,12 @@ class MultiMountainDrawer(DrawerMultiTrack): def change_gpx(self, gpx_paths: list[str] | list[bytes], paper: PaperSize) -> None: """Load several GPX file to create a Multi Mountain Poster.""" gpx_track = MultiGpxTrack.load(gpx_paths) + ele_ratio = 0.45 + bot_ratio, ele_ratio = handle_flat_elevation_profile(gpx_track, self.bot_ratio, ele_ratio) + layouts = VerticalLayoutUnion.from_track(gpx_track, top_ratio=self.top_ratio, - bot_ratio=self.bot_ratio, + bot_ratio=bot_ratio, margin_ratio=self.margin_ratio) total_query = OverpassQuery() @@ -66,7 +70,8 @@ def change_gpx(self, gpx_paths: list[str] | list[bytes], paper: PaperSize) -> No layout = layouts.layouts[paper] background.change_papersize(paper, layout.background_bounds) - ele_pofile = ElevationProfile.from_track(layout.bot_bounds, gpx_track.merge(), scatter_points, ele_ratio=0.45) + ele_profile = ElevationProfile.from_track(layout.bot_bounds, gpx_track.merge(), scatter_points, + ele_ratio=ele_ratio) title = CenteredTitle(bounds=layout.top_bounds) scatter_all = AnnotatedScatterAll.from_scatter(paper, layout.background_bounds, layout.mid_bounds, scatter_points, self.params) @@ -74,7 +79,7 @@ def change_gpx(self, gpx_paths: list[str] | list[bytes], paper: PaperSize) -> No self.data = MultiMountainLayout(layouts=layouts, background=background, - bot=ele_pofile, + bot=ele_profile, top=title, mid_scatter=scatter_all, mid_track=track_data,