geo-activity-playground 0.28.0__py3-none-any.whl → 0.29.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.
Files changed (35) hide show
  1. geo_activity_playground/core/activities.py +3 -6
  2. geo_activity_playground/core/config.py +3 -0
  3. geo_activity_playground/core/paths.py +10 -0
  4. geo_activity_playground/core/tasks.py +5 -4
  5. geo_activity_playground/core/time_conversion.py +1 -1
  6. geo_activity_playground/explorer/tile_visits.py +43 -9
  7. geo_activity_playground/importers/activity_parsers.py +28 -17
  8. geo_activity_playground/importers/csv_parser.py +1 -2
  9. geo_activity_playground/importers/directory.py +2 -1
  10. geo_activity_playground/webui/activity/controller.py +22 -1
  11. geo_activity_playground/webui/activity/templates/activity/show.html.j2 +33 -0
  12. geo_activity_playground/webui/app.py +10 -20
  13. geo_activity_playground/webui/authenticator.py +0 -3
  14. geo_activity_playground/webui/entry_controller.py +8 -4
  15. geo_activity_playground/webui/explorer/controller.py +3 -2
  16. geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +2 -0
  17. geo_activity_playground/webui/heatmap/heatmap_controller.py +1 -0
  18. geo_activity_playground/webui/plot_util.py +9 -0
  19. geo_activity_playground/webui/search/blueprint.py +20 -0
  20. geo_activity_playground/webui/settings/blueprint.py +69 -0
  21. geo_activity_playground/webui/settings/controller.py +4 -3
  22. geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +33 -0
  23. geo_activity_playground/webui/settings/templates/settings/index.html.j2 +9 -0
  24. geo_activity_playground/webui/square_planner/controller.py +2 -0
  25. geo_activity_playground/webui/summary/blueprint.py +3 -2
  26. geo_activity_playground/webui/summary/controller.py +20 -13
  27. geo_activity_playground/webui/templates/home.html.j2 +1 -1
  28. geo_activity_playground/webui/templates/page.html.j2 +56 -28
  29. {geo_activity_playground-0.28.0.dist-info → geo_activity_playground-0.29.1.dist-info}/METADATA +3 -4
  30. {geo_activity_playground-0.28.0.dist-info → geo_activity_playground-0.29.1.dist-info}/RECORD +34 -32
  31. geo_activity_playground/webui/search_controller.py +0 -19
  32. /geo_activity_playground/webui/{templates/search.html.j2 → search/templates/search/index.html.j2} +0 -0
  33. {geo_activity_playground-0.28.0.dist-info → geo_activity_playground-0.29.1.dist-info}/LICENSE +0 -0
  34. {geo_activity_playground-0.28.0.dist-info → geo_activity_playground-0.29.1.dist-info}/WHEEL +0 -0
  35. {geo_activity_playground-0.28.0.dist-info → geo_activity_playground-0.29.1.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@ import datetime
2
2
  import functools
3
3
  import logging
4
4
  import pickle
5
+ from typing import Any
5
6
  from typing import Iterator
6
7
  from typing import Optional
7
8
  from typing import TypedDict
@@ -103,7 +104,7 @@ def build_activity_meta() -> None:
103
104
 
104
105
  class ActivityRepository:
105
106
  def __init__(self) -> None:
106
- self.meta = None
107
+ self.meta = pd.DataFrame()
107
108
 
108
109
  def __len__(self) -> int:
109
110
  return len(self.meta)
@@ -116,10 +117,6 @@ class ActivityRepository:
116
117
  if activity_id in self.meta["id"]:
117
118
  return True
118
119
 
119
- for activity_meta in self._loose_activities:
120
- if activity_meta["id"] == activity_id:
121
- return True
122
-
123
120
  return False
124
121
 
125
122
  def last_activity_date(self) -> Optional[datetime.datetime]:
@@ -198,7 +195,7 @@ def make_geojson_color_line(time_series: pd.DataFrame) -> str:
198
195
  return geojson.dumps(feature_collection)
199
196
 
200
197
 
201
- def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, str]:
198
+ def make_speed_color_bar(time_series: pd.DataFrame) -> dict[str, Any]:
202
199
  speed_without_na = time_series["speed"].dropna()
203
200
  low = min(speed_without_na)
204
201
  high = max(speed_without_na)
@@ -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]
@@ -52,6 +54,7 @@ class ConfigAccessor:
52
54
  return self._config
53
55
 
54
56
  def save(self) -> None:
57
+ print(self._config)
55
58
  with open(new_config_file(), "w") as f:
56
59
  json.dump(
57
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
- temp_location = path.with_suffix(".tmp")
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:
@@ -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)
@@ -2,7 +2,7 @@ import numpy as np
2
2
  import pandas as pd
3
3
 
4
4
 
5
- def convert_to_datetime_ns(date) -> np.datetime64:
5
+ def convert_to_datetime_ns(date) -> np.datetime64 | pd.Series:
6
6
  if isinstance(date, pd.Series):
7
7
  ts = pd.to_datetime(date)
8
8
  ts = ts.dt.tz_localize(None)
@@ -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
@@ -58,7 +59,7 @@ class TileEvolutionState:
58
59
  class TileState(TypedDict):
59
60
  tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
60
61
  tile_history: dict[int, pd.DataFrame]
61
- activities_per_tile: dict[int, set[int]]
62
+ activities_per_tile: dict[int, dict[tuple[int, int], set[int]]]
62
63
  processed_activities: set[int]
63
64
  evolution_state: dict[int, TileEvolutionState]
64
65
  version: int
@@ -79,11 +80,12 @@ class TileVisitAccessor:
79
80
  self.tile_state = make_tile_state()
80
81
  # TODO: Reset work tracker
81
82
 
83
+ def reset(self) -> None:
84
+ self.tile_state = make_tile_state()
85
+
82
86
  def save(self) -> None:
83
- tmp_path = self.PATH.with_suffix(".tmp")
84
- with open(tmp_path, "wb") as f:
87
+ with atomic_open(self.PATH, "wb") as f:
85
88
  pickle.dump(self.tile_state, f)
86
- tmp_path.rename(self.PATH)
87
89
 
88
90
 
89
91
  def make_defaultdict_dict():
@@ -106,20 +108,52 @@ def make_tile_state() -> TileState:
106
108
  return tile_state
107
109
 
108
110
 
111
+ def _consistency_check(
112
+ repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
113
+ ) -> bool:
114
+ present_activity_ids = set(repository.get_activity_ids())
115
+
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
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
133
+
134
+ return True
135
+
136
+
109
137
  def compute_tile_visits_new(
110
138
  repository: ActivityRepository, tile_visit_accessor: TileVisitAccessor
111
139
  ) -> None:
112
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()
146
+
113
147
  for activity_id in tqdm(
114
- work_tracker.filter(repository.get_activity_ids()), desc="Tile visits (new)"
148
+ work_tracker.filter(repository.get_activity_ids()), desc="Tile visits"
115
149
  ):
116
- do_tile_stuff(repository, tile_visit_accessor.tile_state, activity_id)
150
+ _process_activity(repository, tile_visit_accessor.tile_state, activity_id)
117
151
  work_tracker.mark_done(activity_id)
118
152
  tile_visit_accessor.save()
119
153
  work_tracker.close()
120
154
 
121
155
 
