From 7e2ed3206897632c59ce62e7e19852d04b4c256e Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Wed, 22 Mar 2023 20:02:18 +0100 Subject: show heatmap on profile So far, fietsboek does not generate them, but if you happen (by accident) to have hittekaart output a heatmap to the right location, the profile page will now show it. --- fietsboek/data.py | 37 +++++++++++++++++++ fietsboek/routes.py | 5 +++ fietsboek/templates/layout.jinja2 | 6 ++++ fietsboek/templates/profile.jinja2 | 40 +++++++++++++++++++++ fietsboek/views/profile.py | 73 +++++++++++++++++++++++++++++++++++++- 5 files changed, 160 insertions(+), 1 deletion(-) diff --git a/fietsboek/data.py b/fietsboek/data.py index 9f49c49..9be47da 100644 --- a/fietsboek/data.py +++ b/fietsboek/data.py @@ -54,6 +54,9 @@ class DataManager: def _track_data_dir(self, track_id): return self.data_dir / "tracks" / str(track_id) + def _user_data_dir(self, user_id): + return self.data_dir / "users" / str(user_id) + def maintenance_mode(self) -> Optional[str]: """Checks whether the maintenance mode is enabled. @@ -99,6 +102,18 @@ class DataManager: raise FileNotFoundError(f"The path {path} is not a directory") from None return TrackDataDir(track_id, path) + def open_user(self, user_id: int) -> "UserDataDir": + """Opens a user's data directory. + + :raises FileNotFoundError: If the user directory does not exist. + :param user_id: ID of the user. + :return: The manager that can be used to manage this user's data. + """ + path = self._user_data_dir(user_id) + if not path.is_dir(): + raise FileNotFoundError(f"The path {path} is not a directory") from None + return UserDataDir(user_id, path) + class TrackDataDir: """Manager for a single track's data. @@ -360,3 +375,25 @@ class TrackDataDir: self.journal.append(("delete_image", path, path.read_bytes())) path.unlink() + + +class UserDataDir: + """Manager for a single user's data.""" + + def __init__(self, user_id: int, path: Path): + self.user_id = user_id + self.path = path + + def heatmap_path(self) -> Path: + """Returns the path for the heatmap tile file. + + :return: The path of the heatmap SQLite databse. + """ + return self.path / "heatmap.sqlite" + + def tilehunt_path(self) -> Path: + """Returns the path for the tilehunt tile file. + + :return: The path of the tilehunt SQLite database. + """ + return self.path / "tilehunt.sqlite" diff --git a/fietsboek/routes.py b/fietsboek/routes.py index 5e05269..8f109d9 100644 --- a/fietsboek/routes.py +++ b/fietsboek/routes.py @@ -60,5 +60,10 @@ def includeme(config): config.add_route("json-friends", "/me/friends.json") config.add_route("profile", "/user/{user_id}", factory="fietsboek.models.User.factory") + config.add_route( + "user-tile", + "/user/{user_id}/tile/{map}/{z:\\d+}/{x:\\d+}/{y:\\d+}", + factory="fietsboek.models.User.factory", + ) config.add_route("tile-proxy", "/tile/{provider}/{z:\\d+}/{x:\\d+}/{y:\\d+}") diff --git a/fietsboek/templates/layout.jinja2 b/fietsboek/templates/layout.jinja2 index 8f322f6..19fb700 100644 --- a/fietsboek/templates/layout.jinja2 +++ b/fietsboek/templates/layout.jinja2 @@ -17,6 +17,8 @@ + + + + + {% block latescripts %} + {% endblock %} diff --git a/fietsboek/templates/profile.jinja2 b/fietsboek/templates/profile.jinja2 index da4d732..1363216 100644 --- a/fietsboek/templates/profile.jinja2 +++ b/fietsboek/templates/profile.jinja2 @@ -3,6 +3,11 @@ {% block content %}

{{ user.name }}

+ + {% if heatmap_url or tilehunt_url %} +
+ {% endif %} + @@ -34,3 +39,38 @@
{{ _("page.details.length") }} {{ (total_length / 1000) | round(2) | format_decimal }} km
{% endblock %} + +{% block latescripts %} + +{% endblock %} diff --git a/fietsboek/views/profile.py b/fietsboek/views/profile.py index 488ffb0..94133f0 100644 --- a/fietsboek/views/profile.py +++ b/fietsboek/views/profile.py @@ -1,9 +1,13 @@ import datetime +import sqlite3 +from pyramid.httpexceptions import HTTPNotFound from pyramid.request import Request +from pyramid.response import Response from pyramid.view import view_config from .. import models, util +from ..data import UserDataDir from ..models.track import TrackType, TrackWithMetadata @@ -47,6 +51,38 @@ def profile(request: Request) -> dict: total_moving_time = round_to_seconds(total_moving_time) total_stopped_time = round_to_seconds(total_stopped_time) + heatmap_url = None + tilehunt_url = None + try: + user_dir: UserDataDir = request.data_manager.open_user(request.context.id) + except FileNotFoundError: + pass + else: + if user_dir.heatmap_path().is_file(): + heatmap_url = request.route_url( + "user-tile", + user_id=request.context.id, + map="heatmap", + z="ZZZ", + x="XXX", + y="YYY", + ) + heatmap_url = ( + heatmap_url.replace("ZZZ", "{z}").replace("XXX", "{x}").replace("YYY", "{y}") + ) + if user_dir.tilehunt_path().is_file(): + tilehunt_url = request.route_url( + "user-tile", + user_id=request.context.id, + map="tilehunt", + z="ZZZ", + x="XXX", + y="YYY", + ) + tilehunt_url = ( + tilehunt_url.replace("ZZZ", "{z}").replace("XXX", "{x}").replace("YYY", "{y}") + ) + return { "user": request.context, "total_length": total_length, @@ -57,7 +93,42 @@ def profile(request: Request) -> dict: "max_speed": max_speed, "avg_speed": avg_speed, "mps_to_kph": util.mps_to_kph, + "heatmap_url": heatmap_url, + "tilehunt_url": tilehunt_url, } -__all__ = ["profile"] +@view_config(route_name="user-tile", request_method="GET") +def user_tile(request: Request) -> Response: + """Returns a single tile from the user's own overlay maps. + + :param request: The pyramid request. + :return: The response, with the tile content (or an error). + """ + try: + user_dir: UserDataDir = request.data_manager.open_user(request.context.id) + except FileNotFoundError: + return HTTPNotFound() + + paths = { + "heatmap": user_dir.heatmap_path(), + "tilehunt": user_dir.tilehunt_path(), + } + path = paths.get(request.matchdict["map"]) + if path is None: + return HTTPNotFound() + + connection = sqlite3.connect(path) + cursor = connection.cursor() + result = cursor.execute( + "SELECT data FROM tiles WHERE zoom = ? AND x = ? AND y = ?;", + (int(request.matchdict["z"]), int(request.matchdict["x"]), int(request.matchdict["y"])), + ) + result = result.fetchone() + if result is None: + return HTTPNotFound() + + return Response(result[0], content_type="image/png") + + +__all__ = ["profile", "user_tile"] -- cgit v1.2.3