Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the possibility to have total activity time on the poster #7

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions pretty_gpx/drawing/drawing_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,16 +76,18 @@ class DrawingFigure(BaseDrawingFigure):
track_data: list[BaseDrawingData]
peak_data: list[BaseDrawingData]

title: TextData
stats: TextData
title: TextData
stats: TextData
duration: TextData

def draw(self,
fig: Figure,
ax: Axes,
img: np.ndarray,
theme_colors: ThemeColors,
title_txt: str,
stats_txt: str) -> None:
stats_txt: str,
duration_txt: str) -> None:
"""Plot the background image and the annotations on top of it."""
self.imshow(fig, ax, img)
self.adjust_display_width(fig, self.w_display_pix)
Expand All @@ -101,3 +103,7 @@ def draw(self,

self.title.plot(ax, theme_colors.peak_color, self.ref_img_shape, img.shape)
self.stats.plot(ax, theme_colors.background_color, self.ref_img_shape, img.shape)

if duration_txt is not None and duration_txt != "":
self.duration.s = duration_txt
self.duration.plot(ax, theme_colors.background_color, self.ref_img_shape, img.shape)
82 changes: 62 additions & 20 deletions pretty_gpx/drawing/poster_image_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ def from_gpx_data(gpx_data: AugmentedGpxData,
low_res = high_res.change_dpi(WORKING_DPI)
return PosterImageCaches(low_res=low_res, high_res=high_res, gpx_data=gpx_data)

@staticmethod
def update_duration_str(gpx_data: AugmentedGpxData,
paper_size: PaperSize,
override_duration_str:str) -> 'PosterImageCaches':
"""Create a PosterImageCaches from a GPX file."""
high_res = PosterImageCache.from_gpx_data(gpx_data,
dpi=HIGH_RES_DPI,
paper=paper_size,
override_duration=override_duration_str)
low_res = high_res.change_dpi(WORKING_DPI)
return PosterImageCaches(low_res=low_res, high_res=high_res, gpx_data=gpx_data)


@dataclass
class PosterDrawingData:
Expand All @@ -68,6 +80,7 @@ class PosterDrawingData:
theme_colors: ThemeColors
title_txt: str
stats_text: str
duration_txt: str


@dataclass
Expand All @@ -79,6 +92,7 @@ class PosterImageCache:

stats_dist_km: float
stats_uphill_m: float
stat_duration_str: str

plotter: DrawingFigure

Expand All @@ -88,7 +102,8 @@ class PosterImageCache:
def from_gpx_data(gpx_data: AugmentedGpxData,
paper: PaperSize,
layout: VerticalLayout = VerticalLayout(),
dpi: float = HIGH_RES_DPI) -> 'PosterImageCache':
dpi: float = HIGH_RES_DPI,
override_duration: str=None) -> 'PosterImageCache':
"""Create a PosterImageCache from a GPX file."""
# Download the elevation map at the correct layout
bounds, latlon_aspect_ratio = get_bounds(gpx_data.track, layout, paper)
Expand Down Expand Up @@ -169,18 +184,28 @@ def from_gpx_data(gpx_data: AugmentedGpxData,
fontproperties=drawing_style_params.classic_font)

# Draw the elevation profile
if override_duration is not None and override_duration != "":
duration_to_draw = override_duration
else:
duration_to_draw = gpx_data.total_duration

if duration_to_draw is not None and duration_to_draw != "":
draw_duration = True
else:
draw_duration = False
draw_start = gpx_data.start_name is not None
draw_end = draw_start if gpx_data.is_closed else gpx_data.end_name is not None
ele_scatter, ele_fill_poly, stats = get_elevation_drawings(layout=layout,
h_pix=h, w_pix=w,
list_ele=gpx_data.track.list_ele,
list_cum_d=gpx_data.track.list_cumul_d,
passes_ids=gpx_data.passes_ids,
huts_ids=gpx_data.hut_ids,
draw_start=draw_start,
draw_end=draw_end,
drawing_style_params=drawing_style_params,
drawing_size_params=drawing_size_params)
ele_scatter, ele_fill_poly, stats, duration_stat = get_elevation_drawings(layout=layout,
h_pix=h, w_pix=w,
list_ele=gpx_data.track.list_ele,
list_cum_d=gpx_data.track.list_cumul_d,
passes_ids=gpx_data.passes_ids,
huts_ids=gpx_data.hut_ids,
draw_start=draw_start,
draw_end=draw_end,
draw_duration=draw_duration,
drawing_style_params=drawing_style_params,
drawing_size_params=drawing_size_params)

# Prepare the plot data
augmented_hut_ids = [0] + gpx_data.hut_ids + [None]
Expand Down Expand Up @@ -241,13 +266,15 @@ def from_gpx_data(gpx_data: AugmentedGpxData,
track_data=track_data,
peak_data=peak_data,
title=title,
stats=stats)
stats=stats,
duration=duration_stat)