122
- def do_tile_stuff(
156
+ def _process_activity(
123
157
  repository: ActivityRepository, tile_state: TileState, activity_id: int
124
158
  ) -> None:
125
159
  activity = repository.get_activity_by_id(activity_id)
@@ -131,7 +165,7 @@ def do_tile_stuff(
131
165
  for zoom in reversed(range(20)):
132
166
  activities_per_tile = tile_state["activities_per_tile"][zoom]
133
167
 
134
- new_tile_history_soa = {
168
+ new_tile_history_soa: dict[str, list] = {
135
169
  "activity_id": [],
136
170
  "time": [],
137
171
  "tile_x": [],
@@ -145,7 +179,7 @@ def do_tile_stuff(
145
179
  zip(activity_tiles["tile_x"], activity_tiles["tile_y"]),
146
180
  ):
147
181
  if activity["consider_for_achievements"]:
148
- if tile not in activities_per_tile:
182
+ if tile not in tile_state["tile_visits"][zoom]:
149
183
  new_tile_history_soa["activity_id"].append(activity_id)
150
184
  new_tile_history_soa["time"].append(time)
151
185
  new_tile_history_soa["tile_x"].append(tile[0])
@@ -3,10 +3,10 @@ import gzip
3
3
  import logging
4
4
  import pathlib
5
5
  import xml
6
+ from collections.abc import Iterator
6
7
 
7
8
  import charset_normalizer
8
9
  import dateutil.parser
9
- import fitdecode
10
10
  import fitdecode.exceptions
11
11
  import gpxpy
12
12
  import pandas as pd
@@ -246,26 +246,37 @@ def read_kml_activity(path: pathlib.Path, opener) -> pd.DataFrame:
246
246
  with opener(path, "rb") as f:
247
247
  kml_dict = xmltodict.parse(f)
248
248
  doc = kml_dict["kml"]["Document"]
249
- keypoint_folder = doc["Folder"]
250
- placemark = keypoint_folder["Placemark"]
251
- track = placemark["gx:Track"]
252
249
  rows = []
253
- for when, where in zip(track["when"], track["gx:coord"]):
254
- time = dateutil.parser.parse(when)
255
- time = convert_to_datetime_ns(time)
256
- parts = where.split(" ")
257
- if len(parts) == 2:
258
- lon, lat = parts
259
- alt = None
260
- if len(parts) == 3:
261
- lon, lat, alt = parts
262
- row = {"time": time, "latitude": float(lat), "longitude": float(lon)}
263
- if alt is not None:
264
- row["altitude"] = float(alt)
265
- rows.append(row)
250
+ for keypoint_folder in _list_or_scalar(doc["Folder"]):
251
+ for placemark in _list_or_scalar(keypoint_folder["Placemark"]):
252
+ for track in _list_or_scalar(placemark.get("gx:Track", [])):
253
+ for when, where in zip(track["when"], track["gx:coord"]):
254
+ time = dateutil.parser.parse(when)
255
+ time = convert_to_datetime_ns(time)
256
+ parts = where.split(" ")
257
+ if len(parts) == 2:
258
+ lon, lat = parts
259
+ alt = None
260
+ if len(parts) == 3:
261
+ lon, lat, alt = parts
262
+ row = {
263
+ "time": time,
264
+ "latitude": float(lat),
265
+ "longitude": float(lon),
266
+ }
267
+ if alt is not None:
268
+ row["altitude"] = float(alt)
269
+ rows.append(row)
266
270
  return pd.DataFrame(rows)
267
271
 
268
272
 
273
+ def _list_or_scalar(thing) -> Iterator:
274
+ if isinstance(thing, list):
275
+ yield from thing
276
+ else:
277
+ yield thing
278
+
279
+
269
280
  def read_simra_activity(path: pathlib.Path, opener) -> pd.DataFrame:
270
281
  data = pd.read_csv(path, header=1)
271
282
  data["time"] = data["timeStamp"].apply(
@@ -20,9 +20,8 @@ This module implements a "recursive descent parser" that parses this grammar.
20
20
 
21
21
  def parse_csv(text: str) -> list[list]:
22
22
  text = text.strip() + "\n"
23
- result = {}
24
23
  index = 0
25
- result = []
24
+ result: list[list] = []
26
25
  while index < len(text):
27
26
  line, index = _parse_line(text, index)
28
27
  result.append(line)
@@ -126,11 +126,12 @@ def _cache_single_file(path: pathlib.Path) -> Optional[tuple[pathlib.Path, str]]
126
126
  raise
127
127
 
128
128
  if len(timeseries) == 0:
129
- return
129
+ return None
130
130
 
131
131
  timeseries.to_parquet(timeseries_path)
132
132
  with open(file_metadata_path, "wb") as f:
133
133
  pickle.dump(activity_meta_from_file, f)
134
+ return None
134
135
 
135
136
 
136
137
  def get_file_hash(path: pathlib.Path) -> 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
@@ -66,9 +68,27 @@ class ActivityController:
66
68
  ]
67
69
  == activity["id"]
68
70
  )
69
- for zoom in [14, 17]
71
+ for zoom in sorted(self._config.explorer_zoom_levels)
70
72
  }
71
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
+
72
92
  result = {
73
93
  "activity": activity,
74
94
  "line_json": line_json,
@@ -81,6 +101,7 @@ class ActivityController:
81
101
  "date": activity["start"].date(),
82
102
  "time": activity["start"].time(),
83
103
  "new_tiles": new_tiles,
104
+ "new_tiles_geojson": new_tiles_geojson,
84
105
  }
85
106
  if (
86
107
  heart_zones := _extract_heart_rate_zones(
@@ -136,6 +136,39 @@
136
136
  <p>Not happy with the displayed data? <a href="{{ url_for('settings.sharepic') }}">Change share picture
137
137
  settings</a>.</p>
138
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: '&copy; <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>
158
+
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>
167
+ </div>
168
+ {% endfor %}
169
+ </div>
170
+ {% endif %}
171
+
139
172
  {% if similar_activites|length > 0 %}
140
173
  <div class="row mb-3">
141
174
  <div class="col">
@@ -5,7 +5,6 @@ import secrets
5
5
 
6
6
  from flask import Flask
7
7
  from flask import render_template
8
- from flask import request
9
8
 
10
9
  from ..core.activities import ActivityRepository
11
10
  from ..explorer.tile_visits import TileVisitAccessor
@@ -16,31 +15,20 @@ from .entry_controller import EntryController
16
15
  from .equipment.blueprint import make_equipment_blueprint
17
16
  from .explorer.blueprint import make_explorer_blueprint
18
17
  from .heatmap.blueprint import make_heatmap_blueprint
19
- from .search_controller import SearchController
18
+ from .search.blueprint import make_search_blueprint
20
19
  from .square_planner.blueprint import make_square_planner_blueprint
21
20
  from .summary.blueprint import make_summary_blueprint
22
21
  from .tile.blueprint import make_tile_blueprint
23
22
  from .upload.blueprint import make_upload_blueprint
23
+ from geo_activity_playground.core.config import Config
24
24
  from geo_activity_playground.core.config import ConfigAccessor
25
25
  from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
26
26
  from geo_activity_playground.webui.authenticator import Authenticator
27
27
  from geo_activity_playground.webui.settings.blueprint import make_settings_blueprint
28
28
 
29
29
 
30
- def route_search(app: Flask, repository: ActivityRepository) -> None:
31
- search_controller = SearchController(repository)
32
-
33
- @app.route("/search", methods=["POST"])
34
- def search():
35
- form_input = request.form
36
- return render_template(
37
- "search.html.j2",
38
- **search_controller.render_search_results(form_input["name"])
39
- )
40
-
41
-
42
- def route_start(app: Flask, repository: ActivityRepository) -> None:
43
- entry_controller = EntryController(repository)
30
+ def route_start(app: Flask, repository: ActivityRepository, config: Config) -> None:
31
+ entry_controller = EntryController(repository, config)
44
32
 
45
33
  @app.route("/")
46
34
  def index():
@@ -66,7 +54,6 @@ def web_ui_main(
66
54
  host: str,
67
55
  port: int,
68
56
  ) -> None:
69
-
70
57
  repository.reload()
71
58
 
72
59
  app = Flask(__name__)
@@ -75,8 +62,7 @@ def web_ui_main(
75
62
 
76
63
  authenticator = Authenticator(config_accessor())
77
64
 
78
- route_search(app, repository)
79
- route_start(app, repository)
65
+ route_start(app, repository, config_accessor())
80
66
 
81
67
  app.register_blueprint(make_auth_blueprint(authenticator), url_prefix="/auth")
82
68
 
@@ -111,7 +97,11 @@ def web_ui_main(
111
97
  url_prefix="/square-planner",
112
98
  )
113
99
  app.register_blueprint(
114
- make_summary_blueprint(repository),
100
+ make_search_blueprint(repository),
101
+ url_prefix="/search",
102
+ )
103
+ app.register_blueprint(
104
+ make_summary_blueprint(repository, config_accessor()),
115
105
  url_prefix="/summary",
116
106
  )
117
107
  app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
@@ -14,9 +14,6 @@ class Authenticator:
14
14
  self._config = config
15
15
 
16
16
  def is_authenticated(self) -> bool:
17
- print(
18
- f"Password={self._config.upload_password}, Session={session.get('is_authenticated', False)}"
19
- )
20
17
  return not self._config.upload_password or session.get(
21
18
  "is_authenticated", False
22
19
  )
@@ -6,18 +6,22 @@ import pandas as pd
6
6
 
7
7
  from geo_activity_playground.core.activities import ActivityRepository
8
8
  from geo_activity_playground.core.activities import make_geojson_from_time_series
9
+ from geo_activity_playground.core.config import Config
10
+ from geo_activity_playground.webui.plot_util import make_kind_scale
9
11
 
10
12
 
11
13
  class EntryController:
12
- def __init__(self, repository: ActivityRepository) -> None:
14
+ def __init__(self, repository: ActivityRepository, config: Config) -> None:
13
15
  self._repository = repository
16
+ self._config = config
14
17
 
15
18
  def render(self) -> dict:
19
+ kind_scale = make_kind_scale(self._repository.meta, self._config)
16
20
  result = {"latest_activities": []}
17
21
 
18
22
  if len(self._repository):
19
23
  result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
20
- self._repository.meta
24
+ self._repository.meta, kind_scale
21
25
  )
22
26
 
23
27
  for activity in itertools.islice(
@@ -33,7 +37,7 @@ class EntryController:
33
37
  return result
34
38
 
35
39
 
36
- def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
40
+ def distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
37
41
  before_30_days = pd.to_datetime(
38
42
  datetime.datetime.now() - datetime.timedelta(days=31)
39
43
  )
@@ -48,7 +52,7 @@ def distance_last_30_days_meta_plot(meta: pd.DataFrame) -> str:
48
52
  .encode(
49
53
  alt.X("yearmonthdate(start)", title="Date"),
50
54
  alt.Y("sum(distance_km)", title="Distance / km"),
51
- alt.Color("kind", scale=alt.Scale(scheme="category10"), title="Kind"),
55
+ alt.Color("kind", scale=kind_scale, title="Kind"),
52
56
  [
53
57
  alt.Tooltip("yearmonthdate(start)", title="Date"),
54
58
  alt.Tooltip("kind", title="Kind"),
@@ -95,7 +95,7 @@ class ExplorerController:
95
95
  x2, y2 = compute_tile(south, east, zoom)
96
96
  tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
97
97
 
98
- tile_histories = self._tile_visit_accessor.histories
98
+ tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
99
99
  tiles = tile_histories[zoom]
100
100
  points = get_border_tiles(tiles, zoom, tile_bounds)
101
101
  if suffix == "geojson":
@@ -108,7 +108,7 @@ class ExplorerController:
108
108
  x2, y2 = compute_tile(south, east, zoom)
109
109
  tile_bounds = Bounds(x1, y1, x2 + 2, y2 + 2)
110
110
 
111
- tile_visits = self._tile_visit_accessor.visits
111
+ tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
112
112
  tiles = tile_visits[zoom]
113
113
  points = make_grid_points(
114
114
  (tile for tile in tiles.keys() if tile_bounds.contains(*tile)), zoom
@@ -161,6 +161,7 @@ def get_three_color_tiles(
161
161
  "last_visit": tile_data["last_time"].date().isoformat(),
162
162
  "num_visits": len(tile_data["activity_ids"]),
163
163
  "square": False,
164
+ "tile": f"({zoom}, {tile[0]}, {tile[1]})",
164
165
  }
165
166
 
166
167
  # Mark biggest square.
@@ -48,6 +48,8 @@
48
48
  function onEachFeature(feature, layer) {
49
49
  if (feature.properties && feature.properties.first_visit) {
50
50
  let lines = [
51
+ `<dt>Tile</dt>`,
52
+ `<dd>${feature.properties.tile}</dd>`,
51
53
  `<dt>First visit</dt>`,
52
54
  `<dd>${feature.properties.first_visit}</br><a href=/activity/${feature.properties.first_activity_id}>${feature.properties.first_activity_name}</a></dd>`,
53
55
  `<dt>Last visit</dt>`,
@@ -123,6 +123,7 @@ class HeatmapController:
123
123
  tile_counts += aim
124
124
  tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
125
125
  np.save(tmp_path, tile_counts)
126
+ tile_count_cache_path.unlink(missing_ok=True)
126
127
  tmp_path.rename(tile_count_cache_path)
127
128
  return tile_counts
128
129
 
@@ -0,0 +1,9 @@
1
+ import altair as alt
2
+ import pandas as pd
3
+
4
+ from geo_activity_playground.core.config import Config
5
+
6
+
7
+ def make_kind_scale(meta: pd.DataFrame, config: Config) -> alt.Scale:
8
+ kinds = sorted(meta["kind"].unique())
9
+ return alt.Scale(domain=kinds, scheme=config.color_scheme_for_kind)
@@ -0,0 +1,20 @@
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
+
8
+
9
+ def make_search_blueprint(repository: ActivityRepository) -> Blueprint:
10
+ blueprint = Blueprint("search", __name__, template_folder="templates")
11
+
12
+ @blueprint.route("/", methods=["POST"])
13
+ def index():
14
+ activities = []
15
+ for _, row in repository.meta.iterrows():
16
+ if request.form["name"] in row["name"]:
17
+ activities.append(row)
18
+ return render_template("search/index.html.j2", activities=activities)
19
+
20
+ return blueprint
@@ -19,6 +19,8 @@ def int_or_none(s: str) -> Optional[int]:
19
19
  return int(s)
20
20
  except ValueError as e:
21
21
  flash(f"Cannot parse integer from {s}: {e}", category="danger")
22
+ else:
23
+ return None
22
24
 
23
25
 
24
26
  def make_settings_blueprint(
@@ -42,6 +44,73 @@ def make_settings_blueprint(
42
44
  **settings_controller.render_admin_password(),
43
45
  )
44
46
 
47
+ @blueprint.route("/color-schemes", methods=["GET", "POST"])
48
+ @needs_authentication(authenticator)
49
+ def color_schemes():
50
+ if request.method == "POST":
51
+ config_accessor().color_scheme_for_counts = request.form[
52
+ "color_scheme_for_counts"
53
+ ]
54
+ config_accessor().color_scheme_for_kind = request.form[
55
+ "color_scheme_for_kind"
56
+ ]
57
+ config_accessor.save()
58
+ flash("Updated color schemes.", category="success")
59
+ return render_template(
60
+ "settings/color-schemes.html.j2",
61
+ color_scheme_for_counts=config_accessor().color_scheme_for_counts,
62
+ color_scheme_for_counts_avail=[
63
+ "viridis",
64
+ "magma",
65
+ "inferno",
66
+ "plasma",
67
+ "cividis",
68
+ "turbo",
69
+ "bluegreen",
70
+ "bluepurple",
71
+ "goldgreen",
72
+ "goldorange",
73
+ "goldred",
74
+ "greenblue",
75
+ "orangered",
76
+ "purplebluegreen",
77
+ "purpleblue",
78
+ "purplered",
79
+ "redpurple",
80
+ "yellowgreenblue",
81
+ "yellowgreen",
82
+ "yelloworangebrown",
83
+ "yelloworangered",
84
+ "darkblue",
85
+ "darkgold",
86
+ "darkgreen",
87
+ "darkmulti",
88
+ "darkred",
89
+ "lightgreyred",
90
+ "lightgreyteal",
91
+ "lightmulti",
92
+ "lightorange",
93
+ "lighttealblue",
94
+ ],
95
+ color_scheme_for_kind=config_accessor().color_scheme_for_kind,
96
+ color_scheme_for_kind_avail=[
97
+ "accent",
98
+ "category10",
99
+ "category20",
100
+ "category20b",
101
+ "category20c",
102
+ "dark2",
103
+ "paired",
104
+ "pastel1",
105
+ "pastel2",
106
+ "set1",
107
+ "set2",
108
+ "set3",
109
+ "tableau10",
110
+ "tableau20",
111
+ ],
112
+ )
113
+
45
114
  @blueprint.route("/equipment-offsets", methods=["GET", "POST"])
46
115
  @needs_authentication(authenticator)
47
116
  def equipment_offsets():
@@ -1,6 +1,7 @@
1
1
  import json
2
2
  import re
3
3
  import urllib.parse
4
+ from typing import Any
4
5
  from typing import Optional
5
6
 
6
7
  from flask import flash
@@ -70,7 +71,7 @@ class SettingsController:
70
71
  flash("Updated equipment offsets.", category="success")
71
72
 
72
73
  def render_heart_rate(self) -> dict:
73
- result = {
74
+ result: dict[str, Any] = {
74
75
  "birth_year": self._config_accessor().birth_year,
75
76
  "heart_rate_resting": self._config_accessor().heart_rate_resting,
76
77
  "heart_rate_maximum": self._config_accessor().heart_rate_maximum,
@@ -90,7 +91,7 @@ class SettingsController:
90
91
  heart_rate_maximum: Optional[int],
91
92
  ) -> None:
92
93
  self._config_accessor().birth_year = birth_year
93
- self._config_accessor().heart_rate_resting = heart_rate_resting
94
+ self._config_accessor().heart_rate_resting = heart_rate_resting or 0
94
95
  self._config_accessor().heart_rate_maximum = heart_rate_maximum
95
96
  self._config_accessor.save()
96
97
  flash("Updated heart rate data.", category="success")
@@ -251,7 +252,7 @@ class SettingsController:
251
252
  return f"https://www.strava.com/oauth/authorize?{arg_string}"
252
253
 
253
254
  def save_strava_code(self, code: str) -> None:
254
- self._config_accessor().strava_client_id = self._strava_client_id
255
+ self._config_accessor().strava_client_id = int(self._strava_client_id)
255
256
  self._config_accessor().strava_client_secret = self._strava_client_secret
256
257
  self._config_accessor().strava_client_code = code
257
258
  self._config_accessor.save()
@@ -0,0 +1,33 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="mb-3">Color Schemes for Plots</h1>
6
+
7
+ <p>Don't like color schemes in the plots? Have a look at the <a href="https://vega.github.io/vega/docs/schemes/"
8
+ target="_blank">colors schemes of Vega</a> and pick one that you like.</p>
9
+
10
+ <form method="POST">
11
+ <div class="mb-3">
12
+ <label class="form-label">Color scheme for activity kinds</label>
13
+ <select class="form-select" aria-label="Color scheme for activity kinds" name="color_scheme_for_kind">
14
+ {% for cs in color_scheme_for_kind_avail %}
15
+ <option {% if cs==color_scheme_for_kind %} selected {% endif %}>{{ cs }}</option>
16
+ {% endfor %}
17
+ </select>
18
+ </div>
19
+
20
+ <div class="mb-3">
21
+ <label class="form-label">Color scheme for heatmaps</label>
22
+ <select class="form-select" aria-label="Color scheme for heatmaps" name="color_scheme_for_counts">
23
+ {% for cs in color_scheme_for_counts_avail %}
24
+ <option {% if cs==color_scheme_for_counts %} selected {% endif %}>{{ cs }}</option>
25
+ {% endfor %}
26
+ </select>
27
+ </div>
28
+
29
+ <button type="submit" class="btn btn-primary">Save</button>
30
+ </form>
31
+
32
+
33
+ {% endblock %}
@@ -20,6 +20,15 @@
20
20
  </div>
21
21
  </div>
22
22
  </div>
23
+ <div class="col">
24
+ <div class="card">
25
+ <div class="card-body">
26
+ <h5 class="card-title">Color schemes</h5>
27
+ <p class="card-text">Don't like the colors in the plots?</p>
28
+ <a href="{{ url_for('.color_schemes') }}" class="btn btn-primary">Set up color schemes</a>
29
+ </div>
30
+ </div>
31
+ </div>
23
32
  <div class="col">
24
33
  <div class="card">
25
34
  <div class="card-body">
@@ -79,6 +79,8 @@ class SquarePlannerController:
79
79
  return make_grid_file_geojson(points)
80
80
  elif suffix == "gpx":
81
81
  return make_grid_file_gpx(points)
82
+ else:
83
+ raise RuntimeError(f"Unsupported suffix {suffix}.")
82
84
 
83
85
  def _get_explored_tiles(self, zoom: int) -> set[tuple[int, int]]:
84
86
  return set(self._tile_visits[zoom].keys())
@@ -3,10 +3,11 @@ from flask import render_template
3
3
 
4
4
  from ...core.activities import ActivityRepository
5
5
  from .controller import SummaryController
6
+ from geo_activity_playground.core.config import Config
6
7
 
7
8
 
8
- def make_summary_blueprint(repository: ActivityRepository) -> Blueprint:
9
- summary_controller = SummaryController(repository)
9
+ def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
10
+ summary_controller = SummaryController(repository, config)
10
11
  blueprint = Blueprint("summary", __name__, template_folder="templates")
11
12
 
12
13
  @blueprint.route("/")
@@ -8,14 +8,18 @@ import pandas as pd
8
8
 
9
9
  from geo_activity_playground.core.activities import ActivityRepository
10
10
  from geo_activity_playground.core.activities import make_geojson_from_time_series
11
+ from geo_activity_playground.core.config import Config
12
+ from geo_activity_playground.webui.plot_util import make_kind_scale
11
13
 
12
14
 
13
15
  class SummaryController:
14
- def __init__(self, repository: ActivityRepository) -> None:
16
+ def __init__(self, repository: ActivityRepository, config: Config) -> None:
15
17
  self._repository = repository
18
+ self._config = config
16
19
 
17
20
  @functools.cache
18
21
  def render(self) -> dict:
22
+ kind_scale = make_kind_scale(self._repository.meta, self._config)
19
23
  df = embellished_activities(self._repository.meta)
20
24
  df = df.loc[df["consider_for_achievements"]]
21
25
 
@@ -27,14 +31,14 @@ class SummaryController:
27
31
  )
28
32
 
29
33
  return {
30
- "plot_distance_heatmap": plot_distance_heatmap(df),
31
- "plot_monthly_distance": plot_monthly_distance(df),
32
- "plot_yearly_distance": plot_yearly_distance(year_kind_total),
34
+ "plot_distance_heatmap": plot_distance_heatmap(df, self._config),
35
+ "plot_monthly_distance": plot_monthly_distance(df, kind_scale),
36
+ "plot_yearly_distance": plot_yearly_distance(year_kind_total, kind_scale),
33
37
  "plot_year_cumulative": plot_year_cumulative(df),
34
38
  "tabulate_year_kind_mean": tabulate_year_kind_mean(df)
35
39
  .reset_index()
36
40
  .to_dict(orient="split"),
37
- "plot_weekly_distance": plot_weekly_distance(df),
41
+ "plot_weekly_distance": plot_weekly_distance(df, kind_scale),
38
42
  "nominations": [
39
43
  (
40
44
  self._repository.get_activity_by_id(activity_id),
@@ -108,7 +112,7 @@ def embellished_activities(meta: pd.DataFrame) -> pd.DataFrame:
108
112
  return df
109
113
 
110
114
 
111
- def plot_distance_heatmap(meta: pd.DataFrame) -> str:
115
+ def plot_distance_heatmap(meta: pd.DataFrame, config: Config) -> str:
112
116
  return (
113
117
  alt.Chart(
114
118
  meta.loc[
@@ -129,7 +133,10 @@ def plot_distance_heatmap(meta: pd.DataFrame) -> str:
129
133
  scale=alt.Scale(reverse=True),
130
134
  title="Year and month",
131
135
  ),
132
- alt.Color("sum(distance_km)", scale=alt.Scale(scheme="viridis")),
136
+ alt.Color(
137
+ "sum(distance_km)",
138
+ scale=alt.Scale(scheme=config.color_scheme_for_counts),
139
+ ),
133
140
  [
134
141
  alt.Tooltip("yearmonthdate(start)", title="Date"),
135
142
  alt.Tooltip(
@@ -142,7 +149,7 @@ def plot_distance_heatmap(meta: pd.DataFrame) -> str:
142
149
  )
143
150
 
144
151
 
145
- def plot_monthly_distance(meta: pd.DataFrame) -> str:
152
+ def plot_monthly_distance(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
146
153
  return (
147
154
  alt.Chart(
148
155
  meta.loc[
@@ -159,7 +166,7 @@ def plot_monthly_distance(meta: pd.DataFrame) -> str:
159
166
  .encode(
160
167
  alt.X("month(start)", title="Month"),
161
168
  alt.Y("sum(distance_km)", title="Distance / km"),
162
- alt.Color("kind", scale=alt.Scale(scheme="category10"), title="Kind"),
169
+ alt.Color("kind", scale=kind_scale, title="Kind"),
163
170
  alt.Column("year(start):O", title="Year"),
164
171
  )
165
172
  .resolve_axis(x="independent")
@@ -167,14 +174,14 @@ def plot_monthly_distance(meta: pd.DataFrame) -> str:
167
174
  )
168
175
 
169
176
 
170
- def plot_yearly_distance(year_kind_total: pd.DataFrame) -> str:
177
+ def plot_yearly_distance(year_kind_total: pd.DataFrame, kind_scale: alt.Scale) -> str:
171
178
  return (
172
179
  alt.Chart(year_kind_total, title="Total Distance per Year")
173
180
  .mark_bar()
174
181
  .encode(
175
182
  alt.X("year:O", title="Year"),
176
183
  alt.Y("distance_km", title="Distance / km"),
177
- alt.Color("kind", title="Kind"),
184
+ alt.Color("kind", scale=kind_scale, title="Kind"),
178
185
  [
179
186
  alt.Tooltip("year:O", title="Year"),
180
187
  alt.Tooltip("kind", title="Kind"),
@@ -231,7 +238,7 @@ def tabulate_year_kind_mean(df: pd.DataFrame) -> pd.DataFrame:
231
238
  return year_kind_mean_distance
232
239
 
233
240
 
234
- def plot_weekly_distance(df: pd.DataFrame) -> str:
241
+ def plot_weekly_distance(df: pd.DataFrame, kind_scale: alt.Scale) -> str:
235
242
  week_kind_total_distance = (
236
243
  df[["year", "week", "kind", "distance_km"]]
237
244
  .groupby(["year", "week", "kind"])
@@ -261,7 +268,7 @@ def plot_weekly_distance(df: pd.DataFrame) -> str:
261
268
  .encode(
262
269
  alt.X("year_week", title="Year and Week"),
263
270
  alt.Y("distance_km", title="Distance / km"),
264
- alt.Color("kind", title="Kind"),
271
+ alt.Color("kind", scale=kind_scale, title="Kind"),
265
272
  [
266
273
  alt.Tooltip("year_week", title="Year and Week"),
267
274
  alt.Tooltip("kind", title="Kind"),
@@ -12,7 +12,7 @@
12
12
  </div>
13
13
  <div class="col-md-3">
14
14
  <h2>Search activities</h2>
15
- <form method="post" action="/search">
15
+ <form method="post" action="{{ url_for('search.index') }}">
16
16
  <input type="search" name="name" />
17
17
  <input type="submit" class="button" />
18
18
  </form>
@@ -62,42 +62,70 @@
62
62
  <ul class="navbar-nav me-auto mb-2 mb-lg-0">
63
63
 
64
64
  {% if num_activities > 0 %}
65
- <li class="nav-item">
66
- <a class="nav-link" aria-current="page" href="{{ url_for('summary.index') }}">Summary</a>
67
- </li>
68
- <li class="nav-item">
69
- <a class="nav-link" aria-current="page" href="{{ url_for('calendar.index') }}">Calendar</a>
65
+ <li class="nav-item dropdown">
66
+ <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
67
+ aria-expanded="false">
68
+ Activities
69
+ </a>
70
+ <ul class="dropdown-menu">
71
+ <li><a class="dropdown-item" href="{{ url_for('calendar.index') }}">Calendar</a></li>
72
+ {# <li><a class="dropdown-item" href="{{ url_for('search.index') }}">Search</a></li> #}
73
+ </ul>
70
74
  </li>
71
- <li class="nav-item">
72
- <a class="nav-link" aria-current="page"
73
- href="{{ url_for('explorer.map', zoom=14) }}">Explorer</a>
75
+
76
+ <li class="nav-item dropdown">
77
+ <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
78
+ aria-expanded="false">
79
+ Statistics
80
+ </a>
81
+ <ul class="dropdown-menu">
82
+ <li><a class="dropdown-item" href="{{ url_for('summary.index') }}">Summary
83
+ Statistics</a>
84
+ </li>
85
+ <li><a class="dropdown-item" href="{{ url_for('eddington.index') }}">Eddington
86
+ Number</a>
87
+ </li>
88
+ <li><a class="dropdown-item" href="{{ url_for('equipment.index') }}">Equipment</a></li>
89
+ </ul>
74
90
  </li>
75
- <li class="nav-item">
76
- <a class="nav-link" aria-current="page"
77
- href="{{ url_for('explorer.map', zoom=17) }}">Squadratinhos</a>
91
+
92
+ <li class="nav-item dropdown">
93
+ <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
94
+ aria-expanded="false">
95
+ Explorer
96
+ </a>
97
+ <ul class="dropdown-menu">
98
+ <li><a class="dropdown-item" href="{{ url_for('explorer.map', zoom=14) }}">Explorer
99
+ Tiles (Zoom 14)</a></li>
100
+ <li><a class="dropdown-item" href="{{ url_for('explorer.map', zoom=17) }}">Squadratinhos
101
+ (Zoom 17)</a></li>
102
+ </ul>
78
103
  </li>
104
+
79
105
  <li class="nav-item">
80
106
  <a class="nav-link" aria-current="page" href="{{ url_for('heatmap.index') }}">Heatmap</a>
81
107
  </li>
82
- <li class="nav-item">
83
- <a class="nav-link" aria-current="page"
84
- href="{{ url_for('eddington.index') }}">Eddington</a>
85
- </li>
86
- <li class="nav-item">
87
- <a class="nav-link" aria-current="page"
88
- href="{{ url_for('equipment.index') }}">Equipment</a>
89
- </li>
90
108
  {% endif %}
91
- <li class="nav-item">
92
- <a class="nav-link" aria-current="page" href="{{ url_for('upload.index') }}">Upload</a>
93
- </li>
94
- <li class="nav-item">
95
- <a class="nav-link" aria-current="page" href="{{ url_for('upload.reload') }}">Refresh</a>
96
- </li>
97
- <li class="nav-item">
98
- <a class="nav-link" aria-current="page" href="{{ url_for('settings.index') }}">Settings</a>
99
- </li>
100
109
 
110
+ <li class="nav-item dropdown">
111
+ <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
112
+ aria-expanded="false">
113
+ Admin
114
+ </a>
115
+ <ul class="dropdown-menu">
116
+ <li><a class="dropdown-item" href="{{ url_for('upload.index') }}">Upload Activities</a>
117
+ </li>
118
+ <li><a class="dropdown-item" href="{{ url_for('upload.reload') }}">Scan New
119
+ Activities</a>
120
+ </li>
121
+
122
+ <li>
123
+ <hr class="dropdown-divider">
124
+ </li>
125
+
126
+ <li><a class="dropdown-item" href="{{ url_for('settings.index') }}">Settings</a></li>
127
+ </ul>
128
+ </li>
101
129
 
102
130
  <li class="nav-item dropdown">
103
131
  <button
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.28.0
3
+ Version: 0.29.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
7
7
  Author-email: mu@martin-ueding.de
8
- Requires-Python: >=3.9,<3.13
8
+ Requires-Python: >=3.10,<3.13
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.9
12
11
  Classifier: Programming Language :: Python :: 3.10
13
12
  Classifier: Programming Language :: Python :: 3.11
14
13
  Classifier: Programming Language :: Python :: 3.12
@@ -30,7 +29,7 @@ Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
30
29
  Requires-Dist: requests (>=2.28.1,<3.0.0)
31
30
  Requires-Dist: scipy (>=1.8.1,<2.0.0)
32
31
  Requires-Dist: shapely (>=2.0.5,<3.0.0)
33
- Requires-Dist: stravalib (>=1.3.3,<2.0.0)
32
+ Requires-Dist: stravalib (>=2.0,<3.0)
34
33
  Requires-Dist: tcxreader (>=0.4.5,<0.5.0)
35
34
  Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
36
35
  Requires-Dist: tqdm (>=4.64.0,<5.0.0)
@@ -1,28 +1,28 @@
1
1
  geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  geo_activity_playground/__main__.py,sha256=MBZn9K1m3PofiPNTtpsSTVCyB_Gz95TjVP-nb9v_-JE,3989
3
3
  geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- geo_activity_playground/core/activities.py,sha256=yDjf3aVOC3LvnAwAvQUl_e7S3NXq_vKgccq0hGuFdLI,6609
5
- geo_activity_playground/core/config.py,sha256=uys4O2OfXC6yNAsgbgm72WGIhy903zomr_SOwk4GNmA,4498
4
+ geo_activity_playground/core/activities.py,sha256=HcxgLsM2tE48eqZh1t1QBk9O7vxCV9MaL2QRp0loXu0,6509
5
+ geo_activity_playground/core/config.py,sha256=DsjZhiq2BZs94rjd7zgt_KTZIjVfxfntiEQZhgq8NeA,4617
6
6
  geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
7
7
  geo_activity_playground/core/enrichment.py,sha256=CwZhW-svgPAYbdx3n9kvKlTgcsiCaeuJfSRCC4JxX6g,7411
8
8
  geo_activity_playground/core/heart_rate.py,sha256=IwMt58TpjOYqpAxtsj07zP2ttpN_J3GZeiv-qGhYyJc,1598
9
9
  geo_activity_playground/core/heatmap.py,sha256=bRLQHzmTEsQbX8XWeg85x_lRGk272UoYRiCnoxZ5da0,4189
10
- geo_activity_playground/core/paths.py,sha256=AiYUJv46my_FGYbHZmSs5ZrqeE65GNdWEMmXZgunZrk,2150
10
+ geo_activity_playground/core/paths.py,sha256=BZYuIg1LVHjuWLKB0Iz6Cevlq-XSalpCes_ClFuXa0s,2410
11
11
  geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
12
12
  geo_activity_playground/core/similarity.py,sha256=Jo8jRViuORCxdIGvyaflgsQhwu9S_jn10a450FRL18A,3159
13
- geo_activity_playground/core/tasks.py,sha256=f3C2gCJiiqMS1eRQ-Hp5hxRbkwl60rotrYfzKsWdpSU,2937
13
+ geo_activity_playground/core/tasks.py,sha256=aMDBWJqp6ek2ao6G6Xa8GOSZbcQqXoWL74SGRowRPIk,2942
14
14
  geo_activity_playground/core/test_tiles.py,sha256=zce1FxNfsSpOQt66jMehdQRVoNdl-oiFydx6iVBHZXM,764
15
15
  geo_activity_playground/core/test_time_conversion.py,sha256=Sh6nZA3uCTOdZTZa3yOijtR0m74QtZu2mcWXsDNnyQI,984
16
16
  geo_activity_playground/core/tiles.py,sha256=KpzD-h3kNzZ2ieLt6f2xHilSF3lHyfaEXPnrGvlIAz0,3379
17
- geo_activity_playground/core/time_conversion.py,sha256=9J6aTlqJhWvsknQkoECNL-CIG-8BKs6ZatJJ9XJnTsg,367
17
+ geo_activity_playground/core/time_conversion.py,sha256=x5mXG6Y4GtdX7CBmwucGNSWBp9JQJDbZ7u0JkdUY1Vs,379
18
18
  geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  geo_activity_playground/explorer/grid_file.py,sha256=k6j6KBEk2a2BY-onE8SV5TJsERGGyOrlY4as__meWpA,3304
20
- geo_activity_playground/explorer/tile_visits.py,sha256=IqkaGPq-xrmR81Ze31VL1v4NUxQ2YGQeQawvsjYe25s,12776
20
+ geo_activity_playground/explorer/tile_visits.py,sha256=CSHAjgzKWe1iB-zvaqgsR5Z_lFycpWqUfxnPCAWvYaU,14173
21
21
  geo_activity_playground/explorer/video.py,sha256=ROAmV9shfJyqTgnXVD41KFORiwnRgVpEWenIq4hMCRM,4389
22
22
  geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
- geo_activity_playground/importers/activity_parsers.py,sha256=m2SpvGlTZ8F3gG6YB24_ZFrlOAbtqbfWi-GIYspeUco,10593
24
- geo_activity_playground/importers/csv_parser.py,sha256=7x0U4MyKNCsVFnXU6US8FQ28X9PTIoQS8U_hzF5l4So,2147
25
- geo_activity_playground/importers/directory.py,sha256=lJVSAZpq8NAIjVQuCiVxPSNt9m_rIY0yJlOOMB-ZTG0,5746
23
+ geo_activity_playground/importers/activity_parsers.py,sha256=XNQs0ziqAcVqIoiLAX5Ndmhb6v__29XdjUPvNvc7oBI,11082
24
+ geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
25
+ geo_activity_playground/importers/directory.py,sha256=IPnr1xk3beznmPVjfWL6AQiEKA4WN5EDIfnzrM0Tzlc,5767
26
26
  geo_activity_playground/importers/strava_api.py,sha256=pgWZp-cWOLkvlDE85lTiEKM8BCZYzOlAAdKoa2F7c6o,7780
27
27
  geo_activity_playground/importers/strava_checkout.py,sha256=N-uGTkhBJMC7cPYjRRXHOSLwpK3wc6aaSrY2RQfSitA,9419
28
28
  geo_activity_playground/importers/test_csv_parser.py,sha256=LXqva7GuSAfXYE2zZQrg-69lCtfy5MxLSq6BRwL_VyI,1191
@@ -31,15 +31,15 @@ geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrI
31
31
  geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  geo_activity_playground/webui/activity/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  geo_activity_playground/webui/activity/blueprint.py,sha256=upQzZa5sKApj_Fmu6PziFDboi7SBL5Zsi-tNSSNPlEE,1759
34
- geo_activity_playground/webui/activity/controller.py,sha256=YaWHXIfSYSeybEFY706s_m4uHbfJ9FHxqIYtqwR5sGQ,17955
34
+ geo_activity_playground/webui/activity/controller.py,sha256=PJIZ7pFqpUyWDrintnnlW6Hxj7DbdPswARozws6TE30,18861
35
35
  geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=r3qKl9uTzOko4R-ZzyYAZt1j61JSevYP4g0Yi06HHPg,2702
36
36
  geo_activity_playground/webui/activity/templates/activity/lines.html.j2,sha256=5gB1aDjRgi_RventenRfC10_FtMT4ch_VuWvA9AMlBY,1121
37
37
  geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=RDLEt6ip8_ngmdLgaC5jg92Dk-F2umGwKkd8cWmvVko,2400
38
- geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=6k3_MC_AR_sSWVYvH53E5o6YFtE9HZD62NlPf90Rj8Q,5509
39
- geo_activity_playground/webui/app.py,sha256=Vn8G6wf2YSKc_n7JBkulry51Gl57_UjrAW0TftXLQgM,4456
38
+ geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=W77M1S7RQOGY3Vg9LRT5mFnefuMWUKKU1Vd-ZKxUoKg,6552
39
+ geo_activity_playground/webui/app.py,sha256=foON49jw8klBJ3GF70DDjgz-_KRDPFvC31_mScaqDXk,4255
40
40
  geo_activity_playground/webui/auth/blueprint.py,sha256=Lx-ZvMnfHLC1CMre1xPQI3k_pCtQoZvgRhtmafULzoE,812
41
41
  geo_activity_playground/webui/auth/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
42
- geo_activity_playground/webui/authenticator.py,sha256=jXwWJFuinSE-7sQxdi7s7WO9ifvERmeXAM5-hsAPmwQ,1546
42
+ geo_activity_playground/webui/authenticator.py,sha256=k278OEVuOfAmTGT4F2X4pqSTwwkK_FA87EIhAeysEqc,1416
43
43
  geo_activity_playground/webui/calendar/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
44
44
  geo_activity_playground/webui/calendar/blueprint.py,sha256=rlnhgU2DWAcdLMRq7m77NzrM_aDyp4s3kuuQHuzjHhg,782
45
45
  geo_activity_playground/webui/calendar/controller.py,sha256=QpSAkR2s1sbLSu6P_fNNTccgGglOzEH2PIv1XwKxeVY,2778
@@ -49,26 +49,29 @@ geo_activity_playground/webui/eddington/__init__.py,sha256=47DEQpj8HBSa-_TImW-5J
49
49
  geo_activity_playground/webui/eddington/blueprint.py,sha256=evIvueLfDWVTxJ9pRguqmZ9-Pybd2WmBRst_-7vX2QA,551
50
50
  geo_activity_playground/webui/eddington/controller.py,sha256=ly7JSkSS79kO4CL_xugB62uRuuWKVqOjbN-pheelv94,2910
51
51
  geo_activity_playground/webui/eddington/templates/eddington/index.html.j2,sha256=XHKeUymQMS5x00PLOVlg-nSRCz_jHB2pvD8QunULWJ4,1839
52
- geo_activity_playground/webui/entry_controller.py,sha256=n9v4MriyL8kDR91LE9eeqc2tAvxyzFgoNMMXpr0qh4g,1906
52
+ geo_activity_playground/webui/entry_controller.py,sha256=kTEToBtR1T4l30cV3HkCK3KO2hVYfb22BSgcWLdLEXQ,2164
53
53
  geo_activity_playground/webui/equipment/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  geo_activity_playground/webui/equipment/blueprint.py,sha256=_NIhRJuJNbXpEd_nEPo01AqnUqPgo1vawFn7E3yoeng,636
55
55
  geo_activity_playground/webui/equipment/controller.py,sha256=Sx9i2RCK7m4W6FgpDfRMewcH64VBQfUhHJdTSCwMqOU,4079
56
56
  geo_activity_playground/webui/equipment/templates/equipment/index.html.j2,sha256=FEfxB4XwVYELAOdjVlSlprjJH_kLmE-pNWEEXdPqc6I,1778
57
57
  geo_activity_playground/webui/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  geo_activity_playground/webui/explorer/blueprint.py,sha256=EKnBs8llqT6Wy1uac18dF2epp3TebF9p3iGlSbj6Vl0,2337
59
- geo_activity_playground/webui/explorer/controller.py,sha256=-iMsuB05JgUYKahJxiOPZeNVmzr_Uw2psHwmaFiptkI,11614
60
- geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=cm9pWY0vB84DtkTH-LBvSzfLU1FnmxQ2ECyw3Bl7dTo,6945
59
+ geo_activity_playground/webui/explorer/controller.py,sha256=pIzWh0TpLJgKQZlS325-QT7nA1q9ms7fRqQIp24PNfo,11705
60
+ geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=u2htecx-XwINRiINHFN6EZDaRXVtiape1OCUZexTBU8,7049
61
61
  geo_activity_playground/webui/heatmap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
62
  geo_activity_playground/webui/heatmap/blueprint.py,sha256=bjQu-HL3QN5UpJ6tHOifhcLGlPr_hIKvaRu78md4JqM,1470
63
- geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=Bn7FO_ciMEXghSwz2tEQ8fJsbK8AXaHTPNgnGuQ7an8,7353
63
+ geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=Q17Ay8hbU5ZlUiz2a9S-ULWrnNGWQHvTTV3kDY5FhNc,7411
64
64
  geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2,sha256=YLeu6P4djl8G4qAXR6DhetseqrbOodN7aN4coocknc4,1875
65
- geo_activity_playground/webui/search_controller.py,sha256=PzMf7b8tiJKZIZoPvQ9A2hOrzoKV9cS3jq05w2fK94c,532
66
- geo_activity_playground/webui/settings/blueprint.py,sha256=boKeKkG4bbSA7HvgpaqlUXzrcCnOPwys6KWw-Dp5sAE,5416
67
- geo_activity_playground/webui/settings/controller.py,sha256=v14oKvI1QzWn0g41Sm4NA_g9q4SUVZ_bO9SUOZuPAaY,9121
65
+ geo_activity_playground/webui/plot_util.py,sha256=pTTQoqOCkLVjkgOit7mbry28kMruZIL8amZozSzEpxQ,283
66
+ geo_activity_playground/webui/search/blueprint.py,sha256=b3TCIplY60MWE2_VsKHuoV1LAgNwd_T5ft5t0CKALFI,642
67
+ geo_activity_playground/webui/search/templates/search/index.html.j2,sha256=FvNRoDfUlSzXjM_tqZY_fDhuhUDgbPaY73q56gdvF1A,1130
68
+ geo_activity_playground/webui/settings/blueprint.py,sha256=O5GqbbzETOYq5JESoI-F2br8hnk6xHyJzqWTQDnbNEE,7623
69
+ geo_activity_playground/webui/settings/controller.py,sha256=MIZVBfoGNvmJnB_ECV_x5eH2i6gDZvkWSQ4PcSKyLKs,9170
68
70
  geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
71
+ geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2,sha256=CaFbYkkU1yGTOlAzGq97u3tVgS79RIo7PEmiVjuZiBc,1226
69
72
  geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2,sha256=ltaYwFe8S8Mi72ddmIp1vwqlu8MEXXjBGfbpN2WBTC4,1728
70
73
  geo_activity_playground/webui/settings/templates/settings/heart-rate.html.j2,sha256=UPT3MegRgSeff36lhCo0l3ZwhqNSIg5gM6h2s32GkCY,4255
71
- geo_activity_playground/webui/settings/templates/settings/index.html.j2,sha256=rXKq2v42J0eW5OSWx7VU6EFT9jZkbzKTW600Y0OqBtY,4035
74
+ geo_activity_playground/webui/settings/templates/settings/index.html.j2,sha256=-ZSlR6htaeMMOf4ISUSzWPu5BUhYODuAmcWPN0ZoBno,4443
72
75
  geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2,sha256=IdUfXon1Pu8zX3NirKb28ypshLHOvZRpz2T4bJrzrak,1067
73
76
  geo_activity_playground/webui/settings/templates/settings/metadata-extraction.html.j2,sha256=Ppa8O-zRJznbeCsF4YQj37_HM9nOW8fyTi66jvWvHmA,2285
74
77
  geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2,sha256=7BxFvCaVJOEqbImyK5vxCmhh-NGSFaRa9ARhqjZeYJ0,3093
@@ -76,7 +79,7 @@ geo_activity_playground/webui/settings/templates/settings/sharepic.html.j2,sha25
76
79
  geo_activity_playground/webui/settings/templates/settings/strava.html.j2,sha256=FrXgT-m1PgvsQWo9kMKpk8QenKeifSDBCZFqKgsHRxQ,1827
77
80
  geo_activity_playground/webui/square_planner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
81
  geo_activity_playground/webui/square_planner/blueprint.py,sha256=r2VkSM547chX85g6c1BQ8NC-tkdqGdYp-2ZALBiiDTc,1320
79
- geo_activity_playground/webui/square_planner/controller.py,sha256=vV7Wd4Kt1YPOI-UVoqLtIqUb3nke9PJQwWynlA31Lto,3483
82
+ geo_activity_playground/webui/square_planner/controller.py,sha256=ML6ftOyr3tTh7D4DBcRP76CvkyTxqI5QWgMeG9wC8FE,3561
80
83
  geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2,sha256=aIB0ql5qW4HXfp0ENksYYOk9vTgBitwyHJX5W7bqkeY,6512
81
84
  geo_activity_playground/webui/static/android-chrome-192x192.png,sha256=yxZgo8Jw4hrgOgrn3tvi9G0AXWGFD29kjCuxC07WoT4,17610
82
85
  geo_activity_playground/webui/static/android-chrome-384x384.png,sha256=bgeqAdyvDZBMch7rVi3qSawf0Zr4Go0EG8Ws_B8NApY,49297
@@ -91,12 +94,11 @@ geo_activity_playground/webui/static/mstile-150x150.png,sha256=j1ANUQJ1Xi1DR2sGq
91
94
  geo_activity_playground/webui/static/safari-pinned-tab.svg,sha256=OzoEVGY0igWRXM1NiM3SRKugdICBN7aB_XuxaC3Mu9Q,8371
92
95
  geo_activity_playground/webui/static/site.webmanifest,sha256=4vYxdPMpwTdB8EmOvHkkYcjZ8Yrci3pOwwY3o_VwACA,440
93
96
  geo_activity_playground/webui/summary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
94
- geo_activity_playground/webui/summary/blueprint.py,sha256=kzQ6MDOycQKfDcVoEUmL7HYHJA_gu8DlzVHwO37-_jA,514
95
- geo_activity_playground/webui/summary/controller.py,sha256=ZOrwfrKjpc8hecUYImBvesKXZi06obfR1yhQkVTeWzw,8981
97
+ geo_activity_playground/webui/summary/blueprint.py,sha256=tfA2aPF19yKwkQOb5lPQBySoQYYhTn49Iuh0SYvsGP8,593
98
+ geo_activity_playground/webui/summary/controller.py,sha256=cWn5szA1o5Vjht0DyhRwBjmwqJryrLcmm4FUdmVpUoo,9443
96
99
  geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=rsII1eMY-xNugh8A9SecnEcDZqkEOWYIfiHAGroQYuM,4442
97
- geo_activity_playground/webui/templates/home.html.j2,sha256=FjEwr9kt_3qu_evIHpa7F_oGAINN8W2Z1T_j56ugJ5c,2406
98
- geo_activity_playground/webui/templates/page.html.j2,sha256=8DkXaXx_cc9FvfI6eo9z3fbeNDRcvJC2NXcHXs8ytSU,9588
99
- geo_activity_playground/webui/templates/search.html.j2,sha256=FvNRoDfUlSzXjM_tqZY_fDhuhUDgbPaY73q56gdvF1A,1130
100
+ geo_activity_playground/webui/templates/home.html.j2,sha256=eIPNyLHhUNVTITDbn6nR82-ZJA5Dp4SY41cZTjymZDU,2428
101
+ geo_activity_playground/webui/templates/page.html.j2,sha256=HN7s4i4kR3laMFkToktjwmTQiTHdrY3nqKcCVO9nKdA,11088
100
102
  geo_activity_playground/webui/tile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
103
  geo_activity_playground/webui/tile/blueprint.py,sha256=cK0o2Z3BrLycgF9zw0F8s9qF-JaYDbF5Gog-GXDtUZ8,943
102
104
  geo_activity_playground/webui/tile/controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
@@ -105,8 +107,8 @@ geo_activity_playground/webui/upload/blueprint.py,sha256=xX9scEmleN_dL03jfhWRh5y
105
107
  geo_activity_playground/webui/upload/controller.py,sha256=disRtrlvPiqsIFt9UaAokgtRtXCvosg7bXkAnN_qaxk,4102
106
108
  geo_activity_playground/webui/upload/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
107
109
  geo_activity_playground/webui/upload/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
108
- geo_activity_playground-0.28.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
109
- geo_activity_playground-0.28.0.dist-info/METADATA,sha256=0ZheRNfyebSkkcFUph4A3Wo3EheMmw0c0U2fi9C-MWc,1665
110
- geo_activity_playground-0.28.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
111
- geo_activity_playground-0.28.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
112
- geo_activity_playground-0.28.0.dist-info/RECORD,,
110
+ geo_activity_playground-0.29.1.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
111
+ geo_activity_playground-0.29.1.dist-info/METADATA,sha256=O9NvI6x-QQvVw7XklVLmq2pkz2UFTTg9PnLwAK6KhWY,1612
112
+ geo_activity_playground-0.29.1.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
113
+ geo_activity_playground-0.29.1.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
114
+ geo_activity_playground-0.29.1.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- import logging
2
-
3
- from ..core.activities import ActivityRepository
4
-
5
- logger = logging.getLogger(__name__)
6
-
7
-
8
- class SearchController:
9
- def __init__(self, repository: ActivityRepository) -> None:
10
- self._repository = repository
11
-
12
- def render_search_results(self, name: str) -> dict:
13
- logger.info(f"Searching for {name=}")
14
- activities = []
15
- for _, row in self._repository.meta.iterrows():
16
- if name in row["name"]:
17
- activities.append(row)
18
-
19
- return {"activities": activities}