geo-activity-playground 0.38.2__py3-none-any.whl → 0.39.1__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 +53 -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/parametric_plot.py +101 -0
- 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 +96 -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/blueprints/plot_builder_blueprint.py +43 -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/templates/plot_builder/index.html.j2 +44 -0
- 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.1.dist-info}/METADATA +5 -1
- geo_activity_playground-0.39.1.dist-info/RECORD +136 -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.1.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,68 @@
|
|
1
|
+
import collections
|
2
|
+
import datetime
|
3
|
+
|
4
|
+
import altair as alt
|
5
|
+
import flask
|
6
|
+
import pandas as pd
|
7
|
+
from flask import render_template
|
8
|
+
from flask import Response
|
9
|
+
|
10
|
+
from ...core.activities import ActivityRepository
|
11
|
+
from ...core.activities import make_geojson_from_time_series
|
12
|
+
from ...core.config import Config
|
13
|
+
from ..plot_util import make_kind_scale
|
14
|
+
|
15
|
+
|
16
|
+
def register_entry_views(
|
17
|
+
app: flask.Flask, repository: ActivityRepository, config: Config
|
18
|
+
) -> None:
|
19
|
+
@app.route("/")
|
20
|
+
def index() -> Response:
|
21
|
+
context = {"latest_activities": []}
|
22
|
+
|
23
|
+
if len(repository):
|
24
|
+
kind_scale = make_kind_scale(repository.meta, config)
|
25
|
+
context["distance_last_30_days_plot"] = _distance_last_30_days_meta_plot(
|
26
|
+
repository.meta, kind_scale
|
27
|
+
)
|
28
|
+
|
29
|
+
meta = repository.meta.copy()
|
30
|
+
meta["date"] = meta["start"].dt.date
|
31
|
+
|
32
|
+
context["latest_activities"] = collections.defaultdict(list)
|
33
|
+
for date, activity_meta in list(meta.groupby("date"))[:-30:-1]:
|
34
|
+
for index, activity in activity_meta.iterrows():
|
35
|
+
time_series = repository.get_time_series(activity["id"])
|
36
|
+
context["latest_activities"][date].append(
|
37
|
+
{
|
38
|
+
"activity": activity,
|
39
|
+
"line_geojson": make_geojson_from_time_series(time_series),
|
40
|
+
}
|
41
|
+
)
|
42
|
+
return render_template("home.html.j2", **context)
|
43
|
+
|
44
|
+
|
45
|
+
def _distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
|
46
|
+
before_30_days = pd.to_datetime(
|
47
|
+
datetime.datetime.now() - datetime.timedelta(days=31)
|
48
|
+
)
|
49
|
+
return (
|
50
|
+
alt.Chart(
|
51
|
+
meta.loc[meta["start"] > before_30_days],
|
52
|
+
width=700,
|
53
|
+
height=200,
|
54
|
+
title="Distance per day",
|
55
|
+
)
|
56
|
+
.mark_bar()
|
57
|
+
.encode(
|
58
|
+
alt.X("yearmonthdate(start)", title="Date"),
|
59
|
+
alt.Y("sum(distance_km)", title="Distance / km"),
|
60
|
+
alt.Color("kind", scale=kind_scale, title="Kind"),
|
61
|
+
[
|
62
|
+
alt.Tooltip("yearmonthdate(start)", title="Date"),
|
63
|
+
alt.Tooltip("kind", title="Kind"),
|
64
|
+
alt.Tooltip("sum(distance_km)", format=".1f", title="Distance / km"),
|
65
|
+
],
|
66
|
+
)
|
67
|
+
.to_json(format="vega")
|
68
|
+
)
|
@@ -3,10 +3,10 @@ import pandas as pd
|
|
3
3
|
from flask import Blueprint
|
4
4
|
from flask import render_template
|
5
5
|
|
6
|
-
from
|
7
|
-
from
|
8
|
-
from
|
9
|
-
from
|
6
|
+
from ...core.activities import ActivityRepository
|
7
|
+
from ...core.config import Config
|
8
|
+
from ...core.summary_stats import get_equipment_use_table
|
9
|
+
from ..plot_util import make_kind_scale
|
10
10
|
|
11
11
|
|
12
12
|
def make_equipment_blueprint(
|
@@ -20,6 +20,38 @@ def make_equipment_blueprint(
|
|
20
20
|
repository.meta, config.equipment_offsets
|
21
21
|
)
|
22
22
|
|
23
|
+
# Prepare data for the stacked area chart
|
24
|
+
activities = repository.meta
|
25
|
+
activities["month"] = (
|
26
|
+
activities["start"].dt.to_period("M").apply(lambda r: r.start_time)
|
27
|
+
)
|
28
|
+
monthly_data = (
|
29
|
+
activities.groupby(["month", "equipment"])
|
30
|
+
.agg(total_distance=("distance_km", "sum"))
|
31
|
+
.reset_index()
|
32
|
+
)
|
33
|
+
|
34
|
+
stacked_area_chart = (
|
35
|
+
alt.Chart(
|
36
|
+
monthly_data, height=300, width=1200, title="Monthly Equipment Usage"
|
37
|
+
)
|
38
|
+
.mark_area()
|
39
|
+
.encode(
|
40
|
+
x=alt.X("month:T", title="Month"),
|
41
|
+
y=alt.Y("total_distance:Q", title="Total Kilometers per Month"),
|
42
|
+
color=alt.Color("equipment:N", title="Equipment"),
|
43
|
+
tooltip=[
|
44
|
+
alt.Tooltip("month:T", title="Date"), # Add the date to the tooltip
|
45
|
+
alt.Tooltip("equipment:N", title="Equipment"),
|
46
|
+
alt.Tooltip(
|
47
|
+
"total_distance:Q", format=".0f", title="Total Distance"
|
48
|
+
),
|
49
|
+
],
|
50
|
+
)
|
51
|
+
.interactive()
|
52
|
+
.to_json(format="vega") # Specify format="vega"
|
53
|
+
)
|
54
|
+
|
23
55
|
equipment_variables = {}
|
24
56
|
for equipment in equipment_summary["equipment"]:
|
25
57
|
selection = repository.meta.loc[repository.meta["equipment"] == equipment]
|
@@ -115,6 +147,7 @@ def make_equipment_blueprint(
|
|
115
147
|
variables = {
|
116
148
|
"equipment_variables": equipment_variables,
|
117
149
|
"equipment_summary": equipment_summary.to_dict(orient="records"),
|
150
|
+
"stacked_area_chart": stacked_area_chart,
|
118
151
|
}
|
119
152
|
|
120
153
|
return render_template("equipment/index.html.j2", **variables)
|
@@ -1,62 +1,57 @@
|
|
1
1
|
import datetime
|
2
2
|
import itertools
|
3
|
-
import
|
3
|
+
import logging
|
4
4
|
|
5
5
|
import altair as alt
|
6
6
|
import geojson
|
7
7
|
import matplotlib
|
8
8
|
import numpy as np
|
9
9
|
import pandas as pd
|
10
|
+
from flask import Blueprint
|
10
11
|
from flask import flash
|
11
|
-
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from
|
21
|
-
from
|
22
|
-
from
|
23
|
-
from
|
24
|
-
from
|
25
|
-
from
|
26
|
-
from
|
27
|
-
|
12
|
+
from flask import redirect
|
13
|
+
from flask import render_template
|
14
|
+
from flask import Response
|
15
|
+
from flask import url_for
|
16
|
+
|
17
|
+
from ...core.activities import ActivityRepository
|
18
|
+
from ...core.config import ConfigAccessor
|
19
|
+
from ...core.coordinates import Bounds
|
20
|
+
from ...core.tiles import compute_tile
|
21
|
+
from ...core.tiles import get_tile_upper_left_lat_lon
|
22
|
+
from ...explorer.grid_file import get_border_tiles
|
23
|
+
from ...explorer.grid_file import make_explorer_rectangle
|
24
|
+
from ...explorer.grid_file import make_explorer_tile
|
25
|
+
from ...explorer.grid_file import make_grid_file_geojson
|
26
|
+
from ...explorer.grid_file import make_grid_file_gpx
|
27
|
+
from ...explorer.grid_file import make_grid_points
|
28
|
+
from ...explorer.tile_visits import compute_tile_evolution
|
29
|
+
from ...explorer.tile_visits import TileEvolutionState
|
30
|
+
from ...explorer.tile_visits import TileVisitAccessor
|
31
|
+
from ..authenticator import Authenticator
|
32
|
+
from ..authenticator import needs_authentication
|
28
33
|
|
29
34
|
alt.data_transformers.enable("vegafusion")
|
30
35
|
|
36
|
+
logger = logging.getLogger(__name__)
|
31
37
|
|
32
|
-
class ExplorerController:
|
33
|
-
def __init__(
|
34
|
-
self,
|
35
|
-
repository: ActivityRepository,
|
36
|
-
tile_visit_accessor: TileVisitAccessor,
|
37
|
-
config_accessor: ConfigAccessor,
|
38
|
-
) -> None:
|
39
|
-
self._repository = repository
|
40
|
-
self._tile_visit_accessor = tile_visit_accessor
|
41
|
-
self._config_accessor = config_accessor
|
42
38
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
if zoom not in self._config_accessor().explorer_zoom_levels:
|
39
|
+
def make_explorer_blueprint(
|
40
|
+
authenticator: Authenticator,
|
41
|
+
repository: ActivityRepository,
|
42
|
+
tile_visit_accessor: TileVisitAccessor,
|
43
|
+
config_accessor: ConfigAccessor,
|
44
|
+
) -> Blueprint:
|
45
|
+
blueprint = Blueprint("explorer", __name__, template_folder="templates")
|
46
|
+
|
47
|
+
@blueprint.route("/<int:zoom>")
|
48
|
+
def map(zoom: int):
|
49
|
+
if zoom not in config_accessor().explorer_zoom_levels:
|
55
50
|
return {"zoom_level_not_generated": zoom}
|
56
51
|
|
57
|
-
tile_evolution_states =
|
58
|
-
tile_visits =
|
59
|
-
tile_histories =
|
52
|
+
tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
|
53
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"]
|
54
|
+
tile_histories = tile_visit_accessor.tile_state["tile_history"]
|
60
55
|
|
61
56
|
medians = tile_histories[zoom].median()
|
62
57
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
@@ -64,10 +59,10 @@ class ExplorerController:
|
|
64
59
|
)
|
65
60
|
|
66
61
|
explored = get_three_color_tiles(
|
67
|
-
tile_visits[zoom],
|
62
|
+
tile_visits[zoom], repository, tile_evolution_states[zoom], zoom
|
68
63
|
)
|
69
64
|
|
70
|
-
|
65
|
+
context = {
|
71
66
|
"center": {
|
72
67
|
"latitude": median_lat,
|
73
68
|
"longitude": median_lon,
|
@@ -89,35 +84,74 @@ class ExplorerController:
|
|
89
84
|
),
|
90
85
|
"zoom": zoom,
|
91
86
|
}
|
87
|
+
return render_template("explorer/index.html.j2", **context)
|
88
|
+
|
89
|
+
@blueprint.route("/enable-zoom-level/<int:zoom>")
|
90
|
+
@needs_authentication(authenticator)
|
91
|
+
def enable_zoom_level(zoom: int):
|
92
|
+
if 0 <= zoom <= 19:
|
93
|
+
config_accessor().explorer_zoom_levels.append(zoom)
|
94
|
+
config_accessor().explorer_zoom_levels.sort()
|
95
|
+
config_accessor.save()
|
96
|
+
compute_tile_evolution(tile_visit_accessor, config_accessor())
|
97
|
+
flash(f"Enabled {zoom=} for explorer tiles.", category="success")
|
98
|
+
else:
|
99
|
+
flash(f"{zoom=} is not valid, must be between 0 and 19.", category="danger")
|
100
|
+
return redirect(url_for(".map", zoom=zoom))
|
92
101
|
|
93
|
-
|
102
|
+
@blueprint.route(
|
103
|
+
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/explored.<suffix>"
|
104
|
+
)
|
105
|
+
def download(
|
106
|
+
zoom: int, north: float, east: float, south: float, west: float, suffix: str
|
107
|
+
):
|
94
108
|
x1, y1 = compute_tile(north, west, zoom)
|
95
109
|
x2, y2 = compute_tile(south, east, zoom)
|
96
110
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
97
111
|
|
98
|
-
tile_histories =
|
112
|
+
tile_histories = tile_visit_accessor.tile_state["tile_history"]
|
99
113
|
tiles = tile_histories[zoom]
|
100
114
|
points = get_border_tiles(tiles, zoom, tile_bounds)
|
101
115
|
if suffix == "geojson":
|
102
|
-
|
116
|
+
result = make_grid_file_geojson(points)
|
103
117
|
elif suffix == "gpx":
|
104
|
-
|
118
|
+
result = make_grid_file_gpx(points)
|
119
|
+
|
120
|
+
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
121
|
+
return Response(
|
122
|
+
result,
|
123
|
+
mimetype=mimetypes[suffix],
|
124
|
+
headers={"Content-disposition": "attachment"},
|
125
|
+
)
|
105
126
|
|
106
|
-
|
127
|
+
@blueprint.route(
|
128
|
+
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/missing.<suffix>"
|
129
|
+
)
|
130
|
+
def missing(
|
131
|
+
zoom: int, north: float, east: float, south: float, west: float, suffix: str
|
132
|
+
):
|
107
133
|
x1, y1 = compute_tile(north, west, zoom)
|
108
134
|
x2, y2 = compute_tile(south, east, zoom)
|
109
135
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
110
136
|
|
111
|
-
tile_visits =
|
137
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"]
|
112
138
|
tiles = tile_visits[zoom]
|
113
139
|
points = make_grid_points(
|
114
140
|
(tile for tile in tiles.keys() if tile_bounds.contains(*tile)), zoom
|
115
141
|
)
|
116
142
|
if suffix == "geojson":
|
117
|
-
|
143
|
+
result = make_grid_file_geojson(points)
|
118
144
|
elif suffix == "gpx":
|
119
|
-
|
120
|
-
|
145
|
+
result = make_grid_file_gpx(points)
|
146
|
+
|
147
|
+
mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
|
148
|
+
return Response(
|
149
|
+
result,
|
150
|
+
mimetype=mimetypes[suffix],
|
151
|
+
headers={"Content-disposition": "attachment"},
|
152
|
+
)
|
153
|
+
|
154
|
+
return blueprint
|
121
155
|
|
122
156
|
|
123
157
|
def get_three_color_tiles(
|
@@ -0,0 +1,233 @@
|
|
1
|
+
import io
|
2
|
+
import logging
|
3
|
+
import pathlib
|
4
|
+
|
5
|
+
import matplotlib.pylab as pl
|
6
|
+
import numpy as np
|
7
|
+
from flask import Blueprint
|
8
|
+
from flask import render_template
|
9
|
+
from flask import request
|
10
|
+
from flask import Response
|
11
|
+
from PIL import Image
|
12
|
+
from PIL import ImageDraw
|
13
|
+
|
14
|
+
from ...core.activities import ActivityRepository
|
15
|
+
from ...core.config import Config
|
16
|
+
from ...core.meta_search import apply_search_query
|
17
|
+
from ...core.meta_search import SearchQuery
|
18
|
+
from ...core.raster_map import convert_to_grayscale
|
19
|
+
from ...core.raster_map import GeoBounds
|
20
|
+
from ...core.raster_map import get_sensible_zoom_level
|
21
|
+
from ...core.raster_map import get_tile
|
22
|
+
from ...core.raster_map import OSM_TILE_SIZE
|
23
|
+
from ...core.raster_map import PixelBounds
|
24
|
+
from ...core.tasks import work_tracker
|
25
|
+
from ...core.tiles import get_tile_upper_left_lat_lon
|
26
|
+
from ...explorer.tile_visits import TileVisitAccessor
|
27
|
+
from ..search_util import search_query_from_form
|
28
|
+
from ..search_util import SearchQueryHistory
|
29
|
+
from .explorer_blueprint import bounding_box_for_biggest_cluster
|
30
|
+
|
31
|
+
logger = logging.getLogger(__name__)
|
32
|
+
|
33
|
+
|
34
|
+
def make_heatmap_blueprint(
|
35
|
+
repository: ActivityRepository,
|
36
|
+
tile_visit_accessor: TileVisitAccessor,
|
37
|
+
config: Config,
|
38
|
+
search_query_history: SearchQueryHistory,
|
39
|
+
) -> Blueprint:
|
40
|
+
blueprint = Blueprint("heatmap", __name__, template_folder="templates")
|
41
|
+
|
42
|
+
tile_histories = tile_visit_accessor.tile_state["tile_history"]
|
43
|
+
tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
|
44
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"]
|
45
|
+
activities_per_tile = tile_visit_accessor.tile_state["activities_per_tile"]
|
46
|
+
|
47
|
+
@blueprint.route("/")
|
48
|
+
def index():
|
49
|
+
query = search_query_from_form(request.args)
|
50
|
+
search_query_history.register_query(query)
|
51
|
+
|
52
|
+
zoom = 14
|
53
|
+
tiles = tile_histories[zoom]
|
54
|
+
medians = tiles.median(skipna=True)
|
55
|
+
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
56
|
+
medians["tile_x"], medians["tile_y"], zoom
|
57
|
+
)
|
58
|
+
cluster_state = tile_evolution_states[zoom]
|
59
|
+
|
60
|
+
context = {
|
61
|
+
"center": {
|
62
|
+
"latitude": median_lat,
|
63
|
+
"longitude": median_lon,
|
64
|
+
"bbox": (
|
65
|
+
bounding_box_for_biggest_cluster(
|
66
|
+
cluster_state.clusters.values(), zoom
|
67
|
+
)
|
68
|
+
if len(cluster_state.memberships) > 0
|
69
|
+
else {}
|
70
|
+
),
|
71
|
+
},
|
72
|
+
"extra_args": query.to_url_str(),
|
73
|
+
"query": query.to_jinja(),
|
74
|
+
}
|
75
|
+
|
76
|
+
return render_template("heatmap/index.html.j2", **context)
|
77
|
+
|
78
|
+
@blueprint.route("/tile/<int:z>/<int:x>/<int:y>.png")
|
79
|
+
def tile(x: int, y: int, z: int):
|
80
|
+
query = search_query_from_form(request.args)
|
81
|
+
f = io.BytesIO()
|
82
|
+
pl.imsave(
|
83
|
+
f,
|
84
|
+
_render_tile_image(x, y, z, query, config, repository, activities_per_tile),
|
85
|
+
format="png",
|
86
|
+
)
|
87
|
+
return Response(
|
88
|
+
bytes(f.getbuffer()),
|
89
|
+
mimetype="image/png",
|
90
|
+
)
|
91
|
+
|
92
|
+
@blueprint.route(
|
93
|
+
"/download/<float:north>/<float:east>/<float:south>/<float:west>/heatmap.png"
|
94
|
+
)
|
95
|
+
def download(north: float, east: float, south: float, west: float):
|
96
|
+
query = search_query_from_form(request.args)
|
97
|
+
geo_bounds = GeoBounds(south, west, north, east)
|
98
|
+
tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
|
99
|
+
pixel_bounds = PixelBounds.from_tile_bounds(tile_bounds)
|
100
|
+
|
101
|
+
background = np.zeros((*pixel_bounds.shape, 3))
|
102
|
+
for x in range(tile_bounds.x1, tile_bounds.x2):
|
103
|
+
for y in range(tile_bounds.y1, tile_bounds.y2):
|
104
|
+
i = y - tile_bounds.y1
|
105
|
+
j = x - tile_bounds.x1
|
106
|
+
|
107
|
+
background[
|
108
|
+
i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
|
109
|
+
j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
|
110
|
+
:,
|
111
|
+
] = _render_tile_image(
|
112
|
+
x,
|
113
|
+
y,
|
114
|
+
tile_bounds.zoom,
|
115
|
+
query,
|
116
|
+
config,
|
117
|
+
repository,
|
118
|
+
activities_per_tile,
|
119
|
+
)
|
120
|
+
|
121
|
+
f = io.BytesIO()
|
122
|
+
pl.imsave(f, background, format="png")
|
123
|
+
return Response(
|
124
|
+
bytes(f.getbuffer()),
|
125
|
+
mimetype="image/png",
|
126
|
+
headers={"Content-disposition": 'attachment; filename="heatmap.png"'},
|
127
|
+
)
|
128
|
+
|
129
|
+
return blueprint
|
130
|
+
|
131
|
+
|
132
|
+
def _get_counts(
|
133
|
+
x: int,
|
134
|
+
y: int,
|
135
|
+
z: int,
|
136
|
+
query: SearchQuery,
|
137
|
+
repository: ActivityRepository,
|
138
|
+
activities_per_tile: dict[int, dict[tuple[int, int], set[int]]],
|
139
|
+
) -> np.ndarray:
|
140
|
+
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
141
|
+
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
142
|
+
if not query.active:
|
143
|
+
tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{z}/{x}/{y}.npy")
|
144
|
+
if tile_count_cache_path.exists():
|
145
|
+
try:
|
146
|
+
tile_counts = np.load(tile_count_cache_path)
|
147
|
+
except ValueError:
|
148
|
+
logger.warning(
|
149
|
+
f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
|
150
|
+
)
|
151
|
+
tile_count_cache_path.unlink()
|
152
|
+
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
153
|
+
tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
|
154
|
+
activity_ids = activities_per_tile[z].get((x, y), set())
|
155
|
+
|
156
|
+
with work_tracker(
|
157
|
+
tile_count_cache_path.with_suffix(".json")
|
158
|
+
) as parsed_activities:
|
159
|
+
if parsed_activities - activity_ids:
|
160
|
+
logger.warning(
|
161
|
+
f"Resetting heatmap cache for {x=}/{y=}/{z=} because activities have been removed."
|
162
|
+
)
|
163
|
+
tile_counts = np.zeros(tile_pixels, dtype=np.int32)
|
164
|
+
parsed_activities.clear()
|
165
|
+
for activity_id in activity_ids:
|
166
|
+
if activity_id in parsed_activities:
|
167
|
+
continue
|
168
|
+
parsed_activities.add(activity_id)
|
169
|
+
time_series = repository.get_time_series(activity_id)
|
170
|
+
for _, group in time_series.groupby("segment_id"):
|
171
|
+
xy_pixels = (
|
172
|
+
np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
|
173
|
+
* OSM_TILE_SIZE
|
174
|
+
)
|
175
|
+
im = Image.new("L", tile_pixels)
|
176
|
+
draw = ImageDraw.Draw(im)
|
177
|
+
pixels = list(map(int, xy_pixels.flatten()))
|
178
|
+
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
179
|
+
aim = np.array(im)
|
180
|
+
tile_counts += aim
|
181
|
+
tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
|
182
|
+
np.save(tmp_path, tile_counts)
|
183
|
+
tile_count_cache_path.unlink(missing_ok=True)
|
184
|
+
tmp_path.rename(tile_count_cache_path)
|
185
|
+
else:
|
186
|
+
activities = apply_search_query(repository.meta, query)
|
187
|
+
activity_ids = activities_per_tile[z].get((x, y), set())
|
188
|
+
for activity_id in activity_ids:
|
189
|
+
if activity_id not in activities["id"]:
|
190
|
+
continue
|
191
|
+
time_series = repository.get_time_series(activity_id)
|
192
|
+
for _, group in time_series.groupby("segment_id"):
|
193
|
+
xy_pixels = (
|
194
|
+
np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
|
195
|
+
* OSM_TILE_SIZE
|
196
|
+
)
|
197
|
+
im = Image.new("L", tile_pixels)
|
198
|
+
draw = ImageDraw.Draw(im)
|
199
|
+
pixels = list(map(int, xy_pixels.flatten()))
|
200
|
+
draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
|
201
|
+
aim = np.array(im)
|
202
|
+
tile_counts += aim
|
203
|
+
return tile_counts
|
204
|
+
|
205
|
+
|
206
|
+
def _render_tile_image(
|
207
|
+
x: int,
|
208
|
+
y: int,
|
209
|
+
z: int,
|
210
|
+
query: SearchQuery,
|
211
|
+
config: Config,
|
212
|
+
repository: ActivityRepository,
|
213
|
+
activities_per_tile: dict[int, dict[tuple[int, int], set[int]]],
|
214
|
+
) -> np.ndarray:
|
215
|
+
tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
|
216
|
+
tile_counts = np.zeros(tile_pixels)
|
217
|
+
tile_counts += _get_counts(x, y, z, query, repository, activities_per_tile)
|
218
|
+
|
219
|
+
tile_counts = np.sqrt(tile_counts) / 5
|
220
|
+
tile_counts[tile_counts > 1.0] = 1.0
|
221
|
+
|
222
|
+
cmap = pl.get_cmap(config.color_scheme_for_heatmap)
|
223
|
+
data_color = cmap(tile_counts)
|
224
|
+
data_color[data_color == cmap(0.0)] = 0.0 # remove background color
|
225
|
+
|
226
|
+
map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
|
227
|
+
map_tile = convert_to_grayscale(map_tile)
|
228
|
+
map_tile = 1.0 - map_tile # invert colors
|
229
|
+
for c in range(3):
|
230
|
+
map_tile[:, :, c] = (1.0 - data_color[:, :, c]) * map_tile[
|
231
|
+
:, :, c
|
232
|
+
] + data_color[:, :, c]
|
233
|
+
return map_tile
|
@@ -0,0 +1,43 @@
|
|
1
|
+
from flask import Blueprint
|
2
|
+
from flask import render_template
|
3
|
+
from flask import request
|
4
|
+
from flask import Response
|
5
|
+
|
6
|
+
from ...core.activities import ActivityRepository
|
7
|
+
from ...core.parametric_plot import ALL_VARIABLES
|
8
|
+
from ...core.parametric_plot import CONTINUOUS_VARIABLES
|
9
|
+
from ...core.parametric_plot import DISCRETE_VARIABLES
|
10
|
+
from ...core.parametric_plot import make_parametric_plot
|
11
|
+
from ...core.parametric_plot import MARKS
|
12
|
+
from ...core.parametric_plot import ParametricPlotSpec
|
13
|
+
|
14
|
+
|
15
|
+
def make_plot_builder_blueprint(repository: ActivityRepository) -> Blueprint:
|
16
|
+
blueprint = Blueprint("plot_builder", __name__, template_folder="templates")
|
17
|
+
|
18
|
+
@blueprint.route("/")
|
19
|
+
def index() -> Response:
|
20
|
+
context = {}
|
21
|
+
if request.args:
|
22
|
+
spec = ParametricPlotSpec(
|
23
|
+
mark=request.args["mark"],
|
24
|
+
x=request.args["x"],
|
25
|
+
y=request.args["y"],
|
26
|
+
color=request.args.get("color", None),
|
27
|
+
shape=request.args.get("shape", None),
|
28
|
+
size=request.args.get("size", None),
|
29
|
+
row=request.args.get("row", None),
|
30
|
+
column=request.args.get("column", None),
|
31
|
+
)
|
32
|
+
plot = make_parametric_plot(repository.meta, spec)
|
33
|
+
context["plot"] = plot
|
34
|
+
return render_template(
|
35
|
+
"plot_builder/index.html.j2",
|
36
|
+
marks=MARKS,
|
37
|
+
continuous=ALL_VARIABLES,
|
38
|
+
discrete=DISCRETE_VARIABLES,
|
39
|
+
**context,
|
40
|
+
**request.args
|
41
|
+
)
|
42
|
+
|
43
|
+
return blueprint
|
@@ -1,22 +1,18 @@
|
|
1
1
|
import urllib.parse
|
2
2
|
from functools import reduce
|
3
3
|
|
4
|
-
import dateutil.parser
|
5
4
|
from flask import Blueprint
|
6
|
-
from flask import flash
|
7
5
|
from flask import redirect
|
8
6
|
from flask import render_template
|
9
7
|
from flask import request
|
10
|
-
from flask import Response
|
11
8
|
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from geo_activity_playground.webui.search_util import SearchQueryHistory
|
9
|
+
from ...core.activities import ActivityRepository
|
10
|
+
from ...core.config import ConfigAccessor
|
11
|
+
from ...core.meta_search import apply_search_query
|
12
|
+
from ..authenticator import Authenticator
|
13
|
+
from ..authenticator import needs_authentication
|
14
|
+
from ..search_util import search_query_from_form
|
15
|
+
from ..search_util import SearchQueryHistory
|
20
16
|
|
21
17
|
|
22
18
|
def reduce_or(selections):
|