diff options
| -rw-r--r-- | fietsboek/actions.py | 108 | ||||
| -rw-r--r-- | fietsboek/geo.py | 46 | ||||
| -rw-r--r-- | fietsboek/models/climb.py | 32 | ||||
| -rw-r--r-- | fietsboek/osm.py | 53 | ||||
| -rw-r--r-- | fietsboek/routes.py | 14 | ||||
| -rw-r--r-- | fietsboek/templates/details.jinja2 | 13 | ||||
| -rw-r--r-- | fietsboek/trackmap.py | 21 | ||||
| -rw-r--r-- | fietsboek/views/detail.py | 56 | ||||
| -rw-r--r-- | fietsboek/views/upload.py | 8 |
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") |
