geo-activity-playground 0.40.0__py3-none-any.whl → 0.41.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/38882503dc7c_add_tags_to_activities.py +70 -0
- geo_activity_playground/alembic/versions/script.py.mako +0 -6
- geo_activity_playground/core/activities.py +17 -30
- geo_activity_playground/core/datamodel.py +83 -2
- geo_activity_playground/core/test_datamodel.py +14 -1
- geo_activity_playground/importers/strava_checkout.py +2 -5
- geo_activity_playground/webui/app.py +6 -2
- geo_activity_playground/webui/blueprints/activity_blueprint.py +20 -3
- geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +50 -25
- geo_activity_playground/webui/blueprints/calendar_blueprint.py +12 -4
- geo_activity_playground/webui/blueprints/eddington_blueprints.py +253 -0
- geo_activity_playground/webui/blueprints/entry_views.py +30 -15
- geo_activity_playground/webui/blueprints/explorer_blueprint.py +83 -9
- geo_activity_playground/webui/blueprints/summary_blueprint.py +102 -42
- geo_activity_playground/webui/columns.py +37 -0
- geo_activity_playground/webui/templates/activity/show.html.j2 +15 -4
- geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +24 -8
- geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +150 -0
- geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +150 -0
- geo_activity_playground/webui/templates/explorer/server-side.html.j2 +72 -0
- geo_activity_playground/webui/templates/home.html.j2 +14 -5
- geo_activity_playground/webui/templates/page.html.j2 +10 -1
- geo_activity_playground/webui/templates/summary/index.html.j2 +91 -2
- {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/RECORD +29 -24
- geo_activity_playground/webui/blueprints/eddington_blueprint.py +0 -194
- /geo_activity_playground/webui/templates/eddington/{index.html.j2 → distance.html.j2} +0 -0
- {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.40.0.dist-info → geo_activity_playground-0.41.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,253 @@
|
|
1
|
+
import datetime
|
2
|
+
from math import ceil
|
3
|
+
|
4
|
+
import altair as alt
|
5
|
+
import numpy as np
|
6
|
+
import pandas as pd
|
7
|
+
from flask import Blueprint
|
8
|
+
from flask import render_template
|
9
|
+
from flask import Request
|
10
|
+
from flask import request
|
11
|
+
|
12
|
+
from ...core.activities import ActivityRepository
|
13
|
+
from ...core.meta_search import apply_search_query
|
14
|
+
from ..columns import column_distance
|
15
|
+
from ..columns import column_elevation_gain
|
16
|
+
from ..columns import ColumnDescription
|
17
|
+
from ..search_util import search_query_from_form
|
18
|
+
from ..search_util import SearchQueryHistory
|
19
|
+
|
20
|
+
|
21
|
+
def register_eddington_blueprint(
|
22
|
+
repository: ActivityRepository, search_query_history: SearchQueryHistory
|
23
|
+
) -> Blueprint:
|
24
|
+
blueprint = Blueprint("eddington", __name__, template_folder="templates")
|
25
|
+
|
26
|
+
@blueprint.route("/")
|
27
|
+
def distance():
|
28
|
+
return _render_eddington_template(
|
29
|
+
repository, request, search_query_history, "distance", column_distance, [1]
|
30
|
+
)
|
31
|
+
|
32
|
+
@blueprint.route("/elevation_gain")
|
33
|
+
def elevation_gain():
|
34
|
+
return _render_eddington_template(
|
35
|
+
repository,
|
36
|
+
request,
|
37
|
+
search_query_history,
|
38
|
+
"elevation_gain",
|
39
|
+
column_elevation_gain,
|
40
|
+
[20, 10, 1],
|
41
|
+
)
|
42
|
+
|
43
|
+
return blueprint
|
44
|
+
|
45
|
+
|
46
|
+
def _render_eddington_template(
|
47
|
+
repository: ActivityRepository,
|
48
|
+
request: Request,
|
49
|
+
search_query_history: SearchQueryHistory,
|
50
|
+
template_name,
|
51
|
+
column: ColumnDescription,
|
52
|
+
divisor_values_avail: list[int],
|
53
|
+
) -> str:
|
54
|
+
|
55
|
+
column_name = column.name
|
56
|
+
display_name = column.display_name
|
57
|
+
divisor = int(request.args.get("eddington_divisor") or divisor_values_avail[0])
|
58
|
+
|
59
|
+
query = search_query_from_form(request.args)
|
60
|
+
search_query_history.register_query(query)
|
61
|
+
activities = (
|
62
|
+
apply_search_query(repository.meta, query)
|
63
|
+
.dropna(subset=["start", column_name])
|
64
|
+
.copy()
|
65
|
+
)
|
66
|
+
|
67
|
+
activities["year"] = [start.year for start in activities["start"]]
|
68
|
+
activities["date"] = [start.date() for start in activities["start"]]
|
69
|
+
activities["isoyear"] = [start.isocalendar().year for start in activities["start"]]
|
70
|
+
activities["isoweek"] = [start.isocalendar().week for start in activities["start"]]
|
71
|
+
|
72
|
+
en_per_day, eddington_df_per_day = _get_values_per_group(
|
73
|
+
activities.groupby("date"), column_name, divisor
|
74
|
+
)
|
75
|
+
en_per_week, eddington_df_per_week = _get_values_per_group(
|
76
|
+
activities.groupby(["isoyear", "isoweek"]), column_name, divisor
|
77
|
+
)
|
78
|
+
|
79
|
+
return render_template(
|
80
|
+
f"eddington/{template_name}.html.j2",
|
81
|
+
eddington_number=en_per_day,
|
82
|
+
logarithmic_plot=_make_eddington_plot(
|
83
|
+
eddington_df_per_day, en_per_day, "Days", column_name, display_name, divisor
|
84
|
+
),
|
85
|
+
eddington_per_week=en_per_week,
|
86
|
+
eddington_per_week_plot=_make_eddington_plot(
|
87
|
+
eddington_df_per_week,
|
88
|
+
en_per_week,
|
89
|
+
"Weeks",
|
90
|
+
column_name,
|
91
|
+
display_name,
|
92
|
+
divisor,
|
93
|
+
),
|
94
|
+
eddington_table=eddington_df_per_day.loc[
|
95
|
+
(eddington_df_per_day[column_name] > en_per_day)
|
96
|
+
& (eddington_df_per_day[column_name] <= en_per_day + 10 * divisor)
|
97
|
+
& (eddington_df_per_day[column_name] % divisor == 0)
|
98
|
+
].to_dict(orient="records"),
|
99
|
+
eddington_table_weeks=eddington_df_per_week.loc[
|
100
|
+
(eddington_df_per_week[column_name] > en_per_week)
|
101
|
+
& (eddington_df_per_week[column_name] <= en_per_week + 10 * divisor)
|
102
|
+
& (eddington_df_per_week[column_name] % divisor == 0)
|
103
|
+
].to_dict(orient="records"),
|
104
|
+
query=query.to_jinja(),
|
105
|
+
yearly_eddington=_get_yearly_eddington(activities, column_name, divisor),
|
106
|
+
eddington_number_history_plot=_get_eddington_number_history(
|
107
|
+
activities, column_name, divisor
|
108
|
+
),
|
109
|
+
eddington_divisor=divisor,
|
110
|
+
divisor_values_avail=divisor_values_avail,
|
111
|
+
)
|
112
|
+
|
113
|
+
|
114
|
+
def _get_values_per_group(grouped, columnName, divisor) -> tuple[int, pd.DataFrame]:
|
115
|
+
sum_per_group = grouped.apply(
|
116
|
+
lambda group: int(sum(group[columnName])), include_groups=False
|
117
|
+
)
|
118
|
+
counts = dict(zip(*np.unique(sorted(sum_per_group), return_counts=True)))
|
119
|
+
eddington = pd.DataFrame(
|
120
|
+
{columnName: d, "count": counts.get(d, 0)}
|
121
|
+
for d in range(max(counts.keys()) + 1)
|
122
|
+
)
|
123
|
+
eddington["total"] = eddington["count"][::-1].cumsum()[::-1]
|
124
|
+
eddington[f"{columnName}_div"] = eddington[columnName] // divisor
|
125
|
+
en = (
|
126
|
+
eddington.loc[eddington["total"] >= eddington[f"{columnName}_div"]][
|
127
|
+
"total"
|
128
|
+
].iloc[-1]
|
129
|
+
* divisor
|
130
|
+
)
|
131
|
+
eddington["missing"] = eddington[f"{columnName}_div"] - eddington["total"]
|
132
|
+
|
133
|
+
return en, eddington
|
134
|
+
|
135
|
+
|
136
|
+
def _make_eddington_plot(
|
137
|
+
eddington_df: pd.DataFrame,
|
138
|
+
en: int,
|
139
|
+
interval: str,
|
140
|
+
column_name: str,
|
141
|
+
display_name: str,
|
142
|
+
divisor: int,
|
143
|
+
) -> dict:
|
144
|
+
x = list(range(1, max(eddington_df[column_name]) + 1))
|
145
|
+
y = [v / divisor for v in x]
|
146
|
+
return (
|
147
|
+
(
|
148
|
+
(
|
149
|
+
alt.Chart(
|
150
|
+
eddington_df,
|
151
|
+
height=500,
|
152
|
+
width=800,
|
153
|
+
title=f"{display_name} Eddington Number {en}",
|
154
|
+
)
|
155
|
+
.mark_area(interpolate="step")
|
156
|
+
.encode(
|
157
|
+
alt.X(
|
158
|
+
column_name,
|
159
|
+
scale=alt.Scale(domainMin=0, domainMax=en * 3),
|
160
|
+
title=display_name,
|
161
|
+
),
|
162
|
+
alt.Y(
|
163
|
+
"total",
|
164
|
+
scale=alt.Scale(domainMax=en / divisor * 1.5),
|
165
|
+
title=f"{interval} exceeding {display_name}",
|
166
|
+
),
|
167
|
+
[
|
168
|
+
alt.Tooltip(column_name, title=display_name),
|
169
|
+
alt.Tooltip(
|
170
|
+
"total", title=f"{interval} exceeding {display_name}"
|
171
|
+
),
|
172
|
+
alt.Tooltip("missing", title=f"{interval} missing for next"),
|
173
|
+
],
|
174
|
+
)
|
175
|
+
)
|
176
|
+
+ (
|
177
|
+
alt.Chart(pd.DataFrame({column_name: x, "total": y}))
|
178
|
+
.mark_line(color="red")
|
179
|
+
.encode(alt.X(column_name), alt.Y("total"))
|
180
|
+
)
|
181
|
+
)
|
182
|
+
.interactive(bind_x=True, bind_y=True)
|
183
|
+
.to_json(format="vega")
|
184
|
+
)
|
185
|
+
|
186
|
+
|
187
|
+
def _get_eddington_number(elevation_gains: pd.Series, divisor: int) -> int:
|
188
|
+
if len(elevation_gains) == 1:
|
189
|
+
if elevation_gains.iloc[0] >= 1:
|
190
|
+
return 1
|
191
|
+
else:
|
192
|
+
0
|
193
|
+
|
194
|
+
sorted_elevation_gains = sorted(elevation_gains, reverse=True)
|
195
|
+
|
196
|
+
for number_of_days, elevation_gain in enumerate(sorted_elevation_gains, 1):
|
197
|
+
if elevation_gain / divisor < number_of_days:
|
198
|
+
return (number_of_days - 1) * divisor
|
199
|
+
|
200
|
+
|
201
|
+
def _get_yearly_eddington(
|
202
|
+
meta: pd.DataFrame, columnName: str, divisor: int
|
203
|
+
) -> dict[int, int]:
|
204
|
+
meta = meta.dropna(subset=["start", columnName]).copy()
|
205
|
+
meta["year"] = [start.year for start in meta["start"]]
|
206
|
+
meta["date"] = [start.date() for start in meta["start"]]
|
207
|
+
|
208
|
+
yearly_eddington = meta.groupby("year").apply(
|
209
|
+
lambda group: _get_eddington_number(
|
210
|
+
group.groupby("date").apply(
|
211
|
+
lambda group2: int(group2[columnName].sum()), include_groups=False
|
212
|
+
),
|
213
|
+
divisor,
|
214
|
+
),
|
215
|
+
include_groups=False,
|
216
|
+
)
|
217
|
+
return yearly_eddington.to_dict()
|
218
|
+
|
219
|
+
|
220
|
+
def _get_eddington_number_history(
|
221
|
+
meta: pd.DataFrame, columnName: str, divisor: int
|
222
|
+
) -> dict:
|
223
|
+
|
224
|
+
daily_elevation_gains = meta.groupby("date").apply(
|
225
|
+
lambda group2: int(group2[columnName].sum()), include_groups=False
|
226
|
+
)
|
227
|
+
|
228
|
+
eddington_number_history = {"date": [], "eddington_number": []}
|
229
|
+
top_days = []
|
230
|
+
for date, elevation_gain in daily_elevation_gains.items():
|
231
|
+
elevation_gain = elevation_gain / divisor
|
232
|
+
if len(top_days) == 0:
|
233
|
+
top_days.append(elevation_gain)
|
234
|
+
else:
|
235
|
+
if elevation_gain >= top_days[0]:
|
236
|
+
top_days.append(elevation_gain)
|
237
|
+
top_days.sort()
|
238
|
+
while top_days[0] < len(top_days):
|
239
|
+
top_days.pop(0)
|
240
|
+
eddington_number_history["date"].append(
|
241
|
+
datetime.datetime.combine(date, datetime.datetime.min.time())
|
242
|
+
)
|
243
|
+
eddington_number_history["eddington_number"].append(len(top_days) * divisor)
|
244
|
+
history = pd.DataFrame(eddington_number_history)
|
245
|
+
|
246
|
+
return (
|
247
|
+
alt.Chart(history)
|
248
|
+
.mark_line(interpolate="step-after")
|
249
|
+
.encode(
|
250
|
+
alt.X("date", title="Date"),
|
251
|
+
alt.Y("eddington_number", title="Eddington number"),
|
252
|
+
)
|
253
|
+
).to_json(format="vega")
|
@@ -4,12 +4,18 @@ import datetime
|
|
4
4
|
import altair as alt
|
5
5
|
import flask
|
6
6
|
import pandas as pd
|
7
|
+
import sqlalchemy
|
7
8
|
from flask import render_template
|
8
9
|
from flask import Response
|
9
10
|
|
10
11
|
from ...core.activities import ActivityRepository
|
11
12
|
from ...core.activities import make_geojson_from_time_series
|
12
13
|
from ...core.config import Config
|
14
|
+
from ...core.datamodel import Activity
|
15
|
+
from ...core.datamodel import DB
|
16
|
+
from ..columns import column_distance
|
17
|
+
from ..columns import column_elevation_gain
|
18
|
+
from ..columns import ColumnDescription
|
13
19
|
from ..plot_util import make_kind_scale
|
14
20
|
|
15
21
|
|
@@ -22,27 +28,32 @@ def register_entry_views(
|
|
22
28
|
|
23
29
|
if len(repository):
|
24
30
|
kind_scale = make_kind_scale(repository.meta, config)
|
25
|
-
context["distance_last_30_days_plot"] =
|
26
|
-
repository.meta, kind_scale
|
31
|
+
context["distance_last_30_days_plot"] = _last_30_days_meta_plot(
|
32
|
+
repository.meta, kind_scale, column_distance
|
33
|
+
)
|
34
|
+
context["elevation_gain_last_30_days_plot"] = _last_30_days_meta_plot(
|
35
|
+
repository.meta, kind_scale, column_elevation_gain
|
27
36
|
)
|
28
37
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
for index, activity in activity_meta.iterrows():
|
35
|
-
time_series = repository.get_time_series(activity["id"])
|
36
|
-
context["latest_activities"][date].append(
|
38
|
+
context["latest_activities"] = collections.defaultdict(list)
|
39
|
+
for activity in DB.session.scalars(
|
40
|
+
sqlalchemy.select(Activity).order_by(Activity.start.desc()).limit(100)
|
41
|
+
):
|
42
|
+
context["latest_activities"][activity.start.date()].append(
|
37
43
|
{
|
38
44
|
"activity": activity,
|
39
|
-
"line_geojson": make_geojson_from_time_series(
|
45
|
+
"line_geojson": make_geojson_from_time_series(
|
46
|
+
activity.time_series
|
47
|
+
),
|
40
48
|
}
|
41
49
|
)
|
50
|
+
|
42
51
|
return render_template("home.html.j2", **context)
|
43
52
|
|
44
53
|
|
45
|
-
def
|
54
|
+
def _last_30_days_meta_plot(
|
55
|
+
meta: pd.DataFrame, kind_scale: alt.Scale, column: ColumnDescription
|
56
|
+
) -> str:
|
46
57
|
before_30_days = pd.to_datetime(
|
47
58
|
datetime.datetime.now() - datetime.timedelta(days=31)
|
48
59
|
)
|
@@ -51,17 +62,21 @@ def _distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale)
|
|
51
62
|
meta.loc[meta["start"] > before_30_days],
|
52
63
|
width=700,
|
53
64
|
height=200,
|
54
|
-
title="
|
65
|
+
title=f"{column.display_name} per day",
|
55
66
|
)
|
56
67
|
.mark_bar()
|
57
68
|
.encode(
|
58
69
|
alt.X("yearmonthdate(start)", title="Date"),
|
59
|
-
alt.Y("sum(
|
70
|
+
alt.Y(f"sum({column.name})", title=f"{column.name} / {column.unit}"),
|
60
71
|
alt.Color("kind", scale=kind_scale, title="Kind"),
|
61
72
|
[
|
62
73
|
alt.Tooltip("yearmonthdate(start)", title="Date"),
|
63
74
|
alt.Tooltip("kind", title="Kind"),
|
64
|
-
alt.Tooltip(
|
75
|
+
alt.Tooltip(
|
76
|
+
f"sum({column.name})",
|
77
|
+
format=column.format,
|
78
|
+
title=f"{column.display_name} / {column.unit}",
|
79
|
+
),
|
65
80
|
],
|
66
81
|
)
|
67
82
|
.to_json(format="vega")
|
@@ -1,12 +1,15 @@
|
|
1
1
|
import datetime
|
2
|
+
import io
|
2
3
|
import itertools
|
3
4
|
import logging
|
4
5
|
|
5
6
|
import altair as alt
|
6
7
|
import geojson
|
7
8
|
import matplotlib
|
9
|
+
import matplotlib.pyplot as pl
|
8
10
|
import numpy as np
|
9
11
|
import pandas as pd
|
12
|
+
import sqlalchemy
|
10
13
|
from flask import Blueprint
|
11
14
|
from flask import flash
|
12
15
|
from flask import redirect
|
@@ -17,6 +20,10 @@ from flask import url_for
|
|
17
20
|
from ...core.activities import ActivityRepository
|
18
21
|
from ...core.config import ConfigAccessor
|
19
22
|
from ...core.coordinates import Bounds
|
23
|
+
from ...core.datamodel import Activity
|
24
|
+
from ...core.datamodel import DB
|
25
|
+
from ...core.raster_map import ImageTransform
|
26
|
+
from ...core.raster_map import TileGetter
|
20
27
|
from ...core.tiles import compute_tile
|
21
28
|
from ...core.tiles import get_tile_upper_left_lat_lon
|
22
29
|
from ...explorer.grid_file import get_border_tiles
|
@@ -38,9 +45,10 @@ logger = logging.getLogger(__name__)
|
|
38
45
|
|
39
46
|
def make_explorer_blueprint(
|
40
47
|
authenticator: Authenticator,
|
41
|
-
repository: ActivityRepository,
|
42
48
|
tile_visit_accessor: TileVisitAccessor,
|
43
49
|
config_accessor: ConfigAccessor,
|
50
|
+
tile_getter: TileGetter,
|
51
|
+
image_transforms: dict[str, ImageTransform],
|
44
52
|
) -> Blueprint:
|
45
53
|
blueprint = Blueprint("explorer", __name__, template_folder="templates")
|
46
54
|
|
@@ -59,7 +67,7 @@ def make_explorer_blueprint(
|
|
59
67
|
)
|
60
68
|
|
61
69
|
explored = get_three_color_tiles(
|
62
|
-
tile_visits[zoom],
|
70
|
+
tile_visits[zoom], tile_evolution_states[zoom], zoom
|
63
71
|
)
|
64
72
|
|
65
73
|
context = {
|
@@ -151,12 +159,74 @@ def make_explorer_blueprint(
|
|
151
159
|
headers={"Content-disposition": "attachment"},
|
152
160
|
)
|
153
161
|
|
162
|
+
@blueprint.route("/<int:zoom>/server-side")
|
163
|
+
def server_side(zoom: int):
|
164
|
+
if zoom not in config_accessor().explorer_zoom_levels:
|
165
|
+
return {"zoom_level_not_generated": zoom}
|
166
|
+
|
167
|
+
tile_evolution_states = tile_visit_accessor.tile_state["evolution_state"]
|
168
|
+
tile_histories = tile_visit_accessor.tile_state["tile_history"]
|
169
|
+
|
170
|
+
medians = tile_histories[zoom].median()
|
171
|
+
median_lat, median_lon = get_tile_upper_left_lat_lon(
|
172
|
+
medians["tile_x"], medians["tile_y"], zoom
|
173
|
+
)
|
174
|
+
|
175
|
+
context = {
|
176
|
+
"center": {
|
177
|
+
"latitude": median_lat,
|
178
|
+
"longitude": median_lon,
|
179
|
+
"bbox": (
|
180
|
+
bounding_box_for_biggest_cluster(
|
181
|
+
tile_evolution_states[zoom].clusters.values(), zoom
|
182
|
+
)
|
183
|
+
if len(tile_evolution_states[zoom].memberships) > 0
|
184
|
+
else {}
|
185
|
+
),
|
186
|
+
},
|
187
|
+
"plot_tile_evolution": plot_tile_evolution(tile_histories[zoom]),
|
188
|
+
"plot_cluster_evolution": plot_cluster_evolution(
|
189
|
+
tile_evolution_states[zoom].cluster_evolution
|
190
|
+
),
|
191
|
+
"plot_square_evolution": plot_square_evolution(
|
192
|
+
tile_evolution_states[zoom].square_evolution
|
193
|
+
),
|
194
|
+
"zoom": zoom,
|
195
|
+
}
|
196
|
+
return render_template("explorer/server-side.html.j2", **context)
|
197
|
+
|
198
|
+
@blueprint.route("/<int:zoom>/tile/<int:z>/<int:x>/<int:y>.png")
|
199
|
+
def tile(zoom: int, z: int, x: int, y: int) -> Response:
|
200
|
+
tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
|
201
|
+
|
202
|
+
map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
|
203
|
+
if z >= zoom:
|
204
|
+
factor = 2 ** (z - zoom)
|
205
|
+
if (x // factor, y // factor) in tile_visits:
|
206
|
+
map_tile = image_transforms["color"].transform_image(map_tile)
|
207
|
+
else:
|
208
|
+
map_tile = image_transforms["color"].transform_image(map_tile) / 1.2
|
209
|
+
else:
|
210
|
+
grayscale = image_transforms["color"].transform_image(map_tile) / 1.2
|
211
|
+
factor = 2 ** (zoom - z)
|
212
|
+
width = 256 // factor
|
213
|
+
for xo in range(factor):
|
214
|
+
for yo in range(factor):
|
215
|
+
if (x * factor + xo, y * factor + yo) not in tile_visits:
|
216
|
+
map_tile[
|
217
|
+
yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
|
218
|
+
] = grayscale[
|
219
|
+
yo * width : (yo + 1) * width, xo * width : (xo + 1) * width
|
220
|
+
]
|
221
|
+
f = io.BytesIO()
|
222
|
+
pl.imsave(f, map_tile, format="png")
|
223
|
+
return Response(bytes(f.getbuffer()), mimetype="image/png")
|
224
|
+
|
154
225
|
return blueprint
|
155
226
|
|
156
227
|
|
157
228
|
def get_three_color_tiles(
|
158
229
|
tile_visits: dict,
|
159
|
-
repository: ActivityRepository,
|
160
230
|
cluster_state: TileEvolutionState,
|
161
231
|
zoom: int,
|
162
232
|
) -> str:
|
@@ -174,13 +244,17 @@ def get_three_color_tiles(
|
|
174
244
|
last_age_days = 10000
|
175
245
|
tile_dict[tile] = {
|
176
246
|
"first_activity_id": str(tile_data["first_id"]),
|
177
|
-
"first_activity_name":
|
178
|
-
|
179
|
-
|
247
|
+
"first_activity_name": DB.session.scalar(
|
248
|
+
sqlalchemy.select(Activity.name).where(
|
249
|
+
Activity.id == tile_data["first_id"]
|
250
|
+
)
|
251
|
+
),
|
180
252
|
"last_activity_id": str(tile_data["last_id"]),
|
181
|
-
"last_activity_name":
|
182
|
-
|
183
|
-
|
253
|
+
"last_activity_name": DB.session.scalar(
|
254
|
+
sqlalchemy.select(Activity.name).where(
|
255
|
+
Activity.id == tile_data["last_id"]
|
256
|
+
)
|
257
|
+
),
|
184
258
|
"first_age_days": first_age_days,
|
185
259
|
"first_age_color": matplotlib.colors.to_hex(
|
186
260
|
cmap_first(max(1 - first_age_days / (2 * 365), 0.0))
|