geo-activity-playground 0.45.0__py3-none-any.whl → 1.1.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/alembic/versions/dc8073871da7_add_plotspec_group_by.py +28 -0
- geo_activity_playground/core/config.py +1 -0
- geo_activity_playground/core/datamodel.py +12 -0
- geo_activity_playground/core/export.py +129 -0
- geo_activity_playground/core/meta_search.py +1 -1
- geo_activity_playground/core/parametric_plot.py +101 -47
- geo_activity_playground/webui/app.py +10 -1
- geo_activity_playground/webui/authenticator.py +4 -2
- geo_activity_playground/webui/blueprints/activity_blueprint.py +11 -10
- geo_activity_playground/webui/blueprints/auth_blueprint.py +6 -2
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +2 -1
- geo_activity_playground/webui/blueprints/calendar_blueprint.py +3 -2
- geo_activity_playground/webui/blueprints/eddington_blueprints.py +3 -2
- geo_activity_playground/webui/blueprints/entry_views.py +11 -11
- geo_activity_playground/webui/blueprints/equipment_blueprint.py +2 -1
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +343 -197
- geo_activity_playground/webui/blueprints/export_blueprint.py +31 -0
- geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +79 -0
- geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +38 -19
- geo_activity_playground/webui/blueprints/summary_blueprint.py +114 -240
- geo_activity_playground/webui/blueprints/upload_blueprint.py +9 -0
- geo_activity_playground/webui/columns.py +40 -7
- geo_activity_playground/webui/static/{browserconfig.xml → favicons/browserconfig.xml} +1 -1
- geo_activity_playground/webui/static/{site.webmanifest → favicons/site.webmanifest} +2 -2
- geo_activity_playground/webui/static/server-side-explorer.js +60 -0
- geo_activity_playground/webui/templates/activity/name.html.j2 +4 -4
- geo_activity_playground/webui/templates/activity/show.html.j2 +8 -8
- geo_activity_playground/webui/templates/auth/index.html.j2 +1 -0
- geo_activity_playground/webui/templates/eddington/distance.html.j2 +3 -3
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +3 -3
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +3 -3
- geo_activity_playground/webui/templates/equipment/index.html.j2 +1 -1
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +42 -36
- geo_activity_playground/webui/templates/export/index.html.j2 +39 -0
- geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +58 -0
- geo_activity_playground/webui/templates/home.html.j2 +1 -4
- geo_activity_playground/webui/templates/page.html.j2 +26 -43
- geo_activity_playground/webui/templates/plot-macros.html.j2 +72 -0
- geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +12 -7
- geo_activity_playground/webui/templates/plot_builder/import-spec.html.j2 +24 -0
- geo_activity_playground/webui/templates/plot_builder/index.html.j2 +5 -0
- geo_activity_playground/webui/templates/summary/index.html.j2 +23 -230
- geo_activity_playground/webui/templates/summary/vega-chart.html.j2 +3 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/METADATA +2 -1
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/RECORD +74 -65
- geo_activity_playground/webui/templates/explorer/index.html.j2 +0 -148
- /geo_activity_playground/webui/static/{bootstrap-dark-mode.js → bootstrap/bootstrap-dark-mode.js} +0 -0
- /geo_activity_playground/webui/static/{bootstrap.bundle.min.js → bootstrap/bootstrap.bundle.min.js} +0 -0
- /geo_activity_playground/webui/static/{bootstrap.min.css → bootstrap/bootstrap.min.css} +0 -0
- /geo_activity_playground/webui/static/{android-chrome-192x192.png → favicons/android-chrome-192x192.png} +0 -0
- /geo_activity_playground/webui/static/{android-chrome-512x512.png → favicons/android-chrome-512x512.png} +0 -0
- /geo_activity_playground/webui/static/{apple-touch-icon.png → favicons/apple-touch-icon.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-16x16.png → favicons/favicon-16x16.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-32x32.png → favicons/favicon-32x32.png} +0 -0
- /geo_activity_playground/webui/static/{favicon-48x48.png → favicons/favicon-48x48.png} +0 -0
- /geo_activity_playground/webui/static/{favicon.ico → favicons/favicon.ico} +0 -0
- /geo_activity_playground/webui/static/{favicon.svg → favicons/favicon.svg} +0 -0
- /geo_activity_playground/webui/static/{mstile-150x150.png → favicons/mstile-150x150.png} +0 -0
- /geo_activity_playground/webui/static/{web-app-manifest-192x192.png → favicons/web-app-manifest-192x192.png} +0 -0
- /geo_activity_playground/webui/static/{web-app-manifest-512x512.png → favicons/web-app-manifest-512x512.png} +0 -0
- /geo_activity_playground/webui/static/{Leaflet.fullscreen.min.js → leaflet/Leaflet.fullscreen.min.js} +0 -0
- /geo_activity_playground/webui/static/{MarkerCluster.Default.css → leaflet/MarkerCluster.Default.css} +0 -0
- /geo_activity_playground/webui/static/{MarkerCluster.css → leaflet/MarkerCluster.css} +0 -0
- /geo_activity_playground/webui/static/{fullscreen.png → leaflet/fullscreen.png} +0 -0
- /geo_activity_playground/webui/static/{fullscreen@2x.png → leaflet/fullscreen@2x.png} +0 -0
- /geo_activity_playground/webui/static/{leaflet.css → leaflet/leaflet.css} +0 -0
- /geo_activity_playground/webui/static/{leaflet.fullscreen.css → leaflet/leaflet.fullscreen.css} +0 -0
- /geo_activity_playground/webui/static/{leaflet.js → leaflet/leaflet.js} +0 -0
- /geo_activity_playground/webui/static/{leaflet.markercluster.js → leaflet/leaflet.markercluster.js} +0 -0
- /geo_activity_playground/webui/static/{vega-embed@6 → vega/vega-embed@6.js} +0 -0
- /geo_activity_playground/webui/static/{vega-lite@4 → vega/vega-lite@4.js} +0 -0
- /geo_activity_playground/webui/static/{vega@5 → vega/vega@5.js} +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.45.0.dist-info → geo_activity_playground-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,11 @@
|
|
1
|
+
import abc
|
1
2
|
import datetime
|
3
|
+
import hashlib
|
2
4
|
import io
|
3
5
|
import itertools
|
4
6
|
import logging
|
7
|
+
from collections.abc import Iterable
|
8
|
+
from typing import Union
|
5
9
|
|
6
10
|
import altair as alt
|
7
11
|
import geojson
|
@@ -12,10 +16,13 @@ import pandas as pd
|
|
12
16
|
import sqlalchemy
|
13
17
|
from flask import Blueprint
|
14
18
|
from flask import flash
|
19
|
+
from flask import json
|
15
20
|
from flask import redirect
|
16
21
|
from flask import render_template
|
22
|
+
from flask import request
|
17
23
|
from flask import Response
|
18
24
|
from flask import url_for
|
25
|
+
from flask.typing import ResponseReturnValue
|
19
26
|
|
20
27
|
from ...core.activities import ActivityRepository
|
21
28
|
from ...core.config import ConfigAccessor
|
@@ -43,6 +50,105 @@ alt.data_transformers.enable("vegafusion")
|
|
43
50
|
logger = logging.getLogger(__name__)
|
44
51
|
|
45
52
|
|
53
|
+
def blend_color(
|
54
|
+
base: np.ndarray, addition: Union[np.ndarray, float], opacity: float
|
55
|
+
) -> np.ndarray:
|
56
|
+
return (1 - opacity) * base + opacity * addition
|
57
|
+
|
58
|
+
|
59
|
+
class ColorStrategy(abc.ABC):
|
60
|
+
@abc.abstractmethod
|
61
|
+
def color_image(
|
62
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
63
|
+
) -> np.ndarray:
|
64
|
+
pass
|
65
|
+
|
66
|
+
|
67
|
+
class MaxClusterColorStrategy(ColorStrategy):
|
68
|
+
def __init__(self, evolution_state, tile_visits):
|
69
|
+
self.evolution_state = evolution_state
|
70
|
+
self.tile_visits = tile_visits
|
71
|
+
self.max_cluster_members = max(
|
72
|
+
evolution_state.clusters.values(),
|
73
|
+
key=len,
|
74
|
+
)
|
75
|
+
|
76
|
+
def color_image(
|
77
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
78
|
+
) -> np.ndarray:
|
79
|
+
if tile_xy in self.max_cluster_members:
|
80
|
+
return blend_color(grayscale, np.array([[[55, 126, 184]]]) / 256, 0.3)
|
81
|
+
elif tile_xy in self.evolution_state.memberships:
|
82
|
+
return blend_color(grayscale, np.array([[[77, 175, 74]]]) / 256, 0.3)
|
83
|
+
elif tile_xy in self.tile_visits:
|
84
|
+
return blend_color(grayscale, 0.0, 0.3)
|
85
|
+
else:
|
86
|
+
return grayscale
|
87
|
+
|
88
|
+
|
89
|
+
class ColorfulClusterColorStrategy(ColorStrategy):
|
90
|
+
def __init__(self, evolution_state: TileEvolutionState, tile_visits):
|
91
|
+
self.evolution_state = evolution_state
|
92
|
+
self.tile_visits = tile_visits
|
93
|
+
self.max_cluster_members = max(
|
94
|
+
evolution_state.clusters.values(),
|
95
|
+
key=len,
|
96
|
+
)
|
97
|
+
self._cmap = matplotlib.colormaps["hsv"]
|
98
|
+
|
99
|
+
def color_image(
|
100
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
101
|
+
) -> np.ndarray:
|
102
|
+
if tile_xy in self.evolution_state.memberships:
|
103
|
+
cluster_id = self.evolution_state.memberships[tile_xy]
|
104
|
+
m = hashlib.sha256()
|
105
|
+
m.update(str(cluster_id).encode())
|
106
|
+
d = int(m.hexdigest(), base=16) / (256.0**m.digest_size)
|
107
|
+
return blend_color(grayscale, np.array([[self._cmap(d)[:3]]]), 0.3)
|
108
|
+
elif tile_xy in self.tile_visits:
|
109
|
+
return blend_color(grayscale, 0.0, 0.3)
|
110
|
+
else:
|
111
|
+
return grayscale
|
112
|
+
|
113
|
+
|
114
|
+
class VisitTimeColorStrategy(ColorStrategy):
|
115
|
+
def __init__(self, tile_visits, use_first=True):
|
116
|
+
self.tile_visits = tile_visits
|
117
|
+
self.use_first = use_first
|
118
|
+
|
119
|
+
def color_image(
|
120
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
121
|
+
) -> np.ndarray:
|
122
|
+
if tile_xy in self.tile_visits:
|
123
|
+
today = datetime.date.today()
|
124
|
+
cmap = matplotlib.colormaps["plasma"]
|
125
|
+
tile_info = self.tile_visits[tile_xy]
|
126
|
+
relevant_time = (
|
127
|
+
tile_info["first_time"] if self.use_first else tile_info["last_time"]
|
128
|
+
)
|
129
|
+
last_age_days = (today - relevant_time.date()).days
|
130
|
+
color = cmap(max(1 - last_age_days / (2 * 365), 0.0))
|
131
|
+
return blend_color(grayscale, np.array([[color[:3]]]), 0.3)
|
132
|
+
else:
|
133
|
+
return grayscale
|
134
|
+
|
135
|
+
|
136
|
+
class NumVisitsColorStrategy(ColorStrategy):
|
137
|
+
def __init__(self, tile_visits):
|
138
|
+
self.tile_visits = tile_visits
|
139
|
+
|
140
|
+
def color_image(
|
141
|
+
self, tile_xy: tuple[int, int], grayscale: np.ndarray
|
142
|
+
) -> np.ndarray:
|
143
|
+
if tile_xy in self.tile_visits:
|
144
|
+
cmap = matplotlib.colormaps["viridis"]
|
145
|
+
tile_info = self.tile_visits[tile_xy]
|
146
|
+
color = cmap(min(len(tile_info["activity_ids"]) / 50, 1.0))
|
147
|
+
return blend_color(grayscale, np.array([[color[:3]]]), 0.3)
|
148
|
+
else:
|
149
|
+
return grayscale
|
150
|
+
|
151
|
+
|
46
152
|
def make_explorer_blueprint(
|
47
153
|
authenticator: Authenticator,
|
48
154
|
tile_visit_accessor: TileVisitAccessor,
|
@@ -52,51 +158,9 @@ def make_explorer_blueprint(
|
|
52
158
|
) -> Blueprint:
|
53
159
|
blueprint = Blueprint("explorer", __name__, template_folder="templates")
|
54
160
|
|
55
|
-
@blueprint.route("/<int:zoom>")
|
56
|
-
def map(zoom: int):
|
57
|
-
if zoom not in config_accessor().explorer_zoom_levels:
|
58
|
-
return {"zoom_level_not_generated": zoom}
|
59
|
-
|
60
|
-
tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
|
61
|
-
tile_visits = tile_visit_accessor.tile_state["tile_visits"]
|
62
|
-
tile_histories = tile_visit_accessor.tile_state["tile_history"]
|
63
|
-
|
64
|
-
medians = tile_histories[zoom].median()
|
65
|
-
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
66
|
-
medians["tile_x"], medians["tile_y"], zoom
|
67
|
-
)
|
68
|
-
|
69
|
-
explored = get_three_color_tiles(
|
70
|
-
tile_visits[zoom], tile_evolution_states[zoom], zoom
|
71
|
-
)
|
72
|
-
|
73
|
-
context = {
|
74
|
-
"center": {
|
75
|
-
"latitude": median_lat,
|
76
|
-
"longitude": median_lon,
|
77
|
-
"bbox": (
|
78
|
-
bounding_box_for_biggest_cluster(
|
79
|
-
tile_evolution_states[zoom].clusters.values(), zoom
|
80
|
-
)
|
81
|
-
if len(tile_evolution_states[zoom].memberships) > 0
|
82
|
-
else {}
|
83
|
-
),
|
84
|
-
},
|
85
|
-
"explored": explored,
|
86
|
-
"plot_tile_evolution": plot_tile_evolution(tile_histories[zoom]),
|
87
|
-
"plot_cluster_evolution": plot_cluster_evolution(
|
88
|
-
tile_evolution_states[zoom].cluster_evolution
|
89
|
-
),
|
90
|
-
"plot_square_evolution": plot_square_evolution(
|
91
|
-
tile_evolution_states[zoom].square_evolution
|
92
|
-
),
|
93
|
-
"zoom": zoom,
|
94
|
-
}
|
95
|
-
return render_template("explorer/index.html.j2", **context)
|
96
|
-
|
97
161
|
@blueprint.route("/enable-zoom-level/<int:zoom>")
|
98
162
|
@needs_authentication(authenticator)
|
99
|
-
def enable_zoom_level(zoom: int):
|
163
|
+
def enable_zoom_level(zoom: int) -> ResponseReturnValue:
|
100
164
|
if 0 <= zoom <= 19:
|
101
165
|
config_accessor().explorer_zoom_levels.append(zoom)
|
102
166
|
config_accessor().explorer_zoom_levels.sort()
|
@@ -108,11 +172,11 @@ def make_explorer_blueprint(
|
|
108
172
|
return redirect(url_for(".map", zoom=zoom))
|
109
173
|
|
110
174
|
@blueprint.route(
|
111
|
-
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/
|
175
|
+
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/missing.<suffix>"
|
112
176
|
)
|
113
|
-
def
|
177
|
+
def download_missing(
|
114
178
|
zoom: int, north: float, east: float, south: float, west: float, suffix: str
|
115
|
-
):
|
179
|
+
) -> ResponseReturnValue:
|
116
180
|
x1, y1 = compute_tile(north, west, zoom)
|
117
181
|
x2, y2 = compute_tile(south, east, zoom)
|
118
182
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
@@ -133,11 +197,11 @@ def make_explorer_blueprint(
|
|
133
197
|
)
|
134
198
|
|
135
199
|
@blueprint.route(
|
136
|
-
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/
|
200
|
+
"/<int:zoom>/<float:north>/<float:east>/<float:south>/<float:west>/explored.<suffix>"
|
137
201
|
)
|
138
|
-
def
|
202
|
+
def download_explored(
|
139
203
|
zoom: int, north: float, east: float, south: float, west: float, suffix: str
|
140
|
-
):
|
204
|
+
) -> ResponseReturnValue:
|
141
205
|
x1, y1 = compute_tile(north, west, zoom)
|
142
206
|
x2, y2 = compute_tile(south, east, zoom)
|
143
207
|
tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
|
@@ -160,14 +224,14 @@ def make_explorer_blueprint(
|
|
160
224
|
)
|
161
225
|
|
162
226
|
@blueprint.route("/<int:zoom>/server-side")
|
163
|
-
def server_side(zoom: int):
|
227
|
+
def server_side(zoom: int) -> ResponseReturnValue:
|
164
228
|
if zoom not in config_accessor().explorer_zoom_levels:
|
165
229
|
return {"zoom_level_not_generated": zoom}
|
166
230
|
|
167
|
-
|
168
|
-
|
231
|
+
tile_evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
232
|
+
tile_history = tile_visit_accessor.tile_state["tile_history"][zoom]
|
169
233
|
|
170
|
-
medians =
|
234
|
+
medians = tile_history.median()
|
171
235
|
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
172
236
|
medians["tile_x"], medians["tile_y"], zoom
|
173
237
|
)
|
@@ -178,182 +242,264 @@ def make_explorer_blueprint(
|
|
178
242
|
"longitude": median_lon,
|
179
243
|
"bbox": (
|
180
244
|
bounding_box_for_biggest_cluster(
|
181
|
-
|
245
|
+
tile_evolution_state.clusters.values(), zoom
|
182
246
|
)
|
183
|
-
if len(
|
247
|
+
if len(tile_evolution_state.memberships) > 0
|
184
248
|
else {}
|
185
249
|
),
|
186
250
|
},
|
187
|
-
"plot_tile_evolution": plot_tile_evolution(
|
251
|
+
"plot_tile_evolution": plot_tile_evolution(tile_history),
|
188
252
|
"plot_cluster_evolution": plot_cluster_evolution(
|
189
|
-
|
253
|
+
tile_evolution_state.cluster_evolution
|
190
254
|
),
|
191
255
|
"plot_square_evolution": plot_square_evolution(
|
192
|
-
|
256
|
+
tile_evolution_state.square_evolution
|
193
257
|
),
|
194
258
|
"zoom": zoom,
|
259
|
+
"num_tiles": len(tile_history),
|
260
|
+
"num_cluster_tiles": len(tile_evolution_state.memberships),
|
261
|
+
"square_x": tile_evolution_state.square_x,
|
262
|
+
"square_y": tile_evolution_state.square_y,
|
263
|
+
"square_size": tile_evolution_state.max_square_size,
|
264
|
+
"max_cluster_size": max(map(len, tile_evolution_state.clusters.values())),
|
195
265
|
}
|
196
266
|
return render_template("explorer/server-side.html.j2", **context)
|
197
267
|
|
198
268
|
@blueprint.route("/<int:zoom>/tile/<int:z>/<int:x>/<int:y>.png")
|
199
|
-
def tile(zoom: int, z: int, x: int, y: int) ->
|
269
|
+
def tile(zoom: int, z: int, x: int, y: int) -> ResponseReturnValue:
|
200
270
|
tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
|
271
|
+
evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
201
272
|
|
202
273
|
map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
|
274
|
+
grayscale = image_transforms["grayscale"].transform_image(map_tile)
|
275
|
+
square_line_width = 3
|
276
|
+
square_color = np.array([[[228, 26, 28]]]) / 256
|
277
|
+
|
278
|
+
color_strategy_name = request.args.get("color_strategy", "colorful_cluster")
|
279
|
+
if color_strategy_name == "default":
|
280
|
+
color_strategy_name = config_accessor().cluster_color_strategy
|
281
|
+
match color_strategy_name:
|
282
|
+
case "max_cluster":
|
283
|
+
color_strategy = MaxClusterColorStrategy(evolution_state, tile_visits)
|
284
|
+
case "colorful_cluster":
|
285
|
+
color_strategy = ColorfulClusterColorStrategy(
|
286
|
+
evolution_state, tile_visits
|
287
|
+
)
|
288
|
+
case "first":
|
289
|
+
color_strategy = VisitTimeColorStrategy(tile_visits, use_first=True)
|
290
|
+
case "last":
|
291
|
+
color_strategy = VisitTimeColorStrategy(tile_visits, use_first=False)
|
292
|
+
case "visits":
|
293
|
+
color_strategy = NumVisitsColorStrategy(tile_visits)
|
294
|
+
case _:
|
295
|
+
raise ValueError("Unsupported color strategy.")
|
296
|
+
|
203
297
|
if z >= zoom:
|
204
298
|
factor = 2 ** (z - zoom)
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
299
|
+
tile_x = x // factor
|
300
|
+
tile_y = y // factor
|
301
|
+
tile_xy = (tile_x, tile_y)
|
302
|
+
result = color_strategy.color_image(tile_xy, grayscale)
|
303
|
+
|
304
|
+
if x % factor == 0:
|
305
|
+
result[:, 0, :] = 0.5
|
306
|
+
if y % factor == 0:
|
307
|
+
result[0, :, :] = 0.5
|
308
|
+
|
309
|
+
if (
|
310
|
+
evolution_state.square_x is not None
|
311
|
+
and evolution_state.square_y is not None
|
312
|
+
):
|
313
|
+
if (
|
314
|
+
x % factor == 0
|
315
|
+
and tile_x == evolution_state.square_x
|
316
|
+
and evolution_state.square_y
|
317
|
+
<= tile_y
|
318
|
+
< evolution_state.square_y + evolution_state.max_square_size
|
319
|
+
):
|
320
|
+
result[:, 0:square_line_width] = blend_color(
|
321
|
+
result[:, 0:square_line_width], square_color, 0.5
|
322
|
+
)
|
323
|
+
if (
|
324
|
+
y % factor == 0
|
325
|
+
and tile_y == evolution_state.square_y
|
326
|
+
and evolution_state.square_x
|
327
|
+
<= tile_x
|
328
|
+
< evolution_state.square_x + evolution_state.max_square_size
|
329
|
+
):
|
330
|
+
result[0:square_line_width, :] = blend_color(
|
331
|
+
result[0:square_line_width, :], square_color, 0.5
|
332
|
+
)
|
333
|
+
|
334
|
+
if (
|
335
|
+
(x + 1) % factor == 0
|
336
|
+
and (x + 1) // factor
|
337
|
+
== evolution_state.square_x + evolution_state.max_square_size
|
338
|
+
and evolution_state.square_y
|
339
|
+
<= tile_y
|
340
|
+
< evolution_state.square_y + evolution_state.max_square_size
|
341
|
+
):
|
342
|
+
result[:, -square_line_width:] = blend_color(
|
343
|
+
result[:, -square_line_width:], square_color, 0.5
|
344
|
+
)
|
345
|
+
if (
|
346
|
+
(y + 1) % factor == 0
|
347
|
+
and (y + 1) // factor
|
348
|
+
== evolution_state.square_y + evolution_state.max_square_size
|
349
|
+
and evolution_state.square_x
|
350
|
+
<= tile_x
|
351
|
+
< evolution_state.square_x + evolution_state.max_square_size
|
352
|
+
):
|
353
|
+
result[-square_line_width:, :] = blend_color(
|
354
|
+
result[-square_line_width:, :], square_color, 0.5
|
355
|
+
)
|
209
356
|
else:
|
210
|
-
|
357
|
+
result = grayscale
|
211
358
|
factor = 2 ** (zoom - z)
|
212
359
|
width = 256 // factor
|
213
360
|
for xo in range(factor):
|
214
361
|
for yo in range(factor):
|
215
|
-
|
216
|
-
|
217
|
-
|
362
|
+
tile_x = x * factor + xo
|
363
|
+
tile_y = y * factor + yo
|
364
|
+
tile_xy = (tile_x, tile_y)
|
365
|
+
if tile_xy in tile_visits:
|
366
|
+
result[
|
218
367
|
yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
|
219
|
-
] =
|
220
|
-
|
221
|
-
|
368
|
+
] = color_strategy.color_image(
|
369
|
+
tile_xy,
|
370
|
+
grayscale[
|
371
|
+
yo * width : (yo + 1) * width,
|
372
|
+
xo * width : (xo + 1) * width,
|
373
|
+
],
|
374
|
+
)
|
375
|
+
|
376
|
+
if (
|
377
|
+
evolution_state.square_x is not None
|
378
|
+
and evolution_state.square_y is not None
|
379
|
+
):
|
380
|
+
if (
|
381
|
+
tile_x == evolution_state.square_x
|
382
|
+
and evolution_state.square_y
|
383
|
+
<= tile_y
|
384
|
+
< evolution_state.square_y
|
385
|
+
+ evolution_state.max_square_size
|
386
|
+
):
|
387
|
+
result[
|
388
|
+
yo * width : (yo + 1) * width,
|
389
|
+
xo * width : xo * width + square_line_width,
|
390
|
+
] = blend_color(
|
391
|
+
result[
|
392
|
+
yo * width : (yo + 1) * width,
|
393
|
+
xo * width : xo * width + square_line_width,
|
394
|
+
],
|
395
|
+
square_color,
|
396
|
+
0.5,
|
397
|
+
)
|
398
|
+
if (
|
399
|
+
tile_y == evolution_state.square_y
|
400
|
+
and evolution_state.square_x
|
401
|
+
<= tile_x
|
402
|
+
< evolution_state.square_x
|
403
|
+
+ evolution_state.max_square_size
|
404
|
+
):
|
405
|
+
result[
|
406
|
+
yo * width : yo * width + square_line_width,
|
407
|
+
xo * width : (xo + 1) * width,
|
408
|
+
] = blend_color(
|
409
|
+
result[
|
410
|
+
yo * width : yo * width + square_line_width,
|
411
|
+
xo * width : (xo + 1) * width,
|
412
|
+
],
|
413
|
+
square_color,
|
414
|
+
0.5,
|
415
|
+
)
|
416
|
+
|
417
|
+
if (
|
418
|
+
tile_x + 1
|
419
|
+
== evolution_state.square_x
|
420
|
+
+ evolution_state.max_square_size
|
421
|
+
and evolution_state.square_y
|
422
|
+
<= tile_y
|
423
|
+
< evolution_state.square_y
|
424
|
+
+ evolution_state.max_square_size
|
425
|
+
):
|
426
|
+
result[
|
427
|
+
yo * width : (yo + 1) * width,
|
428
|
+
(xo + 1) * width
|
429
|
+
- square_line_width : (xo + 1) * width,
|
430
|
+
] = blend_color(
|
431
|
+
result[
|
432
|
+
yo * width : (yo + 1) * width,
|
433
|
+
(xo + 1) * width
|
434
|
+
- square_line_width : (xo + 1) * width,
|
435
|
+
],
|
436
|
+
square_color,
|
437
|
+
0.5,
|
438
|
+
)
|
439
|
+
|
440
|
+
if (
|
441
|
+
tile_y + 1
|
442
|
+
== evolution_state.square_y
|
443
|
+
+ evolution_state.max_square_size
|
444
|
+
and evolution_state.square_x
|
445
|
+
<= tile_x
|
446
|
+
< evolution_state.square_x
|
447
|
+
+ evolution_state.max_square_size
|
448
|
+
):
|
449
|
+
result[
|
450
|
+
(yo + 1) * width
|
451
|
+
- square_line_width : (yo + 1) * width,
|
452
|
+
xo * width : (xo + 1) * width,
|
453
|
+
] = blend_color(
|
454
|
+
result[
|
455
|
+
(yo + 1) * width
|
456
|
+
- square_line_width : (yo + 1) * width,
|
457
|
+
xo * width : (xo + 1) * width,
|
458
|
+
],
|
459
|
+
square_color,
|
460
|
+
0.5,
|
461
|
+
)
|
462
|
+
if width >= 64:
|
463
|
+
result[yo * width, :, :] = 0.5
|
464
|
+
result[:, xo * width, :] = 0.5
|
222
465
|
f = io.BytesIO()
|
223
|
-
pl.imsave(f,
|
466
|
+
pl.imsave(f, result, format="png")
|
224
467
|
return Response(bytes(f.getbuffer()), mimetype="image/png")
|
225
468
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
469
|
+
@blueprint.route("/<int:zoom>/info/<float:latitude>/<float:longitude>")
|
470
|
+
def info(zoom: int, latitude: float, longitude: float) -> dict:
|
471
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
|
472
|
+
evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
|
473
|
+
tile_xy = compute_tile(latitude, longitude, zoom)
|
474
|
+
if tile_xy in tile_visits:
|
475
|
+
tile_info = tile_visits[tile_xy]
|
476
|
+
first = DB.session.get_one(Activity, tile_info["first_id"])
|
477
|
+
last = DB.session.get_one(Activity, tile_info["last_id"])
|
478
|
+
result = {
|
479
|
+
"tile_xy": f"{tile_xy}",
|
480
|
+
"num_visits": len(tile_info["activity_ids"]),
|
481
|
+
"first_activity_id": first.id,
|
482
|
+
"first_activity_name": first.name,
|
483
|
+
"first_time": tile_info["first_time"].isoformat(),
|
484
|
+
"last_activity_id": last.id,
|
485
|
+
"last_activity_name": last.name,
|
486
|
+
"last_time": tile_info["last_time"].isoformat(),
|
487
|
+
"is_cluster": tile_xy in evolution_state.memberships,
|
488
|
+
"this_cluster_size": len(
|
489
|
+
evolution_state.clusters.get(
|
490
|
+
evolution_state.memberships.get(tile_xy, None), []
|
491
|
+
)
|
492
|
+
),
|
493
|
+
}
|
243
494
|
else:
|
244
|
-
|
245
|
-
|
246
|
-
tile_dict[tile] = {
|
247
|
-
"first_activity_id": str(tile_data["first_id"]),
|
248
|
-
"first_activity_name": DB.session.scalar(
|
249
|
-
sqlalchemy.select(Activity.name).where(
|
250
|
-
Activity.id == tile_data["first_id"]
|
251
|
-
)
|
252
|
-
),
|
253
|
-
"last_activity_id": str(tile_data["last_id"]),
|
254
|
-
"last_activity_name": DB.session.scalar(
|
255
|
-
sqlalchemy.select(Activity.name).where(
|
256
|
-
Activity.id == tile_data["last_id"]
|
257
|
-
)
|
258
|
-
),
|
259
|
-
"first_age_days": first_age_days,
|
260
|
-
"first_age_color": matplotlib.colors.to_hex(
|
261
|
-
cmap_first(max(1 - first_age_days / (2 * 365), 0.0))
|
262
|
-
),
|
263
|
-
"last_age_days": last_age_days,
|
264
|
-
"last_age_color": matplotlib.colors.to_hex(
|
265
|
-
cmap_last(max(1 - last_age_days / (2 * 365), 0.0))
|
266
|
-
),
|
267
|
-
"cluster": False,
|
268
|
-
"color": "#303030",
|
269
|
-
"first_visit": tile_data["first_time"].date().isoformat(),
|
270
|
-
"last_visit": tile_data["last_time"].date().isoformat(),
|
271
|
-
"num_visits": len(tile_data["activity_ids"]),
|
272
|
-
"square": False,
|
273
|
-
"tile": f"({zoom}, {tile[0]}, {tile[1]})",
|
274
|
-
}
|
495
|
+
result = {}
|
496
|
+
return result
|
275
497
|
|
276
|
-
|
277
|
-
if cluster_state.max_square_size:
|
278
|
-
for x in range(
|
279
|
-
cluster_state.square_x,
|
280
|
-
cluster_state.square_x + cluster_state.max_square_size,
|
281
|
-
):
|
282
|
-
for y in range(
|
283
|
-
cluster_state.square_y,
|
284
|
-
cluster_state.square_y + cluster_state.max_square_size,
|
285
|
-
):
|
286
|
-
tile_dict[(x, y)]["square"] = True
|
287
|
-
|
288
|
-
# Add cluster information.
|
289
|
-
for members in cluster_state.clusters.values():
|
290
|
-
for member in members:
|
291
|
-
tile_dict[member]["this_cluster_size"] = len(members)
|
292
|
-
tile_dict[member]["cluster"] = True
|
293
|
-
if len(cluster_state.cluster_evolution) > 0:
|
294
|
-
max_cluster_size = cluster_state.cluster_evolution["max_cluster_size"].iloc[-1]
|
295
|
-
else:
|
296
|
-
max_cluster_size = 0
|
297
|
-
num_cluster_tiles = len(cluster_state.memberships)
|
298
|
-
|
299
|
-
# Apply cluster colors.
|
300
|
-
cluster_cmap = matplotlib.colormaps["tab10"]
|
301
|
-
for color, members in zip(
|
302
|
-
itertools.cycle(map(cluster_cmap, [0, 1, 2, 3, 4, 5, 6, 8, 9])),
|
303
|
-
sorted(
|
304
|
-
cluster_state.clusters.values(),
|
305
|
-
key=lambda members: len(members),
|
306
|
-
reverse=True,
|
307
|
-
),
|
308
|
-
):
|
309
|
-
hex_color = matplotlib.colors.to_hex(color)
|
310
|
-
for member in members:
|
311
|
-
tile_dict[member]["color"] = hex_color
|
312
|
-
|
313
|
-
if cluster_state.max_square_size:
|
314
|
-
square_geojson = geojson.dumps(
|
315
|
-
geojson.FeatureCollection(
|
316
|
-
features=[
|
317
|
-
make_explorer_rectangle(
|
318
|
-
cluster_state.square_x,
|
319
|
-
cluster_state.square_y,
|
320
|
-
cluster_state.square_x + cluster_state.max_square_size,
|
321
|
-
cluster_state.square_y + cluster_state.max_square_size,
|
322
|
-
zoom,
|
323
|
-
)
|
324
|
-
]
|
325
|
-
)
|
326
|
-
)
|
327
|
-
else:
|
328
|
-
square_geojson = "{}"
|
329
|
-
|
330
|
-
try:
|
331
|
-
feature_collection = geojson.FeatureCollection(
|
332
|
-
features=[
|
333
|
-
make_explorer_tile(x, y, v, zoom) for (x, y), v in tile_dict.items()
|
334
|
-
]
|
335
|
-
)
|
336
|
-
explored_geojson = geojson.dumps(feature_collection)
|
337
|
-
except TypeError as e:
|
338
|
-
logger.error(f"Encountered TypeError while building GeoJSON: {e=}")
|
339
|
-
logger.error(f"{tile_dict = }")
|
340
|
-
raise
|
341
|
-
|
342
|
-
result = {
|
343
|
-
"explored_geojson": explored_geojson,
|
344
|
-
"max_cluster_size": max_cluster_size,
|
345
|
-
"num_cluster_tiles": num_cluster_tiles,
|
346
|
-
"num_tiles": len(tile_dict),
|
347
|
-
"square_size": cluster_state.max_square_size,
|
348
|
-
"square_x": cluster_state.square_x,
|
349
|
-
"square_y": cluster_state.square_y,
|
350
|
-
"square_geojson": square_geojson,
|
351
|
-
}
|
352
|
-
return result
|
498
|
+
return blueprint
|
353
499
|
|
354
500
|
|
355
501
|
def bounding_box_for_biggest_cluster(
|
356
|
-
clusters:
|
502
|
+
clusters: Iterable[list[tuple[int, int]]], zoom: int
|
357
503
|
) -> str:
|
358
504
|
biggest_cluster = max(clusters, key=lambda members: len(members))
|
359
505
|
min_x = min(x for x, y in biggest_cluster)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
from flask import Blueprint
|
2
|
+
from flask import render_template
|
3
|
+
from flask import request
|
4
|
+
from flask import Response
|
5
|
+
from flask.typing import ResponseReturnValue
|
6
|
+
|
7
|
+
from ...core.export import export_all
|
8
|
+
from ..authenticator import Authenticator
|
9
|
+
from ..authenticator import needs_authentication
|
10
|
+
|
11
|
+
|
12
|
+
def make_export_blueprint(authenticator: Authenticator) -> Blueprint:
|
13
|
+
blueprint = Blueprint("export", __name__, template_folder="templates")
|
14
|
+
|
15
|
+
@needs_authentication(authenticator)
|
16
|
+
@blueprint.route("/")
|
17
|
+
def index() -> str:
|
18
|
+
return render_template("export/index.html.j2")
|
19
|
+
|
20
|
+
@needs_authentication(authenticator)
|
21
|
+
@blueprint.route("/export")
|
22
|
+
def export() -> Response:
|
23
|
+
meta_format = request.args["meta_format"]
|
24
|
+
activity_format = request.args["activity_format"]
|
25
|
+
return Response(
|
26
|
+
bytes(export_all(meta_format, activity_format)),
|
27
|
+
mimetype="application/zip",
|
28
|
+
headers={"Content-disposition": 'attachment; filename="export.zip"'},
|
29
|
+
)
|
30
|
+
|
31
|
+
return blueprint
|