diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2026-09-30 18:43:24 +0200 |
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2026-09-30 18:43:24 +0200 |
| commit | 1f9e1d0517266418ea5a61f5c9948783ab0a604a (patch) | |
| tree | ea52c1b8d601dc217dd140dcc1c89f237ad13286 | |
| parent | 8f5bd60f507b695ccdc881fdda59e60d121606eb (diff) | |
| download | fietsboek-1f9e1d0517266418ea5a61f5c9948783ab0a604a.tar.gz fietsboek-1f9e1d0517266418ea5a61f5c9948783ab0a604a.tar.bz2 fietsboek-1f9e1d0517266418ea5a61f5c9948783ab0a604a.zip | |
fix lints
| -rw-r--r-- | fietsboek/actions.py | 10 | ||||
| -rw-r--r-- | fietsboek/geo.py | 46 | ||||
| -rw-r--r-- | fietsboek/models/climb.py | 32 | ||||
| -rw-r--r-- | fietsboek/osm.py | 51 | ||||
| -rw-r--r-- | fietsboek/trackmap.py | 21 | ||||
| -rw-r--r-- | fietsboek/views/detail.py | 9 |
6 files changed, 125 insertions, 44 deletions
diff --git a/fietsboek/actions.py b/fietsboek/actions.py index 0c28268..43bbe82 100644 --- a/fietsboek/actions.py +++ b/fietsboek/actions.py @@ -311,6 +311,7 @@ def extract_climbs( :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() @@ -372,4 +373,11 @@ def extract_climbs_background(request: Request, track_id: int): Thread(name=f"climbs-{track_id}", target=_extract_climbs).start() -__all__ = ["add_track", "edit_images", "execute_transformers", "send_verification_token", "extract_climbs"] +__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 5513a67..e75bd11 100644 --- a/fietsboek/osm.py +++ b/fietsboek/osm.py @@ -1,6 +1,5 @@ """OSM related utilities for fietsboek.""" -import gzip import logging from urllib.parse import urljoin from xml.etree import ElementTree @@ -31,9 +30,9 @@ def find_name(path: Path) -> str: """ LOGGER.debug("Starting naming process") session = requests.Session() - session.headers = HEADERS + 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 @@ -64,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) @@ -94,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] @@ -128,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] @@ -151,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 @@ -163,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]) @@ -181,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: @@ -225,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)", @@ -239,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 @@ -256,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]) @@ -281,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 @@ -290,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/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 44000c3..91778fd 100644 --- a/fietsboek/views/detail.py +++ b/fietsboek/views/detail.py @@ -196,9 +196,14 @@ def delete_track(request): @view_config(route_name="reclimb-track", request_method="POST", permission="track.edit") -def reclimb_track(request): +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=track_id)) + return HTTPFound(request.route_url("details", track_id=request.context.id)) @view_config(route_name="badge", http_cache=3600) |