print("Successful GPX Processing")
return PosterImageCache(elevation_map=elevation,
elevation_shading=CachedHillShading(elevation),
stats_dist_km=gpx_data.dist_km,
stats_uphill_m=gpx_data.uphill_m,
stat_duration_str=duration_to_draw,
plotter=plotter,
dpi=dpi)

Expand All @@ -258,6 +285,7 @@ def change_dpi(self, dpi: float) -> 'PosterImageCache':
elevation_shading=CachedHillShading(new_elevation_map),
stats_dist_km=self.stats_dist_km,
stats_uphill_m=self.stats_uphill_m,
stat_duration_str=self.stat_duration_str,
plotter=self.plotter,
dpi=dpi)

Expand All @@ -266,7 +294,8 @@ def update_drawing_data(self,
theme_colors: ThemeColors,
title_txt: str,
uphill_m: str,
dist_km: str) -> PosterDrawingData:
dist_km: str,
duration: str) -> PosterDrawingData:
"""Update the drawing data (can run in a separate thread)."""
grey_hillshade = self.elevation_shading.render_grey(azimuth)[..., None]
background_color_rgb = hex_to_rgb(theme_colors.background_color)
Expand All @@ -280,15 +309,16 @@ def update_drawing_data(self,
uphill_m_int = int(uphill_m if uphill_m != '' else self.stats_uphill_m)
stats_text = f"{dist_km_int} km - {uphill_m_int} m D+"

return PosterDrawingData(img, theme_colors, title_txt=title_txt, stats_text=stats_text)
return PosterDrawingData(img, theme_colors, title_txt=title_txt, stats_text=stats_text, duration_txt=duration)

def draw(self, fig: Figure, ax: Axes, poster_drawing_data: PosterDrawingData) -> None:
"""Draw the updated drawing data (Must run in the main thread because of matplotlib backend)."""
self.plotter.draw(fig, ax,
poster_drawing_data.img,
poster_drawing_data.theme_colors,
poster_drawing_data.title_txt,
poster_drawing_data.stats_text)
poster_drawing_data.stats_text,
poster_drawing_data.duration_txt)
print(f"Drawing updated (Elevation Map {poster_drawing_data.img.shape[1]}x{poster_drawing_data.img.shape[0]})")


Expand All @@ -300,12 +330,15 @@ def get_elevation_drawings(layout: VerticalLayout,
huts_ids: list[int],
draw_start: bool,
draw_end: bool,
draw_duration: bool,
drawing_style_params: DrawingStyleParams,
drawing_size_params: DrawingSizeParams) -> tuple[list[ScatterData], PolyFillData, TextData]:
"""Create the plot elements for the elevation profile."""
# Elevation Profile
h_up_pix = h_pix * (layout.title_relative_h + layout.map_relative_h)
h_bot_pix = h_pix * (layout.title_relative_h + layout.map_relative_h + layout.elevation_relative_h)
h_stat_text = h_pix *(layout.title_relative_h + layout.map_relative_h + layout.elevation_relative_h + 1.0)/2.0
h_duration_text = h_pix * 0.99

elevation_poly_x = np.array(list_cum_d)/list_cum_d[-1]*w_pix

