diff options
-rw-r--r-- | fietsboek/data.py | 37 | ||||
-rw-r--r-- | fietsboek/routes.py | 5 | ||||
-rw-r--r-- | fietsboek/templates/layout.jinja2 | 6 | ||||
-rw-r--r-- | fietsboek/templates/profile.jinja2 | 40 | ||||
-rw-r--r-- | fietsboek/views/profile.py | 73 |
5 files changed, 160 insertions, 1 deletions
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 @@ <!-- Custom styles for this scaffold --> <link href="{{request.static_url('fietsboek:static/fonts.css')}}" rel="stylesheet"> <link href="{{request.static_url('fietsboek:static/theme.css')}}" rel="stylesheet"> + <!-- Pre-load leaflet CSS --> + <link href="{{request.static_url('fietsboek:static/GM_Utils/leaflet/leaflet.css')}}" rel="stylesheet"> <script> const FRIENDS_URL = {{ request.route_url('json-friends') | tojson }}; @@ -111,10 +113,14 @@ const Legende = false; ================================================== --> <!-- Placed at the end of the document so the pages load faster --> <script src="{{request.static_url('fietsboek:static/bootstrap.bundle.min.js')}}"></script> + <!-- Pre-load leaflet Javascript. This lets us use Leaflet on any page, without relying on GPXViewer to load it --> + <script src="{{request.static_url('fietsboek:static/GM_Utils/leaflet/leaflet.js')}}"> <!-- Our patch to the GPX viewer, load before the actual GPX viewer --> <script src="{{request.static_url('fietsboek:static/osm-monkeypatch.js')}}"></script> <!-- Jürgen Berkemeier's GPX viewer --> <script src="{{request.static_url('fietsboek:static/GM_Utils/GPX2GM.js')}}"></script> <script src="{{request.static_url('fietsboek:static/fietsboek.js')}}"></script> + {% block latescripts %} + {% endblock %} </body> </html> 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 %} <div class="container"> <h1>{{ user.name }}</h1> + + {% if heatmap_url or tilehunt_url %} + <div id="userMap" style="height: 600px; width: 100%;"></div> + {% endif %} + <table class="table table-hover table-sm"> <th scope="row">{{ _("page.details.length") }}</th> <td id="detailsLength">{{ (total_length / 1000) | round(2) | format_decimal }} km</td> @@ -34,3 +39,38 @@ </table> </div> {% endblock %} + +{% block latescripts %} +<script> + var map = L.map('userMap').setView([52.520008, 13.404954], 2); + + baseLayers = {}; + overlayLayers = {}; + + {% if heatmap_url %} + overlayLayers[{{ _("page.profile.heatmap") | tojson }}] = L.tileLayer({{ heatmap_url | tojson }}, { + maxZoom: 19, + }); + {% endif %} + {% if tilehunt_url %} + overlayLayers[{{ _("page.profile.tilehunt") | tojson }}] = L.tileLayer({{ tilehunt_url | tojson }}, { + maxZoom: 19, + }); + {% endif %} + + for (let layer of TILE_LAYERS) { + if (layer.type === "base") { + baseLayers[layer.name] = L.tileLayer(layer.url, { + maxZoom: layer.zoom, + attribution: layer.attribution, + }); + } else if (layer.type === "overlay") { + overlayLayers[layer.name] = L.tileLayer(layer.url, { + attribution: layer.attribution, + }); + } + } + + L.control.layers(baseLayers, overlayLayers).addTo(map); +</script> +{% 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"] |