geo-activity-playground 0.27.1__py3-none-any.whl → 0.29.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 +1 -2
- geo_activity_playground/core/activities.py +3 -3
- geo_activity_playground/core/config.py +4 -0
- geo_activity_playground/core/paths.py +10 -0
- geo_activity_playground/core/tasks.py +7 -6
- geo_activity_playground/explorer/tile_visits.py +168 -133
- geo_activity_playground/webui/activity/controller.py +51 -14
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +37 -9
- geo_activity_playground/webui/app.py +20 -22
- geo_activity_playground/webui/auth/blueprint.py +27 -0
- geo_activity_playground/webui/auth/templates/auth/index.html.j2 +21 -0
- geo_activity_playground/webui/authenticator.py +46 -0
- geo_activity_playground/webui/entry_controller.py +8 -4
- geo_activity_playground/webui/equipment/controller.py +2 -1
- geo_activity_playground/webui/explorer/controller.py +4 -3
- geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +2 -0
- geo_activity_playground/webui/heatmap/heatmap_controller.py +20 -6
- geo_activity_playground/webui/plot_util.py +9 -0
- geo_activity_playground/webui/search/blueprint.py +20 -0
- geo_activity_playground/webui/settings/blueprint.py +101 -1
- geo_activity_playground/webui/settings/controller.py +43 -0
- geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2 +19 -0
- geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +33 -0
- geo_activity_playground/webui/settings/templates/settings/index.html.j2 +27 -0
- geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2 +22 -0
- geo_activity_playground/webui/square_planner/controller.py +1 -1
- geo_activity_playground/webui/summary/blueprint.py +3 -2
- geo_activity_playground/webui/summary/controller.py +20 -13
- geo_activity_playground/webui/templates/home.html.j2 +1 -1
- geo_activity_playground/webui/templates/page.html.j2 +57 -29
- geo_activity_playground/webui/upload/blueprint.py +7 -0
- geo_activity_playground/webui/upload/controller.py +4 -8
- geo_activity_playground/webui/upload/templates/upload/index.html.j2 +15 -31
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/METADATA +3 -4
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/RECORD +39 -32
- geo_activity_playground/webui/search_controller.py +0 -19
- /geo_activity_playground/webui/{templates/search.html.j2 → search/templates/search/index.html.j2} +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.27.1.dist-info → geo_activity_playground-0.29.0.dist-info}/entry_points.txt +0 -0
@@ -109,10 +109,9 @@ def make_activity_repository(
|
|
109
109
|
|
110
110
|
|
111
111
|
def main_cache(basedir: pathlib.Path) -> None:
|
112
|
-
repository, tile_visit_accessor, config_accessor = make_activity_repository(
|
112
|
+
(repository, tile_visit_accessor, config_accessor) = make_activity_repository(
|
113
113
|
basedir, False
|
114
114
|
)
|
115
|
-
scan_for_activities(repository, tile_visit_accessor, config_accessor())
|
116
115
|
|
117
116
|
|
118
117
|
if __name__ == "__main__":
|
@@ -128,11 +128,11 @@ class ActivityRepository:
|
|
128
128
|
else:
|
129
129
|
return None
|
130
130
|
|
131
|
-
def get_activity_ids(self, only_achievements: bool = False) ->
|
131
|
+
def get_activity_ids(self, only_achievements: bool = False) -> list[int]:
|
132
132
|
if only_achievements:
|
133
|
-
return
|
133
|
+
return list(self.meta.loc[self.meta["consider_for_achievements"]].index)
|
134
134
|
else:
|
135
|
-
return
|
135
|
+
return list(self.meta.index)
|
136
136
|
|
137
137
|
def iter_activities(self, new_to_old=True, dropna=False) -> Iterator[ActivityMeta]:
|
138
138
|
direction = -1 if new_to_old else 1
|
@@ -21,6 +21,8 @@ logger = logging.getLogger(__name__)
|
|
21
21
|
@dataclasses.dataclass
|
22
22
|
class Config:
|
23
23
|
birth_year: Optional[int] = None
|
24
|
+
color_scheme_for_counts: str = "viridis"
|
25
|
+
color_scheme_for_kind: str = "category10"
|
24
26
|
equipment_offsets: dict[str, float] = dataclasses.field(default_factory=dict)
|
25
27
|
explorer_zoom_levels: list[int] = dataclasses.field(
|
26
28
|
default_factory=lambda: [14, 17]
|
@@ -33,6 +35,7 @@ class Config:
|
|
33
35
|
privacy_zones: dict[str, list[list[float]]] = dataclasses.field(
|
34
36
|
default_factory=dict
|
35
37
|
)
|
38
|
+
sharepic_suppressed_fields: list[str] = dataclasses.field(default_factory=list)
|
36
39
|
strava_client_id: int = 131693
|
37
40
|
strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
|
38
41
|
strava_client_code: Optional[str] = None
|
@@ -51,6 +54,7 @@ class ConfigAccessor:
|
|
51
54
|
return self._config
|
52
55
|
|
53
56
|
def save(self) -> None:
|
57
|
+
print(self._config)
|
54
58
|
with open(new_config_file(), "w") as f:
|
55
59
|
json.dump(
|
56
60
|
dataclasses.asdict(self._config),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
"""
|
2
2
|
Paths within the playground and cache.
|
3
3
|
"""
|
4
|
+
import contextlib
|
4
5
|
import functools
|
5
6
|
import pathlib
|
6
7
|
import typing
|
@@ -24,6 +25,15 @@ def file_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
|
|
24
25
|
return wrapper
|
25
26
|
|
26
27
|
|
28
|
+
@contextlib.contextmanager
|
29
|
+
def atomic_open(path: pathlib.Path, mode: str):
|
30
|
+
temp_path = path.with_stem(path.stem + "-temp")
|
31
|
+
with open(temp_path, mode) as f:
|
32
|
+
yield f
|
33
|
+
path.unlink(missing_ok=True)
|
34
|
+
temp_path.rename(path)
|
35
|
+
|
36
|
+
|
27
37
|
_cache_dir = pathlib.Path("Cache")
|
28
38
|
|
29
39
|
_activity_dir = _cache_dir / "Activity"
|
@@ -8,6 +8,7 @@ from typing import Generic
|
|
8
8
|
from typing import Sequence
|
9
9
|
from typing import TypeVar
|
10
10
|
|
11
|
+
from geo_activity_playground.core.paths import atomic_open
|
11
12
|
from geo_activity_playground.core.paths import cache_dir
|
12
13
|
|
13
14
|
|
@@ -24,11 +25,8 @@ def stored_object(path: pathlib.Path, default):
|
|
24
25
|
|
25
26
|
yield payload
|
26
27
|
|
27
|
-
|
28
|
-
with open(temp_location, "wb") as f:
|
28
|
+
with atomic_open(path, "wb") as f:
|
29
29
|
pickle.dump(payload, f)
|
30
|
-
path.unlink(missing_ok=True)
|
31
|
-
temp_location.rename(path)
|
32
30
|
|
33
31
|
|
34
32
|
def work_tracker_path(name: str) -> pathlib.Path:
|
@@ -59,8 +57,8 @@ class WorkTracker:
|
|
59
57
|
else:
|
60
58
|
self._done = set()
|
61
59
|
|
62
|
-
def filter(self, ids: Iterable) ->
|
63
|
-
return
|
60
|
+
def filter(self, ids: Iterable) -> list:
|
61
|
+
return [elem for elem in ids if elem not in self._done]
|
64
62
|
|
65
63
|
def mark_done(self, id: int) -> None:
|
66
64
|
self._done.add(id)
|
@@ -68,6 +66,9 @@ class WorkTracker:
|
|
68
66
|
def discard(self, id) -> None:
|
69
67
|
self._done.discard(id)
|
70
68
|
|
69
|
+
def reset(self) -> None:
|
70
|
+
self._done = set()
|
71
|
+
|
71
72
|
def close(self) -> None:
|
72
73
|
with open(self._path, "wb") as f:
|
73
74
|
pickle.dump(self._done, f)
|
@@ -14,6 +14,7 @@ from tqdm import tqdm
|
|
14
14
|
|
15
15
|
from geo_activity_playground.core.activities import ActivityRepository
|
16
16
|
from geo_activity_playground.core.config import Config
|
17
|
+
from geo_activity_playground.core.paths import atomic_open
|
17
18
|
from geo_activity_playground.core.paths import tiles_per_time_series
|
18
19
|
from geo_activity_playground.core.tasks import try_load_pickle
|
19
20
|
from geo_activity_playground.core.tasks import work_tracker_path
|
@@ -25,139 +26,192 @@ from geo_activity_playground.core.tiles import interpolate_missing_tile
|
|
25
26
|
logger = logging.getLogger(__name__)
|
26
27
|
|
27
28
|
|
29
|
+
class TileInfo(TypedDict):
|
30
|
+
activity_ids: set[int]
|
31
|
+
first_time: datetime.datetime
|
32
|
+
first_id: int
|
33
|
+
last_time: datetime.datetime
|
34
|
+
last_id: int
|
35
|
+
|
36
|
+
|
28
37
|
class TileHistoryRow(TypedDict):
|
38
|
+
activity_id: int
|
29
39
|
time: datetime.datetime
|
30
40
|
tile_x: int
|
31
41
|
tile_y: int
|
32
42
|
|
33
43
|
|
44
|
+
class TileEvolutionState:
|
45
|
+
def __init__(self) -> None:
|
46
|
+
self.num_neighbors: dict[tuple[int, int], int] = {}
|
47
|
+
self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
|
48
|
+
self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
|
49
|
+
self.cluster_evolution = pd.DataFrame()
|
50
|
+
self.square_start = 0
|
51
|
+
self.cluster_start = 0
|
52
|
+
self.max_square_size = 0
|
53
|
+
self.visited_tiles: set[tuple[int, int]] = set()
|
54
|
+
self.square_evolution = pd.DataFrame()
|
55
|
+
self.square_x: Optional[int] = None
|
56
|
+
self.square_y: Optional[int] = None
|
57
|
+
|
58
|
+
|
59
|
+
class TileState(TypedDict):
|
60
|
+
tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
|
61
|
+
tile_history: dict[int, pd.DataFrame]
|
62
|
+
activities_per_tile: dict[int, set[int]]
|
63
|
+
processed_activities: set[int]
|
64
|
+
evolution_state: dict[int, TileEvolutionState]
|
65
|
+
version: int
|
66
|
+
|
67
|
+
|
68
|
+
TILE_STATE_VERSION = 2
|
69
|
+
|
70
|
+
|
34
71
|
class TileVisitAccessor:
|
35
|
-
|
36
|
-
TILE_HISTORIES_PATH = pathlib.Path(f"Cache/tile-history.pickle")
|
37
|
-
TILE_VISITS_PATH = pathlib.Path(f"Cache/tile-visits.pickle")
|
38
|
-
ACTIVITIES_PER_TILE_PATH = pathlib.Path(f"Cache/activities-per-tile.pickle")
|
72
|
+
PATH = pathlib.Path("Cache/tile-state-2.pickle")
|
39
73
|
|
40
74
|
def __init__(self) -> None:
|
41
|
-
self.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
self.
|
51
|
-
self.TILE_EVOLUTION_STATES_PATH
|
52
|
-
) or collections.defaultdict(TileEvolutionState)
|
53
|
-
|
54
|
-
self.activities_per_tile: dict[
|
55
|
-
int, dict[tuple[int, int], set[int]]
|
56
|
-
] = try_load_pickle(self.ACTIVITIES_PER_TILE_PATH) or collections.defaultdict(
|
57
|
-
dict
|
58
|
-
)
|
75
|
+
self.tile_state: TileState = try_load_pickle(self.PATH)
|
76
|
+
if (
|
77
|
+
self.tile_state is None
|
78
|
+
or self.tile_state.get("version", None) != TILE_STATE_VERSION
|
79
|
+
):
|
80
|
+
self.tile_state = make_tile_state()
|
81
|
+
# TODO: Reset work tracker
|
82
|
+
|
83
|
+
def reset(self) -> None:
|
84
|
+
self.tile_state = make_tile_state()
|
59
85
|
|
60
86
|
def save(self) -> None:
|
61
|
-
with
|
62
|
-
pickle.dump(self.
|
87
|
+
with atomic_open(self.PATH, "wb") as f:
|
88
|
+
pickle.dump(self.tile_state, f)
|
89
|
+
|
90
|
+
|
91
|
+
def make_defaultdict_dict():
|
92
|
+
return collections.defaultdict(dict)
|
93
|
+
|
94
|
+
|
95
|
+
def make_defaultdict_set():
|
96
|
+
return collections.defaultdict(set)
|
97
|
+
|
98
|
+
|
99
|
+
def make_tile_state() -> TileState:
|
100
|
+
tile_state: TileState = {
|
101
|
+
"tile_visits": collections.defaultdict(make_defaultdict_dict),
|
102
|
+
"tile_history": collections.defaultdict(pd.DataFrame),
|
103
|
+
"activities_per_tile": collections.defaultdict(make_defaultdict_set),
|
104
|
+
"processed_activities": set(),
|
105
|
+
"evolution_state": collections.defaultdict(TileEvolutionState),
|
106
|
+
"version": TILE_STATE_VERSION,
|
107
|
+
}
|
108
|
+
return tile_state
|
63
109
|
|
64
|
-
with open(self.TILE_HISTORIES_PATH, "wb") as f:
|
65
|
-
pickle.dump(self.histories, f)
|
66
110
|
|
67
|
-
|
68
|
-
|
111
|
+
def _consistency_check(
|
112
|
+
repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
113
|
+
) -> bool:
|
114
|
+
present_activity_ids = set(repository.get_activity_ids())
|
69
115
|
|
70
|
-
|
71
|
-
|
116
|
+
for zoom, activities_per_tile in tile_visit_accessor.tile_state[
|
117
|
+
"activities_per_tile"
|
118
|
+
].items():
|
119
|
+
for tile, tile_activity_ids in activities_per_tile.items():
|
120
|
+
deleted_activity_ids = tile_activity_ids - present_activity_ids
|
121
|
+
if deleted_activity_ids:
|
122
|
+
logger.info(f"Activities {deleted_activity_ids} have been deleted.")
|
123
|
+
return False
|
72
124
|
|
125
|
+
for zoom, tile_visits in tile_visit_accessor.tile_state["tile_visits"].items():
|
126
|
+
for tile, meta in tile_visits.items():
|
127
|
+
if meta["first_id"] not in present_activity_ids:
|
128
|
+
logger.info(f"Activity {meta['first_id']} have been deleted.")
|
129
|
+
return False
|
130
|
+
if meta["last_id"] not in present_activity_ids:
|
131
|
+
logger.info(f"Activity {meta['last_id']} have been deleted.")
|
132
|
+
return False
|
73
133
|
|
74
|
-
|
75
|
-
|
134
|
+
return True
|
135
|
+
|
136
|
+
|
137
|
+
def compute_tile_visits_new(
|
138
|
+
repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
|
76
139
|
) -> None:
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
for zoom, activities_per_tile in tile_visits_accessor.activities_per_tile.items():
|
84
|
-
for tile, activity_ids in activities_per_tile.items():
|
85
|
-
deleted_ids = activity_ids - present_activity_ids
|
86
|
-
if deleted_ids:
|
87
|
-
logger.debug(
|
88
|
-
f"Removing activities {deleted_ids} from tile {tile} at {zoom=}."
|
89
|
-
)
|
90
|
-
for activity_id in deleted_ids:
|
91
|
-
activity_ids.remove(activity_id)
|
92
|
-
work_tracker.discard(activity_id)
|
93
|
-
changed_zoom_tile[zoom].add(tile)
|
140
|
+
work_tracker = WorkTracker(work_tracker_path("tile-state"))
|
141
|
+
|
142
|
+
if not _consistency_check(repository, tile_visit_accessor):
|
143
|
+
logger.warning("Need to recompute Explorer Tiles due to deleted activities.")
|
144
|
+
tile_visit_accessor.reset()
|
145
|
+
work_tracker.reset()
|
94
146
|
|
95
|
-
# Add visits from new activities.
|
96
|
-
activity_ids_to_process = work_tracker.filter(repository.get_activity_ids())
|
97
147
|
for activity_id in tqdm(
|
98
|
-
|
148
|
+
work_tracker.filter(repository.get_activity_ids()), desc="Tile visits"
|
99
149
|
):
|
100
|
-
|
101
|
-
for time, tile_x, tile_y in _tiles_from_points(
|
102
|
-
repository.get_time_series(activity_id), zoom
|
103
|
-
):
|
104
|
-
tile = (tile_x, tile_y)
|
105
|
-
if tile not in tile_visits_accessor.activities_per_tile[zoom]:
|
106
|
-
tile_visits_accessor.activities_per_tile[zoom][tile] = set()
|
107
|
-
tile_visits_accessor.activities_per_tile[zoom][tile].add(activity_id)
|
108
|
-
changed_zoom_tile[zoom].add(tile)
|
150
|
+
_process_activity(repository, tile_visit_accessor.tile_state, activity_id)
|
109
151
|
work_tracker.mark_done(activity_id)
|
110
|
-
|
111
|
-
# Update tile visits structure.
|
112
|
-
for zoom, changed_tiles in tqdm(
|
113
|
-
changed_zoom_tile.items(), desc="Incorporate changes in tiles"
|
114
|
-
):
|
115
|
-
soa = {"activity_id": [], "time": [], "tile_x": [], "tile_y": []}
|
116
|
-
|
117
|
-
for tile in changed_tiles:
|
118
|
-
activity_ids = tile_visits_accessor.activities_per_tile[zoom][tile]
|
119
|
-
activities = [
|
120
|
-
repository.get_activity_by_id(activity_id)
|
121
|
-
for activity_id in activity_ids
|
122
|
-
]
|
123
|
-
activities_to_consider = [
|
124
|
-
activity
|
125
|
-
for activity in activities
|
126
|
-
if activity["consider_for_achievements"]
|
127
|
-
]
|
128
|
-
activities_to_consider.sort(key=lambda activity: activity["start"])
|
129
|
-
|
130
|
-
if activities_to_consider:
|
131
|
-
tile_visits_accessor.visits[zoom][tile] = {
|
132
|
-
"first_time": activities_to_consider[0]["start"],
|
133
|
-
"first_id": activities_to_consider[0]["id"],
|
134
|
-
"last_time": activities_to_consider[-1]["start"],
|
135
|
-
"last_id": activities_to_consider[-1]["id"],
|
136
|
-
"activity_ids": {
|
137
|
-
activity["id"] for activity in activities_to_consider
|
138
|
-
},
|
139
|
-
}
|
140
|
-
|
141
|
-
soa["activity_id"].append(activities_to_consider[0]["id"])
|
142
|
-
soa["time"].append(activities_to_consider[0]["start"])
|
143
|
-
soa["tile_x"].append(tile[0])
|
144
|
-
soa["tile_y"].append(tile[1])
|
145
|
-
else:
|
146
|
-
if tile in tile_visits_accessor.visits[zoom]:
|
147
|
-
del tile_visits_accessor.visits[zoom][tile]
|
148
|
-
|
149
|
-
df = pd.DataFrame(soa)
|
150
|
-
if len(df) > 0:
|
151
|
-
df = pd.concat([tile_visits_accessor.histories[zoom], df])
|
152
|
-
df.sort_values("time", inplace=True)
|
153
|
-
tile_visits_accessor.histories[zoom] = df.groupby(
|
154
|
-
["tile_x", "tile_y"]
|
155
|
-
).head(1)
|
156
|
-
|
157
|
-
tile_visits_accessor.save()
|
152
|
+
tile_visit_accessor.save()
|
158
153
|
work_tracker.close()
|
159
154
|
|
160
155
|
|
156
|
+
def _process_activity(
|
157
|
+
repository: ActivityRepository, tile_state: TileState, activity_id: int
|
158
|
+
) -> None:
|
159
|
+
activity = repository.get_activity_by_id(activity_id)
|
160
|
+
time_series = repository.get_time_series(activity_id)
|
161
|
+
|
162
|
+
activity_tiles = pd.DataFrame(
|
163
|
+
_tiles_from_points(time_series, 19), columns=["time", "tile_x", "tile_y"]
|
164
|
+
)
|
165
|
+
for zoom in reversed(range(20)):
|
166
|
+
activities_per_tile = tile_state["activities_per_tile"][zoom]
|
167
|
+
|
168
|
+
new_tile_history_soa = {
|
169
|
+
"activity_id": [],
|
170
|
+
"time": [],
|
171
|
+
"tile_x": [],
|
172
|
+
"tile_y": [],
|
173
|
+
}
|
174
|
+
|
175
|
+
activity_tiles = activity_tiles.groupby(["tile_x", "tile_y"]).head(1)
|
176
|
+
|
177
|
+
for time, tile in zip(
|
178
|
+
activity_tiles["time"],
|
179
|
+
zip(activity_tiles["tile_x"], activity_tiles["tile_y"]),
|
180
|
+
):
|
181
|
+
if activity["consider_for_achievements"]:
|
182
|
+
if tile not in tile_state["tile_visits"][zoom]:
|
183
|
+
new_tile_history_soa["activity_id"].append(activity_id)
|
184
|
+
new_tile_history_soa["time"].append(time)
|
185
|
+
new_tile_history_soa["tile_x"].append(tile[0])
|
186
|
+
new_tile_history_soa["tile_y"].append(tile[1])
|
187
|
+
|
188
|
+
tile_visit = tile_state["tile_visits"][zoom][tile]
|
189
|
+
if not tile_visit:
|
190
|
+
tile_visit["activity_ids"] = {activity_id}
|
191
|
+
else:
|
192
|
+
tile_visit["activity_ids"].add(activity_id)
|
193
|
+
|
194
|
+
first_time = tile_visit.get("first_time", None)
|
195
|
+
last_time = tile_visit.get("last_time", None)
|
196
|
+
if first_time is None or time < first_time:
|
197
|
+
tile_visit["first_id"] = activity_id
|
198
|
+
tile_visit["first_time"] = time
|
199
|
+
if last_time is None or time > last_time:
|
200
|
+
tile_visit["last_id"] = activity_id
|
201
|
+
tile_visit["last_time"] = time
|
202
|
+
|
203
|
+
activities_per_tile[tile].add(activity_id)
|
204
|
+
|
205
|
+
if new_tile_history_soa["activity_id"]:
|
206
|
+
tile_state["tile_history"][zoom] = pd.concat(
|
207
|
+
[tile_state["tile_history"][zoom], pd.DataFrame(new_tile_history_soa)]
|
208
|
+
)
|
209
|
+
|
210
|
+
# Move up one layer in the quad-tree.
|
211
|
+
activity_tiles["tile_x"] //= 2
|
212
|
+
activity_tiles["tile_y"] //= 2
|
213
|
+
|
214
|
+
|
161
215
|
def _tiles_from_points(
|
162
216
|
time_series: pd.DataFrame, zoom: int
|
163
217
|
) -> Iterator[tuple[datetime.datetime, int, int]]:
|
@@ -181,38 +235,19 @@ def _tiles_from_points(
|
|
181
235
|
yield (t1,) + interpolated
|
182
236
|
|
183
237
|
|
184
|
-
|
185
|
-
def __init__(self) -> None:
|
186
|
-
self.num_neighbors: dict[tuple[int, int], int] = {}
|
187
|
-
self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
|
188
|
-
self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
|
189
|
-
self.cluster_evolution = pd.DataFrame()
|
190
|
-
self.square_start = 0
|
191
|
-
self.cluster_start = 0
|
192
|
-
self.max_square_size = 0
|
193
|
-
self.visited_tiles: set[tuple[int, int]] = set()
|
194
|
-
self.square_evolution = pd.DataFrame()
|
195
|
-
self.square_x: Optional[int] = None
|
196
|
-
self.square_y: Optional[int] = None
|
197
|
-
|
198
|
-
|
199
|
-
def compute_tile_evolution(
|
200
|
-
tile_visits_accessor: TileVisitAccessor, config: Config
|
201
|
-
) -> None:
|
238
|
+
def compute_tile_evolution(tile_state: TileState, config: Config) -> None:
|
202
239
|
for zoom in config.explorer_zoom_levels:
|
203
240
|
_compute_cluster_evolution(
|
204
|
-
|
205
|
-
|
241
|
+
tile_state["tile_history"][zoom],
|
242
|
+
tile_state["evolution_state"][zoom],
|
206
243
|
zoom,
|
207
244
|
)
|
208
245
|
_compute_square_history(
|
209
|
-
|
210
|
-
|
246
|
+
tile_state["tile_history"][zoom],
|
247
|
+
tile_state["evolution_state"][zoom],
|
211
248
|
zoom,
|
212
249
|
)
|
213
250
|
|
214
|
-
tile_visits_accessor.save()
|
215
|
-
|
216
251
|
|
217
252
|
def _compute_cluster_evolution(
|
218
253
|
tiles: pd.DataFrame, s: TileEvolutionState, zoom: int
|
@@ -12,6 +12,8 @@ import pandas as pd
|
|
12
12
|
from PIL import Image
|
13
13
|
from PIL import ImageDraw
|
14
14
|
|
15
|
+
from ...explorer.grid_file import make_grid_file_geojson
|
16
|
+
from ...explorer.grid_file import make_grid_points
|
15
17
|
from geo_activity_playground.core.activities import ActivityMeta
|
16
18
|
from geo_activity_playground.core.activities import ActivityRepository
|
17
19
|
from geo_activity_playground.core.activities import make_geojson_color_line
|
@@ -61,12 +63,32 @@ class ActivityController:
|
|
61
63
|
|
62
64
|
new_tiles = {
|
63
65
|
zoom: sum(
|
64
|
-
self._tile_visit_accessor.
|
66
|
+
self._tile_visit_accessor.tile_state["tile_history"][zoom][
|
67
|
+
"activity_id"
|
68
|
+
]
|
65
69
|
== activity["id"]
|
66
70
|
)
|
67
|
-
for zoom in
|
71
|
+
for zoom in sorted(self._config.explorer_zoom_levels)
|
68
72
|
}
|
69
73
|
|
74
|
+
new_tiles_geojson = {}
|
75
|
+
for zoom in sorted(self._config.explorer_zoom_levels):
|
76
|
+
new_tiles = self._tile_visit_accessor.tile_state["tile_history"][zoom].loc[
|
77
|
+
self._tile_visit_accessor.tile_state["tile_history"][zoom][
|
78
|
+
"activity_id"
|
79
|
+
]
|
80
|
+
== activity["id"]
|
81
|
+
]
|
82
|
+
if len(new_tiles):
|
83
|
+
points = make_grid_points(
|
84
|
+
(
|
85
|
+
(row["tile_x"], row["tile_y"])
|
86
|
+
for index, row in new_tiles.iterrows()
|
87
|
+
),
|
88
|
+
zoom,
|
89
|
+
)
|
90
|
+
new_tiles_geojson[zoom] = make_grid_file_geojson(points)
|
91
|
+
|
70
92
|
result = {
|
71
93
|
"activity": activity,
|
72
94
|
"line_json": line_json,
|
@@ -79,6 +101,7 @@ class ActivityController:
|
|
79
101
|
"date": activity["start"].date(),
|
80
102
|
"time": activity["start"].time(),
|
81
103
|
"new_tiles": new_tiles,
|
104
|
+
"new_tiles_geojson": new_tiles_geojson,
|
82
105
|
}
|
83
106
|
if (
|
84
107
|
heart_zones := _extract_heart_rate_zones(
|
@@ -100,7 +123,9 @@ class ActivityController:
|
|
100
123
|
time_series = privacy_zone.filter_time_series(time_series)
|
101
124
|
if len(time_series) == 0:
|
102
125
|
time_series = self._repository.get_time_series(id)
|
103
|
-
return make_sharepic(
|
126
|
+
return make_sharepic(
|
127
|
+
activity, time_series, self._config.sharepic_suppressed_fields
|
128
|
+
)
|
104
129
|
|
105
130
|
def render_day(self, year: int, month: int, day: int) -> dict:
|
106
131
|
meta = self._repository.meta
|
@@ -411,7 +436,11 @@ def pixels_in_bounds(bounds: PixelBounds) -> int:
|
|
411
436
|
return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
|
412
437
|
|
413
438
|
|
414
|
-
def make_sharepic(
|
439
|
+
def make_sharepic(
|
440
|
+
activity: ActivityMeta,
|
441
|
+
time_series: pd.DataFrame,
|
442
|
+
sharepic_suppressed_fields: list[str],
|
443
|
+
) -> bytes:
|
415
444
|
lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
|
416
445
|
|
417
446
|
geo_bounds = get_bounds(lat_lon_data)
|
@@ -448,19 +477,27 @@ def make_sharepic(activity: ActivityMeta, time_series: pd.DataFrame) -> bytes:
|
|
448
477
|
draw.line(yx, fill="red", width=4)
|
449
478
|
|
450
479
|
draw.rectangle([0, img.height - 70, img.width, img.height], fill=(0, 0, 0, 128))
|
451
|
-
|
452
|
-
|
453
|
-
f"{activity['
|
454
|
-
f"{activity['
|
455
|
-
f"
|
456
|
-
|
457
|
-
|
480
|
+
|
481
|
+
facts = {
|
482
|
+
"kind": f"{activity['kind']}",
|
483
|
+
"start": f"{activity['start'].date()}",
|
484
|
+
"equipment": f"{activity['equipment']}",
|
485
|
+
"distance_km": f"\n{activity['distance_km']:.1f} km",
|
486
|
+
"elapsed_time": re.sub(r"^0 days ", "", f"{activity['elapsed_time']}"),
|
487
|
+
}
|
488
|
+
|
458
489
|
if activity.get("calories", 0) and not pd.isna(activity["calories"]):
|
459
|
-
facts
|
490
|
+
facts["calories"] = f"{activity['calories']:.0f} kcal"
|
460
491
|
if activity.get("steps", 0) and not pd.isna(activity["steps"]):
|
461
|
-
facts
|
492
|
+
facts["steps"] = f"{activity['steps']:.0f} steps"
|
493
|
+
|
494
|
+
facts = {
|
495
|
+
key: value
|
496
|
+
for key, value in facts.items()
|
497
|
+
if not key in sharepic_suppressed_fields
|
498
|
+
}
|
462
499
|
|
463
|
-
draw.text((35, img.height - 70 + 10), " ".join(facts), font_size=20)
|
500
|
+
draw.text((35, img.height - 70 + 10), " ".join(facts.values()), font_size=20)
|
464
501
|
|
465
502
|
# img_array = np.array(img) / 255
|
466
503
|
|
@@ -111,11 +111,7 @@
|
|
111
111
|
</div>
|
112
112
|
|
113
113
|
{% if heartrate_time_plot is defined %}
|
114
|
-
<
|
115
|
-
<div class="col">
|
116
|
-
<h2>Heart rate</h2>
|
117
|
-
</div>
|
118
|
-
</div>
|
114
|
+
<h2>Heart rate</h2>
|
119
115
|
|
120
116
|
<div class="row mb-3">
|
121
117
|
<div class="col-md-4">
|
@@ -133,13 +129,45 @@
|
|
133
129
|
</div>
|
134
130
|
{% endif %}
|
135
131
|
|
136
|
-
<
|
137
|
-
|
138
|
-
|
132
|
+
<h2>Share picture</h2>
|
133
|
+
|
134
|
+
<p><img src="{{ url_for('.sharepic', id=activity.id) }}" /></p>
|
135
|
+
|
136
|
+
<p>Not happy with the displayed data? <a href="{{ url_for('settings.sharepic') }}">Change share picture
|
137
|
+
settings</a>.</p>
|
138
|
+
|
139
|
+
{% if new_tiles_geojson %}
|
140
|
+
<h2>New explorer tiles</h2>
|
141
|
+
<p>With this activity you have explored new explorer tiles. The following maps show the new tiles on the respective zoom
|
142
|
+
levels.</p>
|
143
|
+
<script>
|
144
|
+
function add_map(id, geojson) {
|
145
|
+
let map = L.map(`map-${id}`, {
|
146
|
+
fullscreenControl: true
|
147
|
+
})
|
148
|
+
L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
|
149
|
+
maxZoom: 19,
|
150
|
+
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
151
|
+
}).addTo(map)
|
152
|
+
|
153
|
+
let geojson_layer = L.geoJSON(geojson).addTo(map)
|
154
|
+
map.fitBounds(geojson_layer.getBounds())
|
155
|
+
return map
|
156
|
+
}
|
157
|
+
</script>
|
139
158
|
|
140
|
-
|
159
|
+
<div class="row mb-3">
|
160
|
+
{% for zoom, geojson in new_tiles_geojson.items() %}
|
161
|
+
<div class="col-md-6">
|
162
|
+
<h3>Zoom {{ zoom }}</h3>
|
163
|
+
<div id="map-{{ zoom }}" style="height: 300px; width: 100%;"></div>
|
164
|
+
<script>
|
165
|
+
let map{{ zoom }} = add_map("{{ zoom }}", {{ geojson | safe }})
|
166
|
+
</script>
|
141
167
|
</div>
|
168
|
+
{% endfor %}
|
142
169
|
</div>
|
170
|
+
{% endif %}
|
143
171
|
|
144
172
|
{% if similar_activites|length > 0 %}
|
145
173
|
<div class="row mb-3">
|