aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/actions.py108
-rw-r--r--fietsboek/geo.py46
-rw-r--r--fietsboek/models/climb.py32
-rw-r--r--fietsboek/osm.py53
-rw-r--r--fietsboek/routes.py14
-rw-r--r--fietsboek/templates/details.jinja213
-rw-r--r--fietsboek/trackmap.py21
-rw-r--r--fietsboek/views/detail.py56
-rw-r--r--fietsboek/views/upload.py8
9 files changed, 305 insertions, 46 deletions
diff --git a/fietsboek/actions.py b/fietsboek/actions.py
index cdddaa2..43bbe82 100644
--- a/fietsboek/actions.py
+++ b/fietsboek/actions.py
@@ -7,9 +7,11 @@ the test functions.
"""
import datetime
+import gzip
import io
import logging
import re
+from threading import Thread
from typing import Optional
from pyramid.i18n import TranslationString as _
@@ -17,14 +19,15 @@ from pyramid.request import Request
from sqlalchemy import select
from sqlalchemy.orm.session import Session
-from . import convert, email, models, trackmap
+from . import convert, email, models, osm, trackmap
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
+from .views.tileproxy import ITileRequester, TileRequester
LOGGER = logging.getLogger(__name__)
@@ -278,4 +281,103 @@ def send_verification_token(request: Request, user: models.User):
)
-__all__ = ["add_track", "edit_images", "execute_transformers", "send_verification_token"]
+def _categorize_climb(length: float, avg_grade: float) -> int | None:
+ # https://bicycles.stackexchange.com/questions/1210/how-are-the-categories-for-climbs-decided
+ score = length * avg_grade * 100
+ if score > 80000:
+ return 0
+ if score > 64000:
+ return 1
+ if score > 32000:
+ return 2
+ if score > 16000:
+ return 3
+ if score > 8000:
+ return 4
+ # You call this a climb?...
+ return None
+
+
+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.
+ """
+ # pylint: disable=too-many-locals
+ 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))
+ profile_compressed = gzip.compress(profile.encode("ascii"))
+
+ avg_grade = movement_data.elevation_gain / movement_data.length
+
+ name = osm.find_name(subpath)
+
+ climb = models.Climb(
+ track=track,
+ start_index=climb_start,
+ end_index=climb_end,
+ image=preview,
+ profile=profile_compressed,
+ avg_grade=avg_grade,
+ length=movement_data.length,
+ lowest_elevation=movement_data.min_elevation,
+ highest_elevation=movement_data.max_elevation,
+ category=_categorize_climb(movement_data.length, avg_grade),
+ automatic_name=name,
+ )
+ dbsession.add(climb)
+
+
+def extract_climbs_background(request: Request, track_id: int):
+ """Re-extracts the climbs of the given track in the background.
+
+ This spawns a new thread that calls :func:`extract_climbs` in a new thread.
+
+ :param request: The request.
+ :param track_id: The track's id.
+ """
+ loader: ITileRequester = request.registry.getUtility(ITileRequester)
+ layer = request.config.public_tile_layers()[0]
+
+ def _extract_climbs():
+ LOGGER.debug("Starting climb extraction for track %d", track_id)
+ request.redis.set(f"bg-tasks:track-{track_id}", 1)
+ session = request.registry["dbsession_factory"]()
+ track = session.get(models.Track, track_id)
+ with session:
+ extract_climbs(session, track, loader, layer)
+ session.commit()
+ request.redis.delete(f"bg-tasks:track-{track_id}")
+ LOGGER.debug("Climb extraction done")
+
+ Thread(name=f"climbs-{track_id}", target=_extract_climbs).start()
+
+
+__all__ = [
+ "add_track",
+ "edit_images",
+ "execute_transformers",
+ "send_verification_token",
+ "extract_climbs",
+ "extract_climbs_background",
+]
diff --git a/fietsboek/geo.py b/fietsboek/geo.py
index 91da445..3a38ee1 100644
--- a/fietsboek/geo.py
+++ b/fietsboek/geo.py
@@ -149,8 +149,6 @@ class Point:
class Path:
"""A GPS path, that is a series of GPS points."""
- # pylint: disable=too-few-public-methods
-
def __init__(self, points: list[Point]):
self.points = points
@@ -159,6 +157,7 @@ class Path:
def movement_data(self) -> MovementData:
"""Returns the movement data."""
+ # pylint: disable=too-many-branches
movement_data = MovementData()
for a, b in self._point_pairs():
distance = a.distance(b)
@@ -207,6 +206,10 @@ class Path:
return movement_data
def bounds(self) -> tuple[tuple[float, float], tuple[float, float]]:
+ """Returns the bounds of this path.
+
+ :return: Bounds in the form `(min_lat, min_lon), (max_lat, max_lon)`.
+ """
min_lat = float("+inf")
max_lat = float("-inf")
min_lon = float("+inf")
@@ -229,8 +232,8 @@ class Path:
# After all the post-processing of climbs, it might happen that we get
# climbs shorter than the minimum amount again. Therefore, we do another
# filtering step here.
- for (i, j) in self._trimmed_climbs():
- path = Path(self.points[i:j + 1])
+ for i, j in self._trimmed_climbs():
+ path = Path(self.points[i : j + 1])
if path.movement_data().length >= CLIMB_MIN_LENGTH:
yield (i, j)
@@ -263,11 +266,12 @@ class Path:
i = 0
while i < len(self.points):
- left = self.points[i]
j = i
length = 0.0
elevation_diff = 0.0
- while j + 1 < len(self.points) and (length == 0.0 or elevation_diff / length >= CLIMB_MIN_ASCENSION):
+ while j + 1 < len(self.points) and (
+ length == 0.0 or elevation_diff / length >= CLIMB_MIN_ASCENSION
+ ):
length += self.points[j].distance(self.points[j + 1])
elevation_diff += self.points[j + 1].elevation - self.points[j].elevation
j += 1
@@ -278,6 +282,18 @@ class Path:
i += 1
def similarity(self, reference: "Path") -> float:
+ """Returns a similarity measure between this track and the given other track.
+
+ The higher the returned number is, the more *dissimilar* the given tracks are.
+
+ Note that currently, this measure is not symmetric, as ``other`` is cut
+ to match ``self`` in length (on a best-effort basis). Therefore,
+ ``longer.similarity(shorter)`` and ``shorter.similarity(longer)`` will
+ not be the same.
+
+ :param reference: The other track.
+ :return: The similarity measure.
+ """
start_idx = 0
start_dst = float("+inf")
end_idx = len(reference.points) - 1
@@ -292,9 +308,9 @@ class Path:
end_dst = dst
if start_idx < end_idx:
- reference = Path(reference.points[start_idx:end_idx + 1])
+ reference = Path(reference.points[start_idx : end_idx + 1])
else:
- subpath = reference.points[end_idx:start_idx + 1]
+ subpath = reference.points[end_idx : start_idx + 1]
subpath.reverse()
reference = Path(subpath)
@@ -309,6 +325,18 @@ class Path:
return similarity / NUM_SIMILARITY_SAMPLES
def interpolate(self, where: float) -> Point:
+ """Returns a point in the middle of the path.
+
+ If ``where`` is 0, the beginning of the path is returned. If ``where``
+ is 1, the end is returned.
+
+ Unlike accessing ``self.points``, this function can return points
+ between two points in the path.
+
+ :param where: Float between 0 and 1 indicating the position of the
+ wanted point.
+ :return: The interpolated point.
+ """
if len(self.points) == 1:
return self.points[0]
@@ -329,6 +357,7 @@ class Path:
elevation=a.elevation + (b.elevation - a.elevation) * partial,
time_offset=a.time_offset + (b.time_offset - a.time_offset) * partial,
)
+ raise ValueError(f"Invalid where: {where}")
def _flat_length(self):
length = 0.0
@@ -336,6 +365,7 @@ class Path:
length += a.flat_distance(b)
return length
+
def gpx_xml(
title: str | None,
description: str | None,
diff --git a/fietsboek/models/climb.py b/fietsboek/models/climb.py
index a2040f2..8fefa82 100644
--- a/fietsboek/models/climb.py
+++ b/fietsboek/models/climb.py
@@ -1,22 +1,18 @@
"""The Climb model."""
-from typing import TYPE_CHECKING
-
from pyramid.httpexceptions import HTTPNotFound
from pyramid.request import Request
from sqlalchemy import Float, ForeignKey, Integer, LargeBinary, Text, select
from sqlalchemy.orm import Mapped, mapped_column, relationship
-from .meta import Base
from ..geo import Path
-
-if TYPE_CHECKING:
- from .track import Track
+from .meta import Base
+from .track import Track
class Climb(Base):
+ """A climb represents a subpath of a track that ascends at least 3% over 500m."""
- # pylint: disable=too-few-public-methods
__tablename__ = "climbs"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
track_id: Mapped[int] = mapped_column(Integer, ForeignKey("tracks.id"), nullable=False)
@@ -61,17 +57,35 @@ class Climb(Base):
def path(self) -> Path:
"""Returns the path of this climb."""
path = self.track.path()
- return Path(path.points[self.start_index:self.end_index])
+ return Path(path.points[self.start_index : self.end_index])
def display_name(self) -> str:
+ """Returns the display name.
+
+ If the given name (by the user) is set, returns this. Otherwise, return
+ the autmatic name.
+
+ :return: The name that should be displayed.
+ """
if self.given_name:
return self.given_name
- return self.automatic_name
+ return self.automatic_name or ""
def bounds(self) -> tuple[tuple[float, float], tuple[float, float]]:
+ """Returns the bounds of this climb.
+
+ See :func:`Path.bounds`.
+ """
return self.path().bounds()
def latlon_points(self) -> list[tuple[float, float]]:
+ """Returns all points of this path as coordinates.
+
+ The returned points will be the latitude, longitude coordinates.
+
+ :return: The points as coordinates.
+ """
return [point.latlon() for point in self.path().points]
+
__all__ = ["Climb"]
diff --git a/fietsboek/osm.py b/fietsboek/osm.py
index 101bb89..e75bd11 100644
--- a/fietsboek/osm.py
+++ b/fietsboek/osm.py
@@ -1,7 +1,6 @@
"""OSM related utilities for fietsboek."""
import logging
-import pathlib
from urllib.parse import urljoin
from xml.etree import ElementTree
@@ -14,6 +13,7 @@ LOGGER = logging.getLogger(__name__)
API_URL = "https://api.openstreetmap.org/"
HEADERS = {
"user-agent": f"Fietsboek/{__VERSION__}",
+ "accept-encoding": "gzip;q=1.0, identity;q=0.5",
}
MAX_SIMILARITY_STREETNAME = 100
@@ -30,8 +30,9 @@ def find_name(path: Path) -> str:
"""
LOGGER.debug("Starting naming process")
session = requests.Session()
+ session.headers.update(HEADERS)
- ((min_lat, min_lon), (max_lat, max_lon)) = path.bounds()
+ (min_lat, min_lon), (max_lat, max_lon) = path.bounds()
min_lat -= 0.001
min_lon -= 0.0015
@@ -41,7 +42,7 @@ def find_name(path: Path) -> str:
LOGGER.debug("bbox=%s,%s,%s,%s", min_lon, min_lat, max_lon, max_lat)
urlpath = f"api/0.6/map?bbox={min_lon},{min_lat},{max_lon},{max_lat}"
- with requests.get(urljoin(API_URL, urlpath), headers=HEADERS) as response:
+ with session.get(urljoin(API_URL, urlpath)) as response:
data = response.content
tree = ElementTree.fromstring(data)
@@ -62,29 +63,34 @@ def find_name(path: Path) -> str:
return name
-def _name_from_street(tree: ElementTree.ElementTree, path: Path) -> str | None:
+def _name_from_street(tree: ElementTree.Element, path: Path) -> str | None:
+ # pylint: disable=too-many-locals,too-many-branches,too-many-statements
LOGGER.debug("Attempting to name by street...")
node_to_coords = {}
for node in tree.iterfind("./node"):
node_id = node.get("id")
- lat = float(node.get("lat"))
- lon = float(node.get("lon"))
+ lat = float(node.get("lat") or 0.0)
+ lon = float(node.get("lon") or 0.0)
node_to_coords[node_id] = (lat, lon)
- named_streets = {}
+ named_streets: dict[str, list[list[str]]] = {}
+ # First, we find all named streets. Streets may contain of several
+ # "stretches" that have the same name
for way in tree.iterfind("./way"):
name_tag = way.find("./tag[@k='name']")
if name_tag is None:
continue
name = name_tag.get("v")
+ assert name is not None
# Generally, highway=* indicates roads
highway_elem = way.find("./tag[@k='highway']")
if highway_elem is None:
continue
- nodes = [nd.get("ref") for nd in way.iterfind("./nd")]
+ nodes_refs = [nd.get("ref") for nd in way.iterfind("./nd")]
+ nodes = [ref for ref in nodes_refs if ref is not None]
stretches = named_streets.setdefault(name, [])
stretches.append(nodes)
@@ -92,9 +98,13 @@ def _name_from_street(tree: ElementTree.ElementTree, path: Path) -> str | None:
best_name = None
best_score = None
+ # Next, we iterate over the streets we found to see how well they match
for name, stretches in named_streets.items():
merged_way = stretches[0]
remaining = stretches[1:]
+ # This combines the stretches into a single long street, by checking
+ # whether we can find a stretch that "glues" onto the end or beginning
+ # of the current merged street.
while remaining:
num_remaining = len(remaining)
anchor = merged_way[-1]
@@ -126,6 +136,8 @@ def _name_from_street(tree: ElementTree.ElementTree, path: Path) -> str | None:
# Made no progress, abort
break
+ # Now that we have the merged street, we can check the similarity after
+ # converting the node references to coordinates
points = []
for node_id in merged_way:
coords = node_to_coords[node_id]
@@ -149,7 +161,7 @@ def _name_from_street(tree: ElementTree.ElementTree, path: Path) -> str | None:
return None
-def _name_from_peak(tree: ElementTree.ElementTree, path: Path) -> str | None:
+def _name_from_peak(tree: ElementTree.Element, path: Path) -> str | None:
LOGGER.debug("Attempting to name by peak...")
best_name = None
best_distance = None
@@ -161,8 +173,8 @@ def _name_from_peak(tree: ElementTree.ElementTree, path: Path) -> str | None:
if name_elem is None:
continue
name = name_elem.get("v")
- lat = float(peak.get("lat"))
- lon = float(peak.get("lon"))
+ lat = float(peak.get("lat") or 0.0)
+ lon = float(peak.get("lon") or 0.0)
peak_point = Point(longitude=lon, latitude=lat, elevation=0.0, time_offset=0.0)
distance = peak_point.flat_distance(path.points[-1])
@@ -179,15 +191,15 @@ def _name_from_peak(tree: ElementTree.ElementTree, path: Path) -> str | None:
return None
-def _name_from_water(tree: ElementTree.ElementTree, path: Path) -> str | None:
+def _name_from_water(tree: ElementTree.Element, path: Path) -> str | None:
LOGGER.debug("Attempting to name by water...")
best_name = None
best_distance = None
node_to_coord = {}
for water in tree.iterfind("./node"):
node_id = water.get("id")
- lat = float(water.get("lat"))
- lon = float(water.get("lon"))
+ lat = float(water.get("lat") or 0.0)
+ lon = float(water.get("lon") or 0.0)
node_to_coord[node_id] = (lat, lon)
if water.find("./tag[@k='natural'][@v='water']") is None:
@@ -223,7 +235,6 @@ def _name_from_water(tree: ElementTree.ElementTree, path: Path) -> str | None:
best_distance = distance
best_name = name
-
accepted = best_distance is not None and best_distance <= MAX_DISTANCE_WATER
LOGGER.debug(
"Best name: %s (distance=%s) (accepted=%s)",
@@ -237,7 +248,7 @@ def _name_from_water(tree: ElementTree.ElementTree, path: Path) -> str | None:
return None
-def _name_from_city(tree: ElementTree.ElementTree, path: Path) -> str | None:
+def _name_from_city(tree: ElementTree.Element, path: Path) -> str | None:
LOGGER.debug("Attempting to name by city...")
best_name = None
best_distance = None
@@ -254,8 +265,8 @@ def _name_from_city(tree: ElementTree.ElementTree, path: Path) -> str | None:
if name_elem is None:
continue
name = name_elem.get("v")
- lat = float(city.get("lat"))
- lon = float(city.get("lon"))
+ lat = float(city.get("lat") or 0.0)
+ lon = float(city.get("lon") or 0.0)
city_point = Point(longitude=lon, latitude=lat, elevation=0.0, time_offset=0.0)
distance = city_point.flat_distance(path.points[-1])
@@ -279,7 +290,7 @@ def _name_from_city(tree: ElementTree.ElementTree, path: Path) -> str | None:
return None
-def _fallback_name(tree: ElementTree.ElementTree, path: Path) -> str | None:
+def _fallback_name(tree: ElementTree.Element, path: Path) -> str | None:
LOGGER.debug("Attempting to find a fallback name...")
best_name = None
best_distance = None
@@ -288,8 +299,8 @@ def _fallback_name(tree: ElementTree.ElementTree, path: Path) -> str | None:
if name_elem is None:
continue
name = name_elem.get("v")
- lat = float(node.get("lat"))
- lon = float(node.get("lon"))
+ lat = float(node.get("lat") or 0.0)
+ lon = float(node.get("lon") or 0.0)
point = Point(longitude=lon, latitude=lat, elevation=0.0, time_offset=0.0)
distance = point.flat_distance(path.points[-1])
diff --git a/fietsboek/routes.py b/fietsboek/routes.py
index 7042415..2e1a171 100644
--- a/fietsboek/routes.py
+++ b/fietsboek/routes.py
@@ -41,6 +41,9 @@ def includeme(config):
"delete-track", "/track/{track_id}/delete", factory="fietsboek.models.Track.factory"
)
config.add_route(
+ "reclimb-track", "/track/{track_id}/reclimb", factory="fietsboek.models.Track.factory"
+ )
+ config.add_route(
"add-comment", "/track/{track_id}/comment", factory="fietsboek.models.Track.factory"
)
config.add_route(
@@ -57,6 +60,17 @@ def includeme(config):
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(
"journey-map",
diff --git a/fietsboek/templates/details.jinja2 b/fietsboek/templates/details.jinja2
index fb2f9d8..a800998 100644
--- a/fietsboek/templates/details.jinja2
+++ b/fietsboek/templates/details.jinja2
@@ -36,8 +36,14 @@
<div class="btn-group" role="group">
<a class="btn btn-success ui-element" href="{{ request.route_path('edit', track_id=track.id) }}"><i class="bi-pencil-square"></i> {{ _("page.details.edit") }}</a>
<button type="button" class="btn btn-info ui-element" id="showShareLink" data-bs-toggle="modal" data-bs-target="#shareLinkModal"><i class="bi-share"></i> {{ _("page.details.share") }}</button>
+ <button type="submit" class="btn btn-info ui-element" form="reclimbForm"><i class="bi bi-graph-up-arrow"></i> {{ _("page.details.reclimb") }}</button>
<button type="button" class="btn btn-danger ui-element" id="deleteLink" data-bs-toggle="modal" data-bs-target="#deleteModal"><i class="bi bi-trash"></i> {{ _("page.details.delete") }}</button>
</div>
+
+ <form id="reclimbForm" method="POST" action="{{ request.route_path('reclimb-track', track_id=track.id) }}">
+ {{ util.hidden_csrf_input() }}
+ </form>
+
<div class="modal fade" id="shareLinkModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
@@ -99,6 +105,13 @@
</p>
{% endif %}
+ {% if bgtasks_in_progress %}
+ <div class="alert alert-warning" role="alert">
+ <i class="bi bi-exclamation-diamond-fill"></i>
+ {{ _("page.details.background_tasks_in_progress") }}
+ </div>
+ {% endif %}
+
{% if 'secret' in request.GET %}
{% set gpx_url = request.route_path('gpx', track_id=track.id, _query=[('secret', request.GET['secret'])]) %}
{% else %}
diff --git a/fietsboek/trackmap.py b/fietsboek/trackmap.py
index cb88264..7584ece 100644
--- a/fietsboek/trackmap.py
+++ b/fietsboek/trackmap.py
@@ -146,6 +146,7 @@ def render(
def _ascension_to_color(ascension: float) -> str:
+ # pylint: disable=too-many-return-statements
if ascension >= 0.20:
return "#99108a"
if ascension >= 0.14:
@@ -162,6 +163,18 @@ def _ascension_to_color(ascension: float) -> str:
def height_profile(track: geo.Path, size: tuple[int, int] = (300, 300)) -> str:
+ """Generates a height profile for the given track.
+
+ The output will be the image as a SVG source.
+
+ Note that SVG is a vector graphic, so the given size is only a hint
+ embedded in metadata. The image is freely scalable.
+
+ :param track: Path for which to generate a height profile.
+ :param size: Display size of the image (embedded as metadata).
+ :return: The height profile as SVG image.
+ """
+ # pylint: disable=too-many-locals
out = io.StringIO()
width, height = size
@@ -177,14 +190,17 @@ def height_profile(track: geo.Path, size: tuple[int, int] = (300, 300)) -> str:
'xmlns="http://www.w3.org/2000/svg">\n'
)
- cur_x = 0
+ cur_x = 0.0
for left, right in zip(track.points, track.points[1:]):
right_x = cur_x + (left.distance(right) / path_len) * 1000
left_y = (left.elevation - min_elev) / delta_elev * 1000
right_y = (right.elevation - min_elev) / delta_elev * 1000
ascension = (right.elevation - left.elevation) / max(left.distance(right), 0.001)
color = _ascension_to_color(ascension)
- out.write(f'<polygon points="{cur_x},0 {cur_x},{-left_y} {right_x},{-right_y} {right_x},0" fill="{color}" stroke="{color}" />\n')
+ out.write(
+ f'<polygon points="{cur_x},0 {cur_x},{-left_y} {right_x},{-right_y} {right_x},0" '
+ f'fill="{color}" stroke="{color}" />\n'
+ )
cur_x = right_x
out.write("</svg>")
@@ -192,7 +208,6 @@ def height_profile(track: geo.Path, size: tuple[int, int] = (300, 300)) -> str:
result = out.getvalue()
out.close()
return result
- return out
__all__ = ["to_web_mercator", "TrackMapRenderer", "render", "height_profile"]
diff --git a/fietsboek/views/detail.py b/fietsboek/views/detail.py
index ca3a0af..91778fd 100644
--- a/fietsboek/views/detail.py
+++ b/fietsboek/views/detail.py
@@ -18,7 +18,7 @@ from pyramid.response import FileResponse, Response
from pyramid.view import view_config
from sqlalchemy import select
-from .. import models, pdf, trackmap, util
+from .. import actions, models, pdf, trackmap, util
from ..models.track import Track, TrackWithMetadata
from .tileproxy import ITileRequester
@@ -104,6 +104,8 @@ def details(request):
# Strip off the sort key again
images = [(image[1], image[2]) for image in images]
+ bgtasks_in_progress = request.redis.get(f"bg-tasks:track-{track.id}") is not None
+
with_meta = TrackWithMetadata(track)
return {
"track": with_meta,
@@ -114,6 +116,7 @@ def details(request):
"description": description,
"og_description": og_description,
"images": images,
+ "bgtasks_in_progress": bgtasks_in_progress,
}
@@ -192,6 +195,17 @@ def delete_track(request):
return HTTPFound(request.route_url("home"))
+@view_config(route_name="reclimb-track", request_method="POST", permission="track.edit")
+def reclimb_track(request: Request) -> Response:
+ """Endpoint to re-extract the track's climbs.
+
+ :param request: The Pyramid request.
+ :return: The HTTP response.
+ """
+ actions.extract_climbs_background(request, request.context.id)
+ return HTTPFound(request.route_url("details", track_id=request.context.id))
+
+
@view_config(route_name="badge", http_cache=3600)
def badge(request):
"""Returns the image data associated with a badge.
@@ -298,6 +312,44 @@ 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
+ accepted = request.accept_encoding.acceptable_offers(["gzip", "identity"])
+ for encoding, _qvalue in accepted:
+ if encoding == "gzip":
+ response = Response(
+ climb.profile,
+ content_type="image/svg+xml",
+ content_encoding="gzip",
+ )
+ break
+ if encoding == "identity":
+ data = gzip.decompress(climb.profile)
+ response = Response(data, content_type="image/svg+xml")
+ break
+ else:
+ return HTTPNotAcceptable("No data with acceptable encoding found")
+ return response
+
+
__all__ = [
"details",
"gpx",
@@ -308,4 +360,6 @@ __all__ = [
"add_comment",
"track_map",
"track_pdf",
+ "climb_preview",
+ "climb_profile",
]
diff --git a/fietsboek/views/upload.py b/fietsboek/views/upload.py
index 7be5a42..dde1c6c 100644
--- a/fietsboek/views/upload.py
+++ b/fietsboek/views/upload.py
@@ -179,7 +179,13 @@ def do_finish_upload(request):
if request.config.hittekaart_autogenerate:
request.redis.sadd("hittekaart:queue:high", request.identity.id)
- return HTTPFound(request.route_url("details", track_id=track.id))
+ track_id = track.id
+ request.tm.commit()
+ request.tm.begin()
+
+ actions.extract_climbs_background(request, track_id)
+
+ return HTTPFound(request.route_url("details", track_id=track_id))
@view_config(route_name="cancel-upload", permission="upload.finish", request_method="POST")