Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
JulesL2 committed Sep 12, 2024
1 parent f3815d9 commit d004451
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 28 deletions.
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)
80 changes: 61 additions & 19 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,
passes_ids=gpx_data.passes_ids,
huts_ids=gpx_data.hut_ids,
daily_dist_km=gpx_data.daily_dist_km,
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,
passes_ids=gpx_data.passes_ids,
huts_ids=gpx_data.hut_ids,
daily_dist_km=gpx_data.daily_dist_km,
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,
daily_dist_km: list[float],
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

if len(huts_ids) == 0:
elevation_poly_x = np.linspace(0., w_pix, num=len(list_ele))
Expand Down Expand Up @@ -348,11 +381,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")
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
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
23 changes: 19 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 @@ -51,6 +52,8 @@ class AugmentedGpxData:
hut_ids: list[int]
daily_dist_km: list[float] # Distance (in km) of each daily track

total_duration: str | None

@staticmethod
def from_path(list_gpx_path: str | bytes | list[str] | list[bytes],
strict_ths_m: float = 50,
Expand All @@ -65,8 +68,17 @@ def from_path(list_gpx_path: str | bytes | list[str] | list[bytes],
dist_km,
uphill_m,
daily_dist_km,
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)
close_to_start = is_close_to_a_mountain_pass(lon=gpx_track.list_lon[0],
Expand Down Expand Up @@ -98,7 +110,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 @@ -225,14 +238,16 @@ def find_huts_between_daily_tracks(list_gpx_path: list[str] | list[bytes],
list_gpx_track: list[GpxTrack] = []
list_dist_km: list[float] = []
list_uphill_m: list[float] = []
list_duration: list[float] = []
for path in list_gpx_path:
gpx_track, dist_km, uphill_m = GpxTrack.load(path)
gpx_track, dist_km, uphill_m, duration = GpxTrack.load(path)
list_gpx_track.append(gpx_track)
list_dist_km.append(dist_km)
list_uphill_m.append(uphill_m)
list_duration.append(duration)

if len(list_gpx_track) == 1:
return list_gpx_track[0], list_dist_km[0], list_uphill_m[0], list_dist_km, [], []
return list_gpx_track[0], list_dist_km[0], list_uphill_m[0], list_dist_km, list_duration, [], []

# Assert consecutive tracks
for i in range(len(list_gpx_track) - 1):
Expand Down Expand Up @@ -304,4 +319,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, full_dist_km, full_uphill_m, list_dist_km, huts_ids, huts
return full_gpx_track, full_dist_km, full_uphill_m, list_dist_km, 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 @@ -56,7 +56,7 @@ def load(gpx_path: str | bytes) -> tuple['GpxTrack', float, float]:
plt.ylabel('Elevation (in m)')
plt.show()

return gpx_track, gpx.length_3d()*1e-3, gpx.get_uphill_downhill().uphill
return gpx_track, gpx.length_3d()*1e-3, gpx.get_uphill_downhill().uphill, 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'

0 comments on commit d004451

Please sign in to comment.