diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2026-09-27 12:05:42 +0200 |
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2026-09-27 12:05:42 +0200 |
| commit | 2c9c566fea75bb25964dbf82f1cca22efa25023f (patch) | |
| tree | 34012d018684cf8c96a86cb22366126c55bbca80 | |
| parent | 3a5187c118aba5eac93e85c9394346fc49fed604 (diff) | |
| download | fietsboek-2c9c566fea75bb25964dbf82f1cca22efa25023f.tar.gz fietsboek-2c9c566fea75bb25964dbf82f1cca22efa25023f.tar.bz2 fietsboek-2c9c566fea75bb25964dbf82f1cca22efa25023f.zip | |
wipwip
| -rw-r--r-- | fietsboek/actions.py | 45 | ||||
| -rw-r--r-- | fietsboek/geo.py | 34 | ||||
| -rw-r--r-- | fietsboek/models/climb.py | 11 | ||||
| -rw-r--r-- | fietsboek/routes.py | 12 | ||||
| -rw-r--r-- | fietsboek/templates/details.jinja2 | 32 | ||||
| -rw-r--r-- | fietsboek/views/detail.py | 43 |
6 files changed, 174 insertions, 3 deletions
diff --git a/fietsboek/actions.py b/fietsboek/actions.py index cdddaa2..1290726 100644 --- a/fietsboek/actions.py +++ b/fietsboek/actions.py @@ -22,6 +22,7 @@ from . import transformers as mod_transformers from . import util from .config import TileLayerConfig from .data import DataManager, TrackDataDir +from .geo import Path from .models.track import TrackType, Visibility from .models.user import TokenType from .views.tileproxy import TileRequester @@ -278,4 +279,46 @@ def send_verification_token(request: Request, user: models.User): ) -__all__ = ["add_track", "edit_images", "execute_transformers", "send_verification_token"] +def extract_climbs( + dbsession: Session, + track: models.Track, + tile_requester: TileRequester, + layer: TileLayerConfig, +): + """(Re-)extracts the climbs from the given track. + + If the track already has climbs assigned, they will be deleted. + + :param dbsession: The database session. + :param track: The track for which to extract climbs. + """ + track.climbs = [] + dbsession.add(track) + path = track.path() + climbs = path.climbs() + for climb_start, climb_end in climbs: + subpath = Path(path.points[climb_start:climb_end]) + movement_data = subpath.movement_data() + + preview = trackmap.render(subpath, layer, tile_requester) + imageio = io.BytesIO() + preview.save(imageio, "png") + preview = imageio.getvalue() + + profile = trackmap.height_profile(subpath, (300, 150)) + + climb = models.Climb( + track=track, + start_index=climb_start, + end_index=climb_end, + image=preview, + profile=profile.encode("ascii"), + avg_grade=movement_data.elevation_gain / movement_data.length, + length=movement_data.length, + lowest_elevation=movement_data.min_elevation, + highest_elevation=movement_data.max_elevation, + ) + dbsession.add(climb) + + +__all__ = ["add_track", "edit_images", "execute_transformers", "send_verification_token", "extract_climbs"] diff --git a/fietsboek/geo.py b/fietsboek/geo.py index 190454b..6f9a3c6 100644 --- a/fietsboek/geo.py +++ b/fietsboek/geo.py @@ -69,6 +69,15 @@ class MovementData: downhill: float = 0.0 """Downhill elevation, in meters.""" + elevation_gain: float = 0.0 + """Elevation gain, in meters.""" + + min_elevation: float = 0.0 + """Minimum elevation, in meters.""" + + max_elevation: float = 0.0 + """Maximum elevation, in meters.""" + @dataclass(slots=True) class Point: @@ -157,6 +166,25 @@ class Path: else: speed = 0.0 elevation = b.elevation - a.elevation + movement_data.elevation_gain += elevation + + if movement_data.min_elevation == 0.0: + movement_data.min_elevation = min(a.elevation, b.elevation) + else: + movement_data.min_elevation = min( + a.elevation, + b.elevation, + movement_data.min_elevation, + ) + + if movement_data.max_elevation == 0.0: + movement_data.max_elevation = max(a.elevation, b.elevation) + else: + movement_data.max_elevation = max( + a.elevation, + b.elevation, + movement_data.max_elevation, + ) movement_data.length += distance if speed >= MOVING_THRESHOLD: @@ -208,6 +236,12 @@ class Path: length += self.points[j].distance(self.points[j + 1]) elevation_diff += self.points[j + 1].elevation - self.points[j].elevation j += 1 + backlen = 0.0 + back_elevation = 0.0 + while j > i + 1 and (backlen == 0.0 or back_elevation / backlen < 0.0): + backlen += self.points[j - 1].distance(self.points[j]) + back_elevation += self.points[j].elevation - self.points[j - 1].elevation + j -= 1 if length >= CLIMB_MIN_LENGTH: yield (i, j) i = j diff --git a/fietsboek/models/climb.py b/fietsboek/models/climb.py index 3dd8d17..33ee0e2 100644 --- a/fietsboek/models/climb.py +++ b/fietsboek/models/climb.py @@ -4,7 +4,7 @@ from typing import TYPE_CHECKING from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request -from sqlalchemy import ForeignKey, Integer, LargeBinary, Text, select +from sqlalchemy import Float, ForeignKey, Integer, LargeBinary, Text, select from sqlalchemy.orm import Mapped, mapped_column, relationship from .meta import Base @@ -22,7 +22,14 @@ class Climb(Base): track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), nullable=False) automatic_name: Mapped[str | None] = mapped_column(Text) given_name: Mapped[str | None] = mapped_column(Text) - image: Mapped[bytes] = mapped_column(LargeBinary) + image: Mapped[bytes] = mapped_column(LargeBinary, nullable=False) + profile: Mapped[bytes] = mapped_column(LargeBinary, nullable=False) + + avg_grade: Mapped[float] = mapped_column(Float, nullable=False) + length: Mapped[float] = mapped_column(Float, nullable=False) + lowest_elevation: Mapped[float] = mapped_column(Float, nullable=False) + highest_elevation: Mapped[float] = mapped_column(Float, nullable=False) + category: Mapped[int | None] = mapped_column(Integer, nullable=True) start_index: Mapped[int] = mapped_column(Integer, nullable=False) end_index: Mapped[int] = mapped_column(Integer, nullable=False) diff --git a/fietsboek/routes.py b/fietsboek/routes.py index 7042415..442c1c3 100644 --- a/fietsboek/routes.py +++ b/fietsboek/routes.py @@ -56,6 +56,18 @@ def includeme(config): "/track/{track_id}/index.pdf", factory="fietsboek.models.Track.factory", ) + config.add_route("climbs", "/track/{track_id}/climbs.json", factory="fietsboek.models.Track.factory") + + config.add_route( + "climb-preview", + "/climb/{climb_id}/preview", + factory="fietsboek.models.Climb.factory", + ) + config.add_route( + "climb-profile", + "/climb/{climb_id}/profile", + factory="fietsboek.models.Climb.factory", + ) config.add_route("journey-list", "/journey/") config.add_route( diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2 index f97f01f..f07aa77 100644 --- a/fietsboek/templates/details.jinja2 +++ b/fietsboek/templates/details.jinja2 @@ -110,6 +110,23 @@ <div id="mainmap_hp" style="width:100%;height:300px"> </div> + <script> + setTimeout(() => { + let colors = ["yellow", "black", "blue"]; + fetch("{{ track.id }}/climbs.json").then((resp) => resp.json()).then((resp) => { + let map = document.getElementById("mainmap").makeMap.GetMap().map; + console.log(resp); + let i = 0; + for (let climb of resp) { + let points = climb.map(([lat, lon]) => new L.LatLng(lat, lon)); + let poly = new L.Polyline(points, { color: colors[i % colors.length] }); + poly.addTo(map); + ++i; + } + })}, 1000); + </script> + + <div class="mb-3"> <div class="btn-group" role="group"> <a class="btn btn-primary ui-element" href="{{ gpx_url }}"><i class="bi bi-download"></i> {{ _("page.details.download") }}</a> @@ -176,6 +193,21 @@ {% endfor %} </div> {% endif %} + + {% if track.climbs %} + <h2>{{ _("page.details.climbs") }}</h2> + <div class="climbs" style="display: flex; flex-wrap: wrap;"> + {% for climb in track.climbs %} + <div class="climb card"> + {{ climb.display_name() }} + + <img src="{{ request.route_url('climb-preview', climb_id=climb.id) }}"> + <img src="{{ request.route_url('climb-profile', climb_id=climb.id) }}"> + </div> + {% endfor %} + </div> + {% endif %} + {% if description %} <div class="track-description"> {{ description }} diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py index ca3a0af..eb41211 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -76,6 +76,12 @@ def details(request): :rtype: pyramid.response.Response """ track = request.context + + loader: ITileRequester = request.registry.getUtility(ITileRequester) + layer = request.config.public_tile_layers()[0] + from fietsboek import actions + actions.extract_climbs(request.dbsession, track, loader, layer) + description = util.safe_markdown(track.description) og_description = _og_description(description) show_edit_link = track.owner == request.identity @@ -117,6 +123,18 @@ def details(request): } +@view_config( + route_name="climbs", renderer="json", permission="track.view" +) +def climbs(request): + track = request.context + path = track.path() + return [ + [path.points[i].latlon() for i in range(climb_start, climb_end + 1)] + for climb_start, climb_end in path.climbs() + ] + + @view_config(route_name="gpx", http_cache=3600, permission="track.view") def gpx(request): """Returns the actual GPX data from the stored track. @@ -298,6 +316,30 @@ def track_pdf(request: Request): return response +@view_config(route_name="climb-preview", permission="track.view") +def climb_preview(request: Request): + """Endpoint to return the climb's preview image. + + :param request: The pyramid request. + :return: The HTTP response. + """ + climb: models.Climb = request.context + response = Response(climb.image, content_type="image/png") + return response + + +@view_config(route_name="climb-profile", permission="track.view") +def climb_profile(request: Request): + """Endpoint to return the climb's profile image. + + :param request: The pyramid request. + :return: The HTTP response. + """ + climb: models.Climb = request.context + response = Response(climb.profile, content_type="image/svg+xml") + return response + + __all__ = [ "details", "gpx", @@ -308,4 +350,5 @@ __all__ = [ "add_comment", "track_map", "track_pdf", + "climb_preview", ] |
