geo-activity-playground 0.38.2__py3-none-any.whl → 0.39.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geo_activity_playground/__main__.py +5 -47
- geo_activity_playground/alembic/README +1 -0
- geo_activity_playground/alembic/env.py +76 -0
- geo_activity_playground/alembic/script.py.mako +26 -0
- geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +33 -0
- geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +73 -0
- geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +28 -0
- geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +30 -0
- geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +28 -0
- geo_activity_playground/alembic/versions/script.py.mako +28 -0
- geo_activity_playground/core/activities.py +50 -136
- geo_activity_playground/core/config.py +3 -3
- geo_activity_playground/core/datamodel.py +257 -0
- geo_activity_playground/core/enrichment.py +90 -92
- geo_activity_playground/core/heart_rate.py +1 -2
- geo_activity_playground/core/paths.py +6 -7
- geo_activity_playground/core/raster_map.py +43 -4
- geo_activity_playground/core/similarity.py +1 -2
- geo_activity_playground/core/tasks.py +2 -2
- geo_activity_playground/core/test_meta_search.py +3 -3
- geo_activity_playground/core/test_summary_stats.py +1 -1
- geo_activity_playground/explorer/grid_file.py +2 -2
- geo_activity_playground/explorer/tile_visits.py +8 -10
- geo_activity_playground/heatmap_video.py +7 -8
- geo_activity_playground/importers/activity_parsers.py +2 -2
- geo_activity_playground/importers/directory.py +9 -10
- geo_activity_playground/importers/strava_api.py +9 -9
- geo_activity_playground/importers/strava_checkout.py +12 -13
- geo_activity_playground/importers/test_csv_parser.py +3 -3
- geo_activity_playground/importers/test_directory.py +1 -1
- geo_activity_playground/importers/test_strava_api.py +1 -1
- geo_activity_playground/webui/app.py +94 -86
- geo_activity_playground/webui/authenticator.py +1 -1
- geo_activity_playground/webui/{activity/controller.py → blueprints/activity_blueprint.py} +246 -108
- geo_activity_playground/webui/{auth_blueprint.py → blueprints/auth_blueprint.py} +1 -1
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +61 -0
- geo_activity_playground/webui/{calendar/controller.py → blueprints/calendar_blueprint.py} +19 -19
- geo_activity_playground/webui/{eddington_blueprint.py → blueprints/eddington_blueprint.py} +5 -5
- geo_activity_playground/webui/blueprints/entry_views.py +68 -0
- geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +37 -4
- geo_activity_playground/webui/{explorer/controller.py → blueprints/explorer_blueprint.py} +88 -54
- geo_activity_playground/webui/blueprints/heatmap_blueprint.py +233 -0
- geo_activity_playground/webui/{search_blueprint.py → blueprints/search_blueprint.py} +7 -11
- geo_activity_playground/webui/blueprints/settings_blueprint.py +446 -0
- geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py} +31 -6
- geo_activity_playground/webui/{summary_blueprint.py → blueprints/summary_blueprint.py} +11 -23
- geo_activity_playground/webui/blueprints/tile_blueprint.py +27 -0
- geo_activity_playground/webui/{upload_blueprint.py → blueprints/upload_blueprint.py} +13 -18
- geo_activity_playground/webui/flasher.py +26 -0
- geo_activity_playground/webui/plot_util.py +1 -1
- geo_activity_playground/webui/search_util.py +4 -6
- geo_activity_playground/webui/static/images/layers-2x.png +0 -0
- geo_activity_playground/webui/static/images/layers.png +0 -0
- geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
- geo_activity_playground/webui/static/images/marker-icon.png +0 -0
- geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
- geo_activity_playground/webui/templates/activity/day.html.j2 +81 -0
- geo_activity_playground/webui/templates/activity/edit.html.j2 +38 -0
- geo_activity_playground/webui/{activity/templates → templates}/activity/name.html.j2 +29 -27
- geo_activity_playground/webui/{activity/templates → templates}/activity/show.html.j2 +57 -33
- geo_activity_playground/webui/templates/activity/trim.html.j2 +68 -0
- geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +26 -0
- geo_activity_playground/webui/templates/calendar/index.html.j2 +48 -0
- geo_activity_playground/webui/templates/calendar/month.html.j2 +57 -0
- geo_activity_playground/webui/templates/equipment/index.html.j2 +7 -0
- geo_activity_playground/webui/templates/home.html.j2 +6 -6
- geo_activity_playground/webui/templates/page.html.j2 +2 -1
- geo_activity_playground/webui/{settings/templates → templates}/settings/index.html.j2 +9 -20
- geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +49 -0
- geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +48 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
- geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
- geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/METADATA +5 -1
- geo_activity_playground-0.39.0.dist-info/RECORD +133 -0
- geo_activity_playground/__init__.py +0 -0
- geo_activity_playground/core/__init__.py +0 -0
- geo_activity_playground/explorer/__init__.py +0 -0
- geo_activity_playground/importers/__init__.py +0 -0
- geo_activity_playground/webui/__init__.py +0 -0
- geo_activity_playground/webui/activity/__init__.py +0 -0
- geo_activity_playground/webui/activity/blueprint.py +0 -109
- geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
- geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
- geo_activity_playground/webui/calendar/__init__.py +0 -0
- geo_activity_playground/webui/calendar/blueprint.py +0 -23
- geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
- geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
- geo_activity_playground/webui/entry_controller.py +0 -63
- geo_activity_playground/webui/explorer/__init__.py +0 -0
- geo_activity_playground/webui/explorer/blueprint.py +0 -62
- geo_activity_playground/webui/heatmap/__init__.py +0 -0
- geo_activity_playground/webui/heatmap/blueprint.py +0 -51
- geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
- geo_activity_playground/webui/settings/blueprint.py +0 -262
- geo_activity_playground/webui/settings/controller.py +0 -272
- geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
- geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
- geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
- geo_activity_playground/webui/tile_blueprint.py +0 -42
- geo_activity_playground-0.38.2.dist-info/RECORD +0 -129
- /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
- /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
- /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/metadata-extraction.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/segmentation.html.j2 +0 -0
- /geo_activity_playground/webui/{settings/templates → templates}/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/entry_points.txt +0 -0
@@ -1,80 +0,0 @@
|
|
1
|
-
{% extends "page.html.j2" %}
|
2
|
-
|
3
|
-
{% block container %}
|
4
|
-
<div class="row mb-3">
|
5
|
-
<div class="col">
|
6
|
-
<h1>{{ date }}</h1>
|
7
|
-
</div>
|
8
|
-
</div>
|
9
|
-
|
10
|
-
|
11
|
-
<div class="row mb-3">
|
12
|
-
<div class="col-12">
|
13
|
-
<div id="activity-map" style="height: 500px;"></div>
|
14
|
-
<script>
|
15
|
-
var map = L.map('activity-map', {
|
16
|
-
fullscreenControl: true
|
17
|
-
});
|
18
|
-
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
19
|
-
maxZoom: 19,
|
20
|
-
attribution: '{{ map_tile_attribution|safe }}'
|
21
|
-
}).addTo(map);
|
22
|
-
|
23
|
-
let geojson = L.geoJSON({{ geojson| safe }}, {
|
24
|
-
style: function (feature) { return { color: feature.properties.color } }
|
25
|
-
}).addTo(map)
|
26
|
-
map.fitBounds(geojson.getBounds());
|
27
|
-
</script>
|
28
|
-
</div>
|
29
|
-
</div>
|
30
|
-
|
31
|
-
<div class="row mb-3">
|
32
|
-
<div class="col">
|
33
|
-
<h2>Activities</h2>
|
34
|
-
|
35
|
-
<table class="table table-sort table-arrows">
|
36
|
-
<thead>
|
37
|
-
<tr>
|
38
|
-
<th>Name</th>
|
39
|
-
<th>Date</th>
|
40
|
-
<th>Distance / km</th>
|
41
|
-
<th>Elapsed time</th>
|
42
|
-
<th>Speed / km/h</th>
|
43
|
-
<th>Equipment</th>
|
44
|
-
<th>Kind</th>
|
45
|
-
</tr>
|
46
|
-
</thead>
|
47
|
-
<tbody>
|
48
|
-
{% for activity in activities %}
|
49
|
-
<tr>
|
50
|
-
<td><span style="color: {{ activity['color'] }};">█</span> <a
|
51
|
-
href="{{ url_for('activity.show', id=activity.id) }}">{{
|
52
|
-
activity.name }}</a></td>
|
53
|
-
<td>{{ activity.start|dt }}</td>
|
54
|
-
<td>{{ activity.distance_km | round(1) }}</td>
|
55
|
-
<td>{{ activity.elapsed_time|td }}</td>
|
56
|
-
<td>{{ activity.average_speed_moving_kmh|round(1) }}</td>
|
57
|
-
<td>{{ activity["equipment"] }}</td>
|
58
|
-
<td>{{ activity["kind"] }}</td>
|
59
|
-
</tr>
|
60
|
-
{% endfor %}
|
61
|
-
{% if activities|length > 1 %}
|
62
|
-
<tr>
|
63
|
-
<td><b>Total</b></td>
|
64
|
-
<td></td>
|
65
|
-
<td><b>{{ total_distance | round(1) }}</b></td>
|
66
|
-
<td><b>{{ total_elapsed_time|td }}</b></td>
|
67
|
-
<td></td>
|
68
|
-
<td></td>
|
69
|
-
</tr>
|
70
|
-
{% endif %}
|
71
|
-
</tbody>
|
72
|
-
</table>
|
73
|
-
</div>
|
74
|
-
</div>
|
75
|
-
|
76
|
-
<h2>Share picture</h2>
|
77
|
-
|
78
|
-
<p><img class="img-fluid" src="{{ url_for('.day_sharepic', year=year, month=month, day=day) }}" /></p>
|
79
|
-
|
80
|
-
{% endblock %}
|
@@ -1,42 +0,0 @@
|
|
1
|
-
{% extends "page.html.j2" %}
|
2
|
-
|
3
|
-
{% block container %}
|
4
|
-
|
5
|
-
<h1 class="mb-3">Edit Activity</h1>
|
6
|
-
|
7
|
-
<form method="POST">
|
8
|
-
<div class="mb-3">
|
9
|
-
<label for="name" class="form-label">Name</label>
|
10
|
-
<input type="text" class="form-control" id="name" name="name" value="{{ override['name'] }}" />
|
11
|
-
</div>
|
12
|
-
|
13
|
-
<div class="mb-3">
|
14
|
-
<label for="kind" class="form-label">Kind</label>
|
15
|
-
<input type="text" class="form-control" id="kind" name="kind" value="{{ override['kind'] }}" />
|
16
|
-
</div>
|
17
|
-
|
18
|
-
<div class="mb-3">
|
19
|
-
<label for="equipment" class="form-label">Equipment</label>
|
20
|
-
<input type="text" class="form-control" id="equipment" name="equipment" value="{{ override['equipment'] }}" />
|
21
|
-
</div>
|
22
|
-
|
23
|
-
<div class="mb-3">
|
24
|
-
<div class="form-check">
|
25
|
-
<input type="checkbox" class="form-check-input" id="commute" name="commute" {% if override['commute'] %}
|
26
|
-
checked {% endif %} />
|
27
|
-
<label for="commute" class="form-check-label">Commute</label>
|
28
|
-
</div>
|
29
|
-
</div>
|
30
|
-
|
31
|
-
<div class="mb-3">
|
32
|
-
<div class="form-check">
|
33
|
-
<input type="checkbox" class="form-check-input" id="consider_for_achievements"
|
34
|
-
name="consider_for_achievements" {% if override['consider_for_achievements'] %} checked {% endif %} />
|
35
|
-
<label for="consider_for_achievements" class="form-check-label">Consider for achievements</label>
|
36
|
-
</div>
|
37
|
-
</div>
|
38
|
-
|
39
|
-
<button type="submit" class="btn btn-primary">Save</button>
|
40
|
-
</form>
|
41
|
-
|
42
|
-
{% endblock %}
|
File without changes
|
@@ -1,23 +0,0 @@
|
|
1
|
-
from flask import Blueprint
|
2
|
-
from flask import render_template
|
3
|
-
|
4
|
-
from geo_activity_playground.webui.calendar.controller import CalendarController
|
5
|
-
|
6
|
-
|
7
|
-
def make_calendar_blueprint(calendar_controller: CalendarController) -> Blueprint:
|
8
|
-
blueprint = Blueprint("calendar", __name__, template_folder="templates")
|
9
|
-
|
10
|
-
@blueprint.route("/")
|
11
|
-
def index():
|
12
|
-
return render_template(
|
13
|
-
"calendar/index.html.j2", **calendar_controller.render_overview()
|
14
|
-
)
|
15
|
-
|
16
|
-
@blueprint.route("/<year>/<month>")
|
17
|
-
def month(year: str, month: str):
|
18
|
-
return render_template(
|
19
|
-
"calendar/month.html.j2",
|
20
|
-
**calendar_controller.render_month(int(year), int(month))
|
21
|
-
)
|
22
|
-
|
23
|
-
return blueprint
|
@@ -1,46 +0,0 @@
|
|
1
|
-
{% extends "page.html.j2" %}
|
2
|
-
|
3
|
-
{% block container %}
|
4
|
-
<div class="row mb-1">
|
5
|
-
<div class="col">
|
6
|
-
<h1>Calendar</h1>
|
7
|
-
</div>
|
8
|
-
</div>
|
9
|
-
|
10
|
-
<div class="row mb-1">
|
11
|
-
<div class="col">
|
12
|
-
<table class="table">
|
13
|
-
<thead>
|
14
|
-
<tr>
|
15
|
-
<th>Year</th>
|
16
|
-
{% for i in range(1, 13) %}
|
17
|
-
<th style="text-align: right;">{{ i }}</th>
|
18
|
-
{% endfor %}
|
19
|
-
<th style="text-align: right;">Total</th>
|
20
|
-
</tr>
|
21
|
-
</thead>
|
22
|
-
<tbody>
|
23
|
-
{% for year, month_data in monthly_distances.items() %}
|
24
|
-
{% set year_int = year|round(0)|int %}
|
25
|
-
<tr>
|
26
|
-
<td>{{ year_int }}</td>
|
27
|
-
{% for month in range(1, 13) %}
|
28
|
-
<td align="right">
|
29
|
-
{% set distance = month_data[month] %}
|
30
|
-
{% if distance %}
|
31
|
-
<a href="{{ url_for(".month", year=year_int, month=month) }}">{{ distance|int() }} km</a>
|
32
|
-
{% else %}
|
33
|
-
0 km
|
34
|
-
{% endif %}
|
35
|
-
</td>
|
36
|
-
{% endfor %}
|
37
|
-
<td align="right">{{ yearly_distances[year]|int() }} km</td>
|
38
|
-
</tr>
|
39
|
-
{% endfor %}
|
40
|
-
</tbody>
|
41
|
-
</table>
|
42
|
-
</div>
|
43
|
-
</div>
|
44
|
-
|
45
|
-
|
46
|
-
{% endblock %}
|
@@ -1,55 +0,0 @@
|
|
1
|
-
{% extends "page.html.j2" %}
|
2
|
-
|
3
|
-
{% block container %}
|
4
|
-
<div class="row mb-1">
|
5
|
-
<div class="col">
|
6
|
-
<h1>Calendar {{ year }}-{{ "{0:02d}".format(month) }}</h1>
|
7
|
-
</div>
|
8
|
-
</div>
|
9
|
-
|
10
|
-
<div class="row mb-1">
|
11
|
-
<div class="col">
|
12
|
-
<table class="table">
|
13
|
-
<thead>
|
14
|
-
<tr>
|
15
|
-
<th>Week</th>
|
16
|
-
<th>Monday</th>
|
17
|
-
<th>Tuesday</th>
|
18
|
-
<th>Wednesday</th>
|
19
|
-
<th>Thursday</th>
|
20
|
-
<th>Friday</th>
|
21
|
-
<th>Saturday</th>
|
22
|
-
<th>Sunday</th>
|
23
|
-
</tr>
|
24
|
-
</thead>
|
25
|
-
<tbody>
|
26
|
-
{% for week, week_data in weeks.items() %}
|
27
|
-
<tr>
|
28
|
-
<td>{{ week }}</td>
|
29
|
-
{% for day in range(1, 8) %}
|
30
|
-
<td>
|
31
|
-
{% if weeks[week][day] %}
|
32
|
-
<a href="{{ url_for('activity.day', year=year, month=month, day=day_of_month[week][day]) }}"><b>{{
|
33
|
-
day_of_month[week][day] }}.</b></a>
|
34
|
-
{% elif day_of_month[week][day] %}
|
35
|
-
<b>{{ day_of_month[week][day] }}.</b>
|
36
|
-
{% endif %}
|
37
|
-
|
38
|
-
{% if weeks[week][day] %}
|
39
|
-
<ul>
|
40
|
-
{% for activity in weeks[week][day] %}
|
41
|
-
<li><a href="{{ url_for('activity.show', id=activity.id) }}">{{ activity.name }}</a></li>
|
42
|
-
{% endfor %}
|
43
|
-
</ul>
|
44
|
-
{% endif %}
|
45
|
-
</td>
|
46
|
-
{% endfor %}
|
47
|
-
</tr>
|
48
|
-
{% endfor %}
|
49
|
-
</tbody>
|
50
|
-
</table>
|
51
|
-
</div>
|
52
|
-
</div>
|
53
|
-
|
54
|
-
|
55
|
-
{% endblock %}
|
@@ -1,63 +0,0 @@
|
|
1
|
-
import datetime
|
2
|
-
import itertools
|
3
|
-
|
4
|
-
import altair as alt
|
5
|
-
import pandas as pd
|
6
|
-
|
7
|
-
from geo_activity_playground.core.activities import ActivityRepository
|
8
|
-
from geo_activity_playground.core.activities import make_geojson_from_time_series
|
9
|
-
from geo_activity_playground.core.config import Config
|
10
|
-
from geo_activity_playground.webui.plot_util import make_kind_scale
|
11
|
-
|
12
|
-
|
13
|
-
class EntryController:
|
14
|
-
def __init__(self, repository: ActivityRepository, config: Config) -> None:
|
15
|
-
self._repository = repository
|
16
|
-
self._config = config
|
17
|
-
|
18
|
-
def render(self) -> dict:
|
19
|
-
result = {"latest_activities": []}
|
20
|
-
|
21
|
-
if len(self._repository):
|
22
|
-
kind_scale = make_kind_scale(self._repository.meta, self._config)
|
23
|
-
result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
|
24
|
-
self._repository.meta, kind_scale
|
25
|
-
)
|
26
|
-
|
27
|
-
for activity in itertools.islice(
|
28
|
-
self._repository.iter_activities(dropna=True), 15
|
29
|
-
):
|
30
|
-
time_series = self._repository.get_time_series(activity["id"])
|
31
|
-
result["latest_activities"].append(
|
32
|
-
{
|
33
|
-
"line_geojson": make_geojson_from_time_series(time_series),
|
34
|
-
"activity": activity,
|
35
|
-
}
|
36
|
-
)
|
37
|
-
return result
|
38
|
-
|
39
|
-
|
40
|
-
def distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
|
41
|
-
before_30_days = pd.to_datetime(
|
42
|
-
datetime.datetime.now() - datetime.timedelta(days=31)
|
43
|
-
)
|
44
|
-
return (
|
45
|
-
alt.Chart(
|
46
|
-
meta.loc[meta["start"] > before_30_days],
|
47
|
-
width=700,
|
48
|
-
height=200,
|
49
|
-
title="Distance per day",
|
50
|
-
)
|
51
|
-
.mark_bar()
|
52
|
-
.encode(
|
53
|
-
alt.X("yearmonthdate(start)", title="Date"),
|
54
|
-
alt.Y("sum(distance_km)", title="Distance / km"),
|
55
|
-
alt.Color("kind", scale=kind_scale, title="Kind"),
|
56
|
-
[
|
57
|
-
alt.Tooltip("yearmonthdate(start)", title="Date"),
|
58
|
-
alt.Tooltip("kind", title="Kind"),
|
59
|
-
alt.Tooltip("sum(distance_km)", format=".1f", title="Distance / km"),
|
60
|
-
],
|
61
|
-
)
|
62
|
-
.to_json(format="vega")
|
63
|
-
)
|
File without changes
|
@@ -1,62 +0,0 @@
|
|
1
|
-
from flask import Blueprint
|
2
|
-
from flask import redirect
|
3
|
-
from flask import render_template
|
4
|
-
from flask import Response
|
5
|
-
from flask import url_for
|
6
|
-
|
7
|
-
from geo_activity_playground.webui.authenticator import Authenticator
|
8
|
-
from geo_activity_playground.webui.authenticator import needs_authentication
|
9
|
-
from geo_activity_playground.webui.explorer.controller import ExplorerController
|
10
|
-
|
11
|
-
|
12
|
-
def make_explorer_blueprint(
|
13
|
-
explorer_controller: ExplorerController,
|
14
|
-
authenticator: Authenticator,
|
15
|
-
) -> Blueprint:
|
16
|
-
blueprint = Blueprint("explorer", __name__, template_folder="templates")
|
17
|
-
|
18
|
-
@blueprint.route("/<zoom>")
|
19
|
-
def map(zoom: str):
|
20
|
-
return render_template(
|
21
|
-
"explorer/index.html.j2", **explorer_controller.render(int(zoom))
|
22
|
-
)
|
23
|
-
|
24
|
-
@blueprint.route("/enable-zoom-level/<zoom>")
|
25
|
-
@needs_authentication(authenticator)
|
26
|
-
def enable_zoom_level(zoom: str):
|
27
|
-
explorer_controller.enable_zoom_level(int(zoom))
|
28
|
-
return redirect(url_for(".map", zoom=zoom))
|
29
|
-
|
30
|
-
@blueprint.route("/<zoom>/<north>/<east>/<south>/<west>/explored.<suffix>")
|
31
|
-
def download(zoom: str, north: str, east: str, south: str, west: str, suffix: str):
|
32
|
-
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
33
|
-
return Response(
|
34
|
-
explorer_controller.export_explored_tiles(
|
35
|
-
int(zoom),
|
36
|
-
float(north),
|
37
|
-
float(east),
|
38
|
-
float(south),
|
39
|
-
float(west),
|
40
|
-
suffix,
|
41
|
-
),
|
42
|
-
mimetype=mimetypes[suffix],
|
43
|
-
headers={"Content-disposition": "attachment"},
|
44
|
-
)
|
45
|
-
|
46
|
-
@blueprint.route("/<zoom>/<north>/<east>/<south>/<west>/missing.<suffix>")
|
47
|
-
def missing(zoom: str, north: str, east: str, south: str, west: str, suffix: str):
|
48
|
-
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
49
|
-
return Response(
|
50
|
-
explorer_controller.export_missing_tiles(
|
51
|
-
int(zoom),
|
52
|
-
float(north),
|
53
|
-
float(east),
|
54
|
-
float(south),
|
55
|
-
float(west),
|
56
|
-
suffix,
|
57
|
-
),
|
58
|
-
mimetype=mimetypes[suffix],
|
59
|
-
headers={"Content-disposition": "attachment"},
|
60
|
-
)
|
61
|
-
|
62
|
-
return blueprint
|
File without changes
|
@@ -1,51 +0,0 @@
|
|
1
|
-
import dateutil.parser
|
2
|
-
from flask import Blueprint
|
3
|
-
from flask import render_template
|
4
|
-
from flask import request
|
5
|
-
from flask import Response
|
6
|
-
|
7
|
-
from geo_activity_playground.core.activities import ActivityRepository
|
8
|
-
from geo_activity_playground.core.config import Config
|
9
|
-
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
10
|
-
from geo_activity_playground.webui.heatmap.heatmap_controller import HeatmapController
|
11
|
-
from geo_activity_playground.webui.search_util import search_query_from_form
|
12
|
-
from geo_activity_playground.webui.search_util import SearchQueryHistory
|
13
|
-
|
14
|
-
|
15
|
-
def make_heatmap_blueprint(
|
16
|
-
repository: ActivityRepository,
|
17
|
-
tile_visit_accessor: TileVisitAccessor,
|
18
|
-
config: Config,
|
19
|
-
search_query_history: SearchQueryHistory,
|
20
|
-
) -> Blueprint:
|
21
|
-
heatmap_controller = HeatmapController(repository, tile_visit_accessor, config)
|
22
|
-
blueprint = Blueprint("heatmap", __name__, template_folder="templates")
|
23
|
-
|
24
|
-
@blueprint.route("/")
|
25
|
-
def index():
|
26
|
-
query = search_query_from_form(request.args)
|
27
|
-
search_query_history.register_query(query)
|
28
|
-
return render_template(
|
29
|
-
"heatmap/index.html.j2", **heatmap_controller.render(query)
|
30
|
-
)
|
31
|
-
|
32
|
-
@blueprint.route("/tile/<int:z>/<int:x>/<int:y>.png")
|
33
|
-
def tile(x: int, y: int, z: int):
|
34
|
-
query = search_query_from_form(request.args)
|
35
|
-
return Response(
|
36
|
-
heatmap_controller.render_tile(x, y, z, query),
|
37
|
-
mimetype="image/png",
|
38
|
-
)
|
39
|
-
|
40
|
-
@blueprint.route(
|
41
|
-
"/download/<float:north>/<float:east>/<float:south>/<float:west>/heatmap.png"
|
42
|
-
)
|
43
|
-
def download(north: float, east: float, south: float, west: float):
|
44
|
-
query = search_query_from_form(request.args)
|
45
|
-
return Response(
|
46
|
-
heatmap_controller.download_heatmap(north, east, south, west, query),
|
47
|
-
mimetype="image/png",
|
48
|
-
headers={"Content-disposition": 'attachment; filename="heatmap.png"'},
|
49
|
-
)
|
50
|
-
|
51
|
-
return blueprint
|
@@ -1,216 +0,0 @@
|
|
1
|
-
import datetime
|
2
|
-
import io
|
3
|
-
import logging
|
4
|
-
import pathlib
|
5
|
-
from typing import Optional
|
6
|
-
|
7
|
-
import matplotlib.pylab as pl
|
8
|
-
import numpy as np
|
9
|
-
from PIL import Image
|
10
|
-
from PIL import ImageDraw
|
11
|
-
|
12
|
-
from geo_activity_playground.core.activities import ActivityRepository
|
13
|
-
from geo_activity_playground.core.config import Config
|
14
|
-
from geo_activity_playground.core.meta_search import apply_search_query
|
15
|
-
from geo_activity_playground.core.meta_search import SearchQuery
|
16
|
-
from geo_activity_playground.core.raster_map import convert_to_grayscale
|
17
|
-
from geo_activity_playground.core.raster_map import GeoBounds
|
18
|
-
from geo_activity_playground.core.raster_map import get_sensible_zoom_level
|
19
|
-
from geo_activity_playground.core.raster_map import get_tile
|
20
|
-
from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
|
21
|
-
from geo_activity_playground.core.raster_map import PixelBounds
|
22
|
-
from geo_activity_playground.core.tasks import work_tracker
|
23
|
-
from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
|
24
|
-
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
25
|
-
from geo_activity_playground.webui.explorer.controller import (
|
26
|
-
bounding_box_for_biggest_cluster,
|
27
|
-
)
|
28
|
-
|
29
|
-
|
30
|
-
logger = logging.getLogger(__name__)
|
31
|
-
|
32
|
-
|
33
|
-
class HeatmapController:
|
34
|
-
def __init__(
|
35
|
-
self,
|
36
|
-
repository: ActivityRepository,
|
37
|
-
tile_visit_accessor: TileVisitAccessor,
|
38
|
-
config: Config,
|
39
|
-
) -> None:
|
40
|
-
self._repository = repository
|
41
|
-
self._tile_visit_accessor = tile_visit_accessor
|
42
|
-
self._config = config
|
43
|
-
|
44
|
-
self.tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
|
45
|
-
self.tile_evolution_states = self._tile_visit_accessor.tile_state[
|
46
|
-
"evolution_state"
|
47
|
-
]
|
48
|
-
self.tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
|
49
|
-
self.activities_per_tile = self._tile_visit_accessor.tile_state[
|
50
|
-
"activities_per_tile"
|
51
|
-
]
|
52
|
-
|
53
|
-
def render(self, query: SearchQuery) -> dict:
|
54
|
-
zoom = 14
|
55
|
-
tiles = self.tile_histories[zoom]
|
56
|
-
medians = tiles.median(skipna=True)
|
57
|
-
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
58
|
-
medians["tile_x"], medians["tile_y"], zoom
|
59
|
-
)
|
60
|
-
cluster_state = self.tile_evolution_states[zoom]
|
61
|
-
|
62
|
-
values = {
|
63
|
-
"center": {
|
64
|
-
"latitude": median_lat,
|
65
|
-
"longitude": median_lon,
|
66
|
-
"bbox": (
|
67
|
-
bounding_box_for_biggest_cluster(
|
68
|
-
cluster_state.clusters.values(), zoom
|
69
|
-
)
|
70
|
-
if len(cluster_state.memberships) > 0
|
71
|
-
else {}
|
72
|
-
),
|
73
|
-
},
|
74
|
-
"extra_args": query.to_url_str(),
|
75
|
-
"query": query.to_jinja(),
|
76
|
-
}
|
77
|
-
|
78
|
-
return values
|
79
|
-
|
80
|
-
def _get_counts(
|
81
|
-
self,
|
82
|
-
x: int,
|
83
|
-
y: int,
|
84
|
-
z: int,
|
85
|
-
query: SearchQuery,
|
86
|
-
) -> np.ndarray:
|
87
|
-
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
88
|
-
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
89
|
-
if not query.active:
|
90
|
-
tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{z}/{x}/{y}.npy")
|
91
|
-
if tile_count_cache_path.exists():
|
92
|
-
try:
|
93
|
-
tile_counts = np.load(tile_count_cache_path)
|
94
|
-
except ValueError:
|
95
|
-
logger.warning(
|
96
|
-
f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
|
97
|
-
)
|
98
|
-
tile_count_cache_path.unlink()
|
99
|
-
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
100
|
-
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
101
|
-
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
102
|
-
|
103
|
-
with work_tracker(
|
104
|
-
tile_count_cache_path.with_suffix(".json")
|
105
|
-
) as parsed_activities:
|
106
|
-
if parsed_activities - activity_ids:
|
107
|
-
logger.warning(
|
108
|
-
f"Resetting heatmap cache for {x=}/{y=}/{z=} because activities have been removed."
|
109
|
-
)
|
110
|
-
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
111
|
-
parsed_activities.clear()
|
112
|
-
for activity_id in activity_ids:
|
113
|
-
if activity_id in parsed_activities:
|
114
|
-
continue
|
115
|
-
parsed_activities.add(activity_id)
|
116
|
-
time_series = self._repository.get_time_series(activity_id)
|
117
|
-
for _, group in time_series.groupby("segment_id"):
|
118
|
-
xy_pixels = (
|
119
|
-
np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
|
120
|
-
* OSM_TILE_SIZE
|
121
|
-
)
|
122
|
-
im = Image.new("L", tile_pixels)
|
123
|
-
draw = ImageDraw.Draw(im)
|
124
|
-
pixels = list(map(int, xy_pixels.flatten()))
|
125
|
-
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
126
|
-
aim = np.array(im)
|
127
|
-
tile_counts += aim
|
128
|
-
tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
|
129
|
-
np.save(tmp_path, tile_counts)
|
130
|
-
tile_count_cache_path.unlink(missing_ok=True)
|
131
|
-
tmp_path.rename(tile_count_cache_path)
|
132
|
-
else:
|
133
|
-
activities = apply_search_query(self._repository.meta, query)
|
134
|
-
activity_ids = self.activities_per_tile[z].get((x, y), set())
|
135
|
-
for activity_id in activity_ids:
|
136
|
-
if activity_id not in activities["id"]:
|
137
|
-
continue
|
138
|
-
time_series = self._repository.get_time_series(activity_id)
|
139
|
-
for _, group in time_series.groupby("segment_id"):
|
140
|
-
xy_pixels = (
|
141
|
-
np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
|
142
|
-
* OSM_TILE_SIZE
|
143
|
-
)
|
144
|
-
im = Image.new("L", tile_pixels)
|
145
|
-
draw = ImageDraw.Draw(im)
|
146
|
-
pixels = list(map(int, xy_pixels.flatten()))
|
147
|
-
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
148
|
-
aim = np.array(im)
|
149
|
-
tile_counts += aim
|
150
|
-
return tile_counts
|
151
|
-
|
152
|
-
def _render_tile_image(
|
153
|
-
self,
|
154
|
-
x: int,
|
155
|
-
y: int,
|
156
|
-
z: int,
|
157
|
-
query: SearchQuery,
|
158
|
-
) -> np.ndarray:
|
159
|
-
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
160
|
-
tile_counts = np.zeros(tile_pixels)
|
161
|
-
tile_counts += self._get_counts(x, y, z, query)
|
162
|
-
|
163
|
-
tile_counts = np.sqrt(tile_counts) / 5
|
164
|
-
tile_counts[tile_counts > 1.0] = 1.0
|
165
|
-
|
166
|
-
cmap = pl.get_cmap(self._config.color_scheme_for_heatmap)
|
167
|
-
data_color = cmap(tile_counts)
|
168
|
-
data_color[data_color == cmap(0.0)] = 0.0 # remove background color
|
169
|
-
|
170
|
-
map_tile = np.array(get_tile(z, x, y, self._config.map_tile_url)) / 255
|
171
|
-
map_tile = convert_to_grayscale(map_tile)
|
172
|
-
map_tile = 1.0 - map_tile # invert colors
|
173
|
-
for c in range(3):
|
174
|
-
map_tile[:, :, c] = (1.0 - data_color[:, :, c]) * map_tile[
|
175
|
-
:, :, c
|
176
|
-
] + data_color[:, :, c]
|
177
|
-
return map_tile
|
178
|
-
|
179
|
-
def render_tile(self, x: int, y: int, z: int, query: SearchQuery) -> bytes:
|
180
|
-
f = io.BytesIO()
|
181
|
-
pl.imsave(
|
182
|
-
f,
|
183
|
-
self._render_tile_image(x, y, z, query),
|
184
|
-
format="png",
|
185
|
-
)
|
186
|
-
return bytes(f.getbuffer())
|
187
|
-
|
188
|
-
def download_heatmap(
|
189
|
-
self, north: float, east: float, south: float, west: float, query: SearchQuery
|
190
|
-
) -> bytes:
|
191
|
-
geo_bounds = GeoBounds(south, west, north, east)
|
192
|
-
tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
|
193
|
-
pixel_bounds = PixelBounds.from_tile_bounds(tile_bounds)
|
194
|
-
|
195
|
-
background = np.zeros((*pixel_bounds.shape, 3))
|
196
|
-
for x in range(tile_bounds.x1, tile_bounds.x2):
|
197
|
-
for y in range(tile_bounds.y1, tile_bounds.y2):
|
198
|
-
tile = (
|
199
|
-
np.array(
|
200
|
-
get_tile(tile_bounds.zoom, x, y, self._config.map_tile_url)
|
201
|
-
)
|
202
|
-
/ 255
|
203
|
-
)
|
204
|
-
|
205
|
-
i = y - tile_bounds.y1
|
206
|
-
j = x - tile_bounds.x1
|
207
|
-
|
208
|
-
background[
|
209
|
-
i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
|
210
|
-
j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
|
211
|
-
:,
|
212
|
-
] = self._render_tile_image(x, y, tile_bounds.zoom, query)
|
213
|
-
|
214
|
-
f = io.BytesIO()
|
215
|
-
pl.imsave(f, background, format="png")
|
216
|
-
return bytes(f.getbuffer())
|