aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--fietsboek/data.py37
-rw-r--r--fietsboek/routes.py5
-rw-r--r--fietsboek/templates/layout.jinja26
-rw-r--r--fietsboek/templates/profile.jinja240
-rw-r--r--fietsboek/views/profile.py73
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"]