aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/actions.py45
-rw-r--r--fietsboek/geo.py34
-rw-r--r--fietsboek/models/climb.py11
-rw-r--r--fietsboek/routes.py12
-rw-r--r--fietsboek/templates/details.jinja232
-rw-r--r--fietsboek/views/detail.py43
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",
]