Expand Down Expand Up @@ -339,11 +372,20 @@ def get_elevation_drawings(layout: VerticalLayout,
elevation_poly_y = np.hstack((h_pix, h_bot_pix, elevation_poly_y, h_bot_pix, h_pix)).tolist()
elevation_data = PolyFillData(x=elevation_poly_x, y=elevation_poly_y)

stats = TextData(x=0.5 * w_pix, y=0.5 * (h_bot_pix+h_pix),
s="", fontsize=drawing_size_params.stats_fontsize,
fontproperties=drawing_style_params.pretty_font, ha="center")

return scatter_data, elevation_data, stats
if draw_duration:
duration = TextData(x=0.01 * w_pix, y=h_duration_text,
s="", fontsize=drawing_size_params.stats_fontsize*0.5,
fontproperties=drawing_style_params.pretty_font, ha="left")
stats = TextData(x=0.5 * w_pix, y=h_stat_text,
s="", fontsize=drawing_size_params.stats_fontsize*0.8,
fontproperties=drawing_style_params.pretty_font, ha="center")
else:
duration = None
stats = TextData(x=0.5 * w_pix, y=h_stat_text,
s="", fontsize=drawing_size_params.stats_fontsize*0.8,
fontproperties=drawing_style_params.pretty_font, ha="center")

return scatter_data, elevation_data, stats, duration


def rescale_elevation_to_dpi(elevation_map: np.ndarray, paper: PaperSize, target_dpi: float) -> np.ndarray:
Expand Down
21 changes: 17 additions & 4 deletions pretty_gpx/gpx/augmented_gpx_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pretty_gpx.gpx.gpx_bounds import GpxBounds
from pretty_gpx.gpx.gpx_track import GpxTrack
from pretty_gpx.gpx.gpx_track import local_m_to_deg
from pretty_gpx.utils.utils import format_timedelta

DEBUG_OVERPASS_QUERY = False

Expand Down Expand Up @@ -58,6 +59,8 @@ def uphill_m(self) -> float:
"""Total climb in m."""
return self.track.list_cumul_ele[-1]

total_duration: str | None

@staticmethod
def from_path(list_gpx_path: str | bytes | list[str] | list[bytes],
strict_ths_m: float = 50,
Expand All @@ -68,7 +71,15 @@ def from_path(list_gpx_path: str | bytes | list[str] | list[bytes],
if isinstance(list_gpx_path[0], str):
list_gpx_path = natsorted(list_gpx_path)

gpx_track, huts_ids, huts_names = find_huts_between_daily_tracks(list_gpx_path)
gpx_track, duration_l, huts_ids, huts_names = find_huts_between_daily_tracks(list_gpx_path)

if None in duration_l:
print("WARNING: cannot get the duration, some GPX files have not the time saved")
total_duration_str = None
else:
total_duration = sum(duration_l)
total_duration_str = format_timedelta(total_duration)
print("TOTAL DURATION = ",total_duration,total_duration_str)

is_closed = gpx_track.is_closed(loose_ths_m)
passes_ids, mountain_passes = get_close_mountain_passes(gpx_track, strict_ths_m)
Expand Down Expand Up @@ -98,7 +109,8 @@ def from_path(list_gpx_path: str | bytes | list[str] | list[bytes],
mountain_passes=mountain_passes,
passes_ids=passes_ids,
huts=huts_names,
hut_ids=huts_ids)
hut_ids=huts_ids,
total_duration=total_duration_str)


def overpass_query(query_elements: list[str], gpx_track: GpxTrack) -> overpy.Result:
Expand Down Expand Up @@ -219,7 +231,8 @@ def find_huts_between_daily_tracks(list_gpx_path: list[str] | list[bytes],
list[MountainHut]]:
"""Merge ordered GPX tracks into a single one and find huts between them."""
# Load GPX tracks
list_gpx_track = [GpxTrack.load(path) for path in list_gpx_path]
list_gpx_track, list_duration = zip(*[GpxTrack.load(path) for path in list_gpx_path])


if len(list_gpx_track) == 1:
return list_gpx_track[0], [], []
Expand Down Expand Up @@ -284,4 +297,4 @@ def find_huts_between_daily_tracks(list_gpx_path: list[str] | list[bytes],
huts.append(MountainHut(name=None, lat=hut_lat, lon=hut_lon))

print(f"Huts: {', '.join([h.name if h.name is not None else '?' for h in huts if h.name])}")
return full_gpx_track, huts_ids, huts
return full_gpx_track, list_duration, huts_ids, huts
2 changes: 1 addition & 1 deletion pretty_gpx/gpx/gpx_track.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ def load(gpx_path: str | bytes) -> tuple['GpxTrack', float, float]:
# possible to reduce the threshold
assert abs(all_segment_cumul_ele - gpx.get_uphill_downhill().uphill)/all_segment_cumul_ele < 0.2,f"Total climb is not coherent between point to point calculation ({all_segment_cumul_ele:.0f}) and total sum {gpx.get_uphill_downhill().uphill:.0f}"

return gpx_track
return gpx_track, gpx.get_duration()

def is_closed(self, dist_m: float) -> bool:
"""Estimate if the track is closed."""
Expand Down
32 changes: 31 additions & 1 deletion pretty_gpx/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ def change_paper_size(gpx_data: AugmentedGpxData, new_paper_size: PaperSize) ->
return PosterImageCaches.from_gpx_data(gpx_data, new_paper_size)


def change_duration_str(gpx_data: AugmentedGpxData, paper_size: PaperSize, new_duration_str: str) -> PosterImageCaches:
return PosterImageCaches.update_duration_str(gpx_data,
paper_size,
new_duration_str)


async def on_click_load_example() -> None:
contents = [os.path.join(HIKING_DIR, "vanoise1.gpx"),
os.path.join(HIKING_DIR, "vanoise2.gpx"),
Expand All @@ -74,6 +80,20 @@ async def on_paper_size_change() -> None:
cache = await run.cpu_bound(change_paper_size, copy.deepcopy(cache.gpx_data), PAPER_SIZES[new_paper_size_name])
await on_click_update()()

async def on_duration_change() -> None:
duration_value = ""
if duration_switch.value:
duration_value = override_duration.value
print("DURATION VALUE ",duration_value)
new_paper_size_name = safe(paper_size_mode_toggle.value)
with UiModal(f"Adding/deleting duration"):
global cache
cache = await run.cpu_bound(change_duration_str,
copy.deepcopy(cache.gpx_data),
PAPER_SIZES[new_paper_size_name],
duration_value)
await on_click_update()()

with ui.row():
with ui.card().classes(f'w-[{W_DISPLAY_PIX}px]').style('box-shadow: 0 0 20px 10px rgba(0, 0, 0, 0.2);'):
with ui.pyplot(close=False) as plot:
Expand Down Expand Up @@ -112,7 +132,8 @@ def _update(c: PosterImageCache) -> PosterDrawingData:
theme_colors=color_themes[safe(theme_toggle.value)],
title_txt=title_button.value,
uphill_m=uphill_button.value,
dist_km=dist_km_button.value)
dist_km=dist_km_button.value,
duration=override_duration.value)

def _update_done_callback(c: PosterImageCache, poster_drawing_data: PosterDrawingData) -> None:
with plot:
Expand Down Expand Up @@ -143,6 +164,15 @@ def on_click_update() -> Callable:
with ui.input(label='Distance (km)', value="").on('keydown.enter', on_click_update()) as dist_km_button:
ui.tooltip("Press Enter to override distance from GPX")

duration_switch = ui.switch(text='Duration', value=False)
duration_switch.tooltip("Toggle to show the duration")

override_duration = ui.input(label='Duration', value="")
override_duration.tooltip("Press the update button to override the duration from GPX")

button = ui.button('Update duration', on_click=on_duration_change)


azimuth_toggle = ui.toggle(list(AZIMUTHS.keys()), value=list(AZIMUTHS.keys())[0],
on_change=on_click_update())

Expand Down
29 changes: 29 additions & 0 deletions pretty_gpx/utils/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import os
from typing import TypeVar
import datetime

T = TypeVar('T')

Expand Down Expand Up @@ -35,3 +36,31 @@ def suffix_filename(filepath: str, suffix: str) -> str:
"""
base, ext = os.path.splitext(filepath)
return f"{base}{suffix}{ext}"

def format_timedelta(total_seconds: float | int) -> str:
"""Format the timedelta to a string"""

# Extract days, hours, minutes, and seconds
days, remainder = divmod(total_seconds, 86400) # 86400 seconds in a day
hours, remainder = divmod(remainder, 3600)
minutes, seconds = divmod(remainder, 60)

parts = []
if days > 0:
parts.append(f"{days}d-")
if hours > 0 or days > 0: # Show hours if days are shown or hours are non-zero
if days > 0:
parts.append(f"{hours:2.0f}h")
else:
parts.append(f"{hours}h")
if minutes > 0 or hours > 0 or days > 0: # Show minutes if hours or days are shown or minutes are non-zero
if days > 0 or hours > 0:
parts.append(f"{minutes:2.0f}")
else:
parts.append(f"{minutes}min")
if seconds > 0 or minutes > 0 or hours > 0 or days > 0: # Show seconds if any higher units are shown or seconds are non-zero
if not(days > 0 or hours > 0):
parts.append(f"{seconds:2.0f}")

# Join the parts with commas
return ''.join(parts) if parts else '0s'
Loading