aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2026-09-30 18:43:24 +0200
committerDaniel Schadt <kingdread@gmx.de>2026-09-30 18:43:24 +0200
commit1f9e1d0517266418ea5a61f5c9948783ab0a604a (patch)
treeea52c1b8d601dc217dd140dcc1c89f237ad13286
parent8f5bd60f507b695ccdc881fdda59e60d121606eb (diff)
downloadfietsboek-1f9e1d0517266418ea5a61f5c9948783ab0a604a.tar.gz
fietsboek-1f9e1d0517266418ea5a61f5c9948783ab0a604a.tar.bz2
fietsboek-1f9e1d0517266418ea5a61f5c9948783ab0a604a.zip
fix lints
-rw-r--r--fietsboek/actions.py10
-rw-r--r--fietsboek/geo.py46
-rw-r--r--fietsboek/models/climb.py32
-rw-r--r--fietsboek/osm.py51
-rw-r--r--fietsboek/trackmap.py21
-rw-r--r--fietsboek/views/detail.py9
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)