geo-activity-playground 0.30.0__py3-none-any.whl → 0.32.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.
Files changed (23) hide show
  1. geo_activity_playground/core/activities.py +11 -2
  2. geo_activity_playground/core/config.py +1 -0
  3. geo_activity_playground/core/heatmap.py +63 -0
  4. geo_activity_playground/core/paths.py +3 -0
  5. geo_activity_playground/importers/directory.py +6 -2
  6. geo_activity_playground/webui/activity/blueprint.py +54 -2
  7. geo_activity_playground/webui/activity/controller.py +47 -31
  8. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +3 -3
  9. geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +42 -0
  10. geo_activity_playground/webui/activity/templates/activity/name.html.j2 +2 -2
  11. geo_activity_playground/webui/activity/templates/activity/show.html.j2 +7 -5
  12. geo_activity_playground/webui/app.py +14 -3
  13. geo_activity_playground/webui/heatmap/heatmap_controller.py +8 -6
  14. geo_activity_playground/webui/search/templates/search/index.html.j2 +2 -2
  15. geo_activity_playground/webui/summary/controller.py +1 -1
  16. geo_activity_playground/webui/summary/templates/summary/index.html.j2 +1 -1
  17. geo_activity_playground/webui/templates/home.html.j2 +2 -2
  18. geo_activity_playground/webui/upload/controller.py +1 -2
  19. {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/METADATA +1 -1
  20. {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/RECORD +23 -22
  21. {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/WHEEL +1 -1
  22. {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/LICENSE +0 -0
  23. {geo_activity_playground-0.30.0.dist-info → geo_activity_playground-0.32.0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import datetime
2
2
  import functools
3
+ import json
3
4
  import logging
4
5
  import pickle
5
6
  from typing import Any
@@ -16,6 +17,7 @@ from tqdm import tqdm
16
17
  from geo_activity_playground.core.paths import activities_file
17
18
  from geo_activity_playground.core.paths import activity_enriched_meta_dir
18
19
  from geo_activity_playground.core.paths import activity_enriched_time_series_dir
20
+ from geo_activity_playground.core.paths import activity_meta_override_dir
19
21
 
20
22
  logger = logging.getLogger(__name__)
21
23
 
@@ -83,7 +85,12 @@ def build_activity_meta() -> None:
83
85
  rows = []
84
86
  for new_id in tqdm(new_ids, desc="Register new activities"):
85
87
  with open(activity_enriched_meta_dir() / f"{new_id}.pickle", "rb") as f:
86
- rows.append(pickle.load(f))
88
+ data = pickle.load(f)
89
+ override_file = activity_meta_override_dir() / f"{new_id}.json"
90
+ if override_file.exists():
91
+ with open(override_file) as f:
92
+ data.update(json.load(f))
93
+ rows.append(data)
87
94
 
88
95
  if rows:
89
96
  new_shard = pd.DataFrame(rows)
@@ -140,7 +147,6 @@ class ActivityRepository:
140
147
  if not dropna or not pd.isna(row["start"]):
141
148
  yield row
142
149
 
143
- @functools.lru_cache()
144
150
  def get_activity_by_id(self, id: int) -> ActivityMeta:
145
151
  activity = self.meta.loc[id]
146
152
  assert isinstance(activity["name"], str), activity["name"]
@@ -158,6 +164,9 @@ class ActivityRepository:
158
164
 
159
165
  return df
160
166
 
167
+ def save(self) -> None:
168
+ self._meta.to_parquet(activities_file())
169
+
161
170
 
162
171
  def make_geojson_from_time_series(time_series: pd.DataFrame) -> str:
163
172
  fc = geojson.FeatureCollection(
@@ -29,6 +29,7 @@ class Config:
29
29
  )
30
30
  heart_rate_resting: int = 0
31
31
  heart_rate_maximum: Optional[int] = None
32
+ ignore_suffixes: list[str] = dataclasses.field(default_factory=list)
32
33
  kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
33
34
  kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
34
35
  metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
@@ -142,6 +142,69 @@ def build_map_from_tiles(tile_bounds: TileBounds) -> np.ndarray:
142
142
  return background
143
143
 
144
144
 
145
+ def build_map_from_tiles_around_center(
146
+ center: tuple[float, float],
147
+ zoom: int,
148
+ target: tuple[int, int],
149
+ inner_target: tuple[int, int],
150
+ ) -> np.ndarray:
151
+ background = np.zeros((target[1], target[0], 3))
152
+
153
+ # We will work with the center point and have it in terms of tiles `t` and also in terms of pixels `p`. At the start we know that the tile center must be in the middle of the image.
154
+ t = np.array(center)
155
+ p = np.array([inner_target[0] / 2, inner_target[1] / 2])
156
+
157
+ # Shift both such that they are in the top-left corner of an even tile.
158
+ t_offset = np.array([center[0] % 1, center[1] % 1])
159
+ t -= t_offset
160
+ p -= t_offset * OSM_TILE_SIZE
161
+
162
+ # Shift until we have left the image.
163
+ shift = np.ceil(p / OSM_TILE_SIZE)
164
+ p -= shift * OSM_TILE_SIZE
165
+ t -= shift
166
+
167
+ num_tiles = np.ceil(np.array(target) / OSM_TILE_SIZE) + 1
168
+
169
+ for x in range(int(t[0]), int(t[0] + num_tiles[0])):
170
+ for y in range(int(t[1]), int(t[1]) + int(num_tiles[1])):
171
+ source_x_min = 0
172
+ source_y_min = 0
173
+ source_x_max = source_x_min + OSM_TILE_SIZE
174
+ source_y_max = source_y_min + OSM_TILE_SIZE
175
+
176
+ target_x_min = (x - int(t[0])) * OSM_TILE_SIZE + int(p[0])
177
+ target_y_min = (y - int(t[1])) * OSM_TILE_SIZE + int(p[1])
178
+ target_x_max = target_x_min + OSM_TILE_SIZE
179
+ target_y_max = target_y_min + OSM_TILE_SIZE
180
+
181
+ if target_x_min < 0:
182
+ source_x_min -= target_x_min
183
+ target_x_min = 0
184
+ if target_y_min < 0:
185
+ source_y_min -= target_y_min
186
+ target_y_min = 0
187
+ if target_x_max > target[0]:
188
+ a = target_x_max - target[0]
189
+ target_x_max -= a
190
+ source_x_max -= a
191
+ if target_y_max > target[1]:
192
+ a = target_y_max - target[1]
193
+ target_y_max -= a
194
+ source_y_max -= a
195
+
196
+ if source_x_max < 0 or source_y_max < 0:
197
+ continue
198
+
199
+ tile = np.array(get_tile(zoom, x, y)) / 255
200
+
201
+ background[target_y_min:target_y_max, target_x_min:target_x_max] = tile[
202
+ source_y_min:source_y_max, source_x_min:source_x_max, :3
203
+ ]
204
+
205
+ return background
206
+
207
+
145
208
  def convert_to_grayscale(image: np.ndarray) -> np.ndarray:
146
209
  image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
147
210
  image = np.dstack((image, image, image))
@@ -53,6 +53,8 @@ _strava_last_activity_date_path = _cache_dir / "strava-last-activity-date.json"
53
53
 
54
54
  _new_config_file = pathlib.Path("config.json")
55
55
 
56
+ _activity_meta_override_dir = pathlib.Path("Metadata Override")
57
+
56
58
 
57
59
  cache_dir = dir_wrapper(_cache_dir)
58
60
 
@@ -63,6 +65,7 @@ activity_enriched_meta_dir = dir_wrapper(_activity_enriched_meta_dir)
63
65
  activity_enriched_time_series_dir = dir_wrapper(_activity_enriched_time_series_dir)
64
66
  tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
65
67
  strava_api_dir = dir_wrapper(_strava_api_dir)
68
+ activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
66
69
 
67
70
  activities_file = file_wrapper(_activities_file)
68
71
  strava_dynamic_config_path = file_wrapper(_strava_dynamic_config_path)
@@ -10,6 +10,7 @@ from typing import Optional
10
10
  from tqdm import tqdm
11
11
 
12
12
  from geo_activity_playground.core.activities import ActivityMeta
13
+ from geo_activity_playground.core.config import Config
13
14
  from geo_activity_playground.core.paths import activity_extracted_dir
14
15
  from geo_activity_playground.core.paths import activity_extracted_meta_dir
15
16
  from geo_activity_playground.core.paths import activity_extracted_time_series_dir
@@ -24,13 +25,16 @@ ACTIVITY_DIR = pathlib.Path("Activities")
24
25
 
25
26
 
26
27
  def import_from_directory(
27
- metadata_extraction_regexes: list[str], num_processes: Optional[int]
28
+ metadata_extraction_regexes: list[str], num_processes: Optional[int], config: Config
28
29
  ) -> None:
29
30
 
30
31
  activity_paths = [
31
32
  path
32
33
  for path in ACTIVITY_DIR.rglob("*.*")
33
- if path.is_file() and path.suffixes and not path.stem.startswith(".")
34
+ if path.is_file()
35
+ and path.suffixes
36
+ and not path.stem.startswith(".")
37
+ and not path.suffix in config.ignore_suffixes
34
38
  ]
35
39
  work_tracker = WorkTracker(activity_extracted_dir() / "work-tracker-extract.pickle")
36
40
  new_activity_paths = work_tracker.filter(activity_paths)
@@ -1,21 +1,29 @@
1
+ import json
1
2
  import urllib.parse
2
3
  from collections.abc import Collection
3
4
 
4
5
  from flask import Blueprint
6
+ from flask import redirect
5
7
  from flask import render_template
8
+ from flask import request
6
9
  from flask import Response
10
+ from flask import url_for
7
11
 
8
12
  from ...core.activities import ActivityRepository
9
13
  from ...explorer.tile_visits import TileVisitAccessor
10
14
  from .controller import ActivityController
11
15
  from geo_activity_playground.core.config import Config
16
+ from geo_activity_playground.core.paths import activity_meta_override_dir
12
17
  from geo_activity_playground.core.privacy_zones import PrivacyZone
18
+ from geo_activity_playground.webui.authenticator import Authenticator
19
+ from geo_activity_playground.webui.authenticator import needs_authentication
13
20
 
14
21
 
15
22
  def make_activity_blueprint(
16
23
  repository: ActivityRepository,
17
24
  tile_visit_accessor: TileVisitAccessor,
18
25
  config: Config,
26
+ authenticator: Authenticator,
19
27
  ) -> Blueprint:
20
28
  blueprint = Blueprint("activity", __name__, template_folder="templates")
21
29
 
@@ -44,14 +52,58 @@ def make_activity_blueprint(
44
52
  def day(year: str, month: str, day: str):
45
53
  return render_template(
46
54
  "activity/day.html.j2",
47
- **activity_controller.render_day(int(year), int(month), int(day))
55
+ **activity_controller.render_day(int(year), int(month), int(day)),
48
56
  )
49
57
 
50
58
  @blueprint.route("/name/<name>")
51
59
  def name(name: str):
52
60
  return render_template(
53
61
  "activity/name.html.j2",
54
- **activity_controller.render_name(urllib.parse.unquote(name))
62
+ **activity_controller.render_name(urllib.parse.unquote(name)),
63
+ )
64
+
65
+ @blueprint.route("/edit/<id>", methods=["GET", "POST"])
66
+ @needs_authentication(authenticator)
67
+ def edit(id: str):
68
+ activity_id = int(id)
69
+ activity = repository.get_activity_by_id(activity_id)
70
+ override_file = activity_meta_override_dir() / f"{activity_id}.json"
71
+ if override_file.exists():
72
+ with open(override_file) as f:
73
+ override = json.load(f)
74
+ else:
75
+ override = {}
76
+
77
+ if request.method == "POST":
78
+ override = {}
79
+ if value := request.form.get("name"):
80
+ override["name"] = value
81
+ repository.meta.loc[activity_id, "name"] = value
82
+ if value := request.form.get("kind"):
83
+ override["kind"] = value
84
+ repository.meta.loc[activity_id, "kind"] = value
85
+ if value := request.form.get("equipment"):
86
+ override["equipment"] = value
87
+ repository.meta.loc[activity_id, "equipment"] = value
88
+ if value := request.form.get("commute"):
89
+ override["commute"] = True
90
+ repository.meta.loc[activity_id, "commute"] = True
91
+ if value := request.form.get("consider_for_achievements"):
92
+ override["consider_for_achievements"] = True
93
+ repository.meta.loc[activity_id, "consider_for_achievements"] = True
94
+
95
+ with open(override_file, "w") as f:
96
+ json.dump(override, f, ensure_ascii=False, indent=4, sort_keys=True)
97
+
98
+ repository.save()
99
+
100
+ return redirect(url_for(".show", id=activity_id))
101
+
102
+ return render_template(
103
+ "activity/edit.html.j2",
104
+ activity_id=activity_id,
105
+ activity=activity,
106
+ override=override,
55
107
  )
56
108
 
57
109
  return blueprint
@@ -23,9 +23,11 @@ from geo_activity_playground.core.config import Config
23
23
  from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
24
24
  from geo_activity_playground.core.heatmap import add_margin_to_geo_bounds
25
25
  from geo_activity_playground.core.heatmap import build_map_from_tiles
26
+ from geo_activity_playground.core.heatmap import build_map_from_tiles_around_center
26
27
  from geo_activity_playground.core.heatmap import GeoBounds
27
28
  from geo_activity_playground.core.heatmap import get_bounds
28
29
  from geo_activity_playground.core.heatmap import get_sensible_zoom_level
30
+ from geo_activity_playground.core.heatmap import OSM_MAX_ZOOM
29
31
  from geo_activity_playground.core.heatmap import OSM_TILE_SIZE
30
32
  from geo_activity_playground.core.heatmap import PixelBounds
31
33
  from geo_activity_playground.core.heatmap import TileBounds
@@ -459,42 +461,55 @@ def make_sharepic(
459
461
  time_series: pd.DataFrame,
460
462
  sharepic_suppressed_fields: list[str],
461
463
  ) -> bytes:
462
- lat_lon_data = np.array([time_series["latitude"], time_series["longitude"]]).T
464
+ tile_x = time_series["x"]
465
+ tile_y = time_series["y"]
466
+ tile_width = tile_x.max() - tile_x.min()
467
+ tile_height = tile_y.max() - tile_y.min()
468
+
469
+ target_width = 600
470
+ target_height = 600
471
+ footer_height = 100
472
+ target_map_height = target_height - footer_height
473
+
474
+ zoom = int(
475
+ min(
476
+ np.log2(target_width / tile_width / OSM_TILE_SIZE),
477
+ np.log2(target_map_height / tile_height / OSM_TILE_SIZE),
478
+ OSM_MAX_ZOOM,
479
+ )
480
+ )
463
481
 
464
- geo_bounds = get_bounds(lat_lon_data)
465
- geo_bounds = add_margin_to_geo_bounds(geo_bounds)
466
- tile_bounds = get_sensible_zoom_level(geo_bounds, (1500, 1500))
467
- tile_bounds = make_tile_bounds_square(tile_bounds)
468
- background = build_map_from_tiles(tile_bounds)
469
- # background = convert_to_grayscale(background)
482
+ tile_xz = tile_x * 2**zoom
483
+ tile_yz = tile_y * 2**zoom
470
484
 
471
- crop_mask = get_crop_mask(geo_bounds, tile_bounds)
472
- assert pixels_in_bounds(crop_mask) <= 10_000_000, crop_mask
485
+ tile_xz_center = (
486
+ (tile_xz.max() + tile_xz.min()) / 2,
487
+ (tile_yz.max() + tile_yz.min()) / 2,
488
+ )
473
489
 
474
- background = background[
475
- crop_mask.y_min : crop_mask.y_max,
476
- crop_mask.x_min : crop_mask.x_max,
477
- :,
478
- ]
490
+ background = build_map_from_tiles_around_center(
491
+ tile_xz_center,
492
+ zoom,
493
+ (target_width, target_height),
494
+ (target_width, target_map_height),
495
+ )
479
496
 
480
497
  img = Image.fromarray((background * 255).astype("uint8"), "RGB")
481
498
  draw = ImageDraw.Draw(img, mode="RGBA")
482
499
 
483
500
  for _, group in time_series.groupby("segment_id"):
484
- xs, ys = compute_tile_float(
485
- group["latitude"], group["longitude"], tile_bounds.zoom
486
- )
487
501
  yx = list(
488
- (
489
- int((x - tile_bounds.x_tile_min) * OSM_TILE_SIZE - crop_mask.x_min),
490
- int((y - tile_bounds.y_tile_min) * OSM_TILE_SIZE - crop_mask.y_min),
502
+ zip(
503
+ (tile_xz - tile_xz_center[0]) * OSM_TILE_SIZE + target_width / 2,
504
+ (tile_yz - tile_xz_center[1]) * OSM_TILE_SIZE + target_map_height / 2,
491
505
  )
492
- for x, y in zip(xs, ys)
493
506
  )
494
507
 
495
508
  draw.line(yx, fill="red", width=4)
496
509
 
497
- draw.rectangle([0, img.height - 70, img.width, img.height], fill=(0, 0, 0, 128))
510
+ draw.rectangle(
511
+ [0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
512
+ )
498
513
 
499
514
  facts = {
500
515
  "kind": f"{activity['kind']}",
@@ -515,19 +530,20 @@ def make_sharepic(
515
530
  if not key in sharepic_suppressed_fields
516
531
  }
517
532
 
518
- draw.text((35, img.height - 70 + 10), " ".join(facts.values()), font_size=20)
519
-
520
- # img_array = np.array(img) / 255
521
-
522
- # weight = np.dstack([img_array[:, :, 0]] * 3)
533
+ draw.text(
534
+ (35, img.height - footer_height + 10),
535
+ " ".join(facts.values()),
536
+ font_size=20,
537
+ )
523
538
 
524
- # background = (1 - weight) * background + img_array
525
- # background[background > 1.0] = 1.0
526
- # background[background < 0.0] = 0.0
539
+ draw.text(
540
+ (img.width - 250, img.height - 20),
541
+ "Map: © Open Street Map Contributors",
542
+ font_size=14,
543
+ )
527
544
 
528
545
  f = io.BytesIO()
529
546
  img.save(f, format="png")
530
- # pl.imsave(f, background, format="png")
531
547
  return bytes(f.getbuffer())
532
548
 
533
549
 
@@ -58,9 +58,9 @@
58
58
  <td><span style="color: {{ activity['color'] }};">█</span> <a
59
59
  href="{{ url_for('activity.show', id=activity.id) }}">{{
60
60
  activity.name }}</a></td>
61
- <td>{{ activity.start }}</td>
61
+ <td>{{ activity.start|dt }}</td>
62
62
  <td>{{ activity.distance_km | round(1) }}</td>
63
- <td>{{ activity.elapsed_time }}</td>
63
+ <td>{{ activity.elapsed_time|td }}</td>
64
64
  <td>{{ activity["equipment"] }}</td>
65
65
  <td>{{ activity["kind"] }}</td>
66
66
  </tr>
@@ -70,7 +70,7 @@
70
70
  <td><b>Total</b></td>
71
71
  <td></td>
72
72
  <td><b>{{ total_distance | round(1) }}</b></td>
73
- <td><b>{{ total_elapsed_time }}</b></td>
73
+ <td><b>{{ total_elapsed_time|td }}</b></td>
74
74
  <td></td>
75
75
  <td></td>
76
76
  </tr>
@@ -0,0 +1,42 @@
1
+ {% extends "page.html.j2" %}
2
+
3
+ {% block container %}
4
+
5
+ <h1 class="mb-3">Edit Activity</h1>
6
+
7
+ <form method="POST">
8
+ <div class="mb-3">
9
+ <label for="name" class="form-label">Name</label>
10
+ <input type="text" class="form-control" id="name" name="name" value="{{ override['name'] }}" />
11
+ </div>
12
+
13
+ <div class="mb-3">
14
+ <label for="kind" class="form-label">Kind</label>
15
+ <input type="text" class="form-control" id="kind" name="kind" value="{{ override['kind'] }}" />
16
+ </div>
17
+
18
+ <div class="mb-3">
19
+ <label for="equipment" class="form-label">Equipment</label>
20
+ <input type="text" class="form-control" id="equipment" name="equipment" value="{{ override['equipment'] }}" />
21
+ </div>
22
+
23
+ <div class="mb-3">
24
+ <div class="form-check">
25
+ <input type="checkbox" class="form-check-input" id="commute" name="commute" {% if override['commute'] %}
26
+ checked {% endif %} />
27
+ <label for="commute" class="form-check-label">Commute</label>
28
+ </div>
29
+ </div>
30
+
31
+ <div class="mb-3">
32
+ <div class="form-check">
33
+ <input type="checkbox" class="form-check-input" id="consider_for_achievements"
34
+ name="consider_for_achievements" {% if override['consider_for_achievements'] %} checked {% endif %} />
35
+ <label for="consider_for_achievements" class="form-check-label">Consider for achievements</label>
36
+ </div>
37
+ </div>
38
+
39
+ <button type="submit" class="btn btn-primary">Save</button>
40
+ </form>
41
+
42
+ {% endblock %}
@@ -66,9 +66,9 @@
66
66
  <tr>
67
67
  <td><span style="color: {{ activity['color'] }};">█</span> <a href="{{ url_for('activity.show', id=activity.id) }}">{{
68
68
  activity.name }}</a></td>
69
- <td>{{ activity.start }}</td>
69
+ <td>{{ activity.start|dt }}</td>
70
70
  <td>{{ activity.distance_km | round(1) }}</td>
71
- <td>{{ activity.elapsed_time }}</td>
71
+ <td>{{ activity.elapsed_time|td }}</td>
72
72
  <td>{{ activity["equipment"] }}</td>
73
73
  <td>{{ activity["kind"] }}</td>
74
74
  </tr>
@@ -19,9 +19,9 @@
19
19
  <dt>Distance</dt>
20
20
  <dd>{{ activity.distance_km|round(1) }} km</dd>
21
21
  <dt>Elapsed time</dt>
22
- <dd>{{ activity.elapsed_time }}</dd>
22
+ <dd>{{ activity.elapsed_time|td }}</dd>
23
23
  <dt>Moving time</dt>
24
- <dd>{{ activity.moving_time }}</dd>
24
+ <dd>{{ activity.moving_time|td }}</dd>
25
25
  <dt>Start time</dt>
26
26
  <dd><a href="{{ url_for('activity.day', year=date.year, month=date.month, day=date.day) }}">{{ date }}</a>
27
27
  {{ time }}
@@ -31,7 +31,7 @@
31
31
  <dt>Steps</dt>
32
32
  <dd>{{ activity.steps }}</dd>
33
33
  <dt>Equipment</dt>
34
- <dd>{{ activity.equipment }}</dd>
34
+ <dd>{{ activity['equipment'] }}</dd>
35
35
  <dt>New Explorer Tiles</dt>
36
36
  <dd>{{ new_tiles[14] }}</dd>
37
37
  <dt>New Squadratinhos</dt>
@@ -41,6 +41,8 @@
41
41
  <dt>Source path</dt>
42
42
  <dd>{{ activity.path }}</dd>
43
43
  </dl>
44
+
45
+ <a href="{{ url_for('.edit', id=activity['id']) }}" class="btn btn-secondary btn-small">Edit metadata</a>
44
46
  </div>
45
47
  <div class="col-8">
46
48
  <div id="activity-map" style="height: 500px;" class="mb-3"></div>
@@ -201,10 +203,10 @@
201
203
  <tbody>
202
204
  {% for other_activity in similar_activites %}
203
205
  <tr>
204
- <td><a href="{{ url_for('.show', id=other_activity.id) }}">{{ other_activity.start
206
+ <td><a href="{{ url_for('.show', id=other_activity.id) }}">{{ other_activity.start|dt
205
207
  }}</a></td>
206
208
  <td>{{ other_activity.distance_km | round(1) }}</td>
207
- <td>{{ other_activity.elapsed_time }}</td>
209
+ <td>{{ other_activity.elapsed_time|td }}</td>
208
210
  <td>{{ other_activity["equipment"] }}</td>
209
211
  <td>{{ other_activity["kind"] }}</td>
210
212
  </tr>
@@ -1,3 +1,4 @@
1
+ import datetime
1
2
  import importlib
2
3
  import json
3
4
  import pathlib
@@ -60,6 +61,18 @@ def web_ui_main(
60
61
  app.config["UPLOAD_FOLDER"] = "Activities"
61
62
  app.secret_key = get_secret_key()
62
63
 
64
+ @app.template_filter()
65
+ def dt(value: datetime.datetime):
66
+ return value.strftime("%Y-%m-%d %H:%M")
67
+
68
+ @app.template_filter()
69
+ def td(v: datetime.timedelta):
70
+ seconds = v.total_seconds()
71
+ h = int(seconds // 3600)
72
+ m = int(seconds // 60 % 60)
73
+ s = int(seconds // 1 % 60)
74
+ return f"{h}:{m:02d}:{s:02d}"
75
+
63
76
  authenticator = Authenticator(config_accessor())
64
77
 
65
78
  route_start(app, repository, config_accessor())
@@ -68,9 +81,7 @@ def web_ui_main(
68
81
 
69
82
  app.register_blueprint(
70
83
  make_activity_blueprint(
71
- repository,
72
- tile_visit_accessor,
73
- config_accessor(),
84
+ repository, tile_visit_accessor, config_accessor(), authenticator
74
85
  ),
75
86
  url_prefix="/activity",
76
87
  )
@@ -90,23 +90,25 @@ class HeatmapController:
90
90
  tile_counts = np.zeros(tile_pixels, dtype=np.int32)
91
91
  tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
92
92
  activity_ids = self.activities_per_tile[z].get((x, y), set())
93
- if activity_ids:
93
+ activity_ids_kind = set()
94
+ for activity_id in activity_ids:
95
+ activity = self._repository.get_activity_by_id(activity_id)
96
+ if activity["kind"] == kind:
97
+ activity_ids_kind.add(activity_id)
98
+ if activity_ids_kind:
94
99
  with work_tracker(
95
100
  tile_count_cache_path.with_suffix(".json")
96
101
  ) as parsed_activities:
97
- if parsed_activities - activity_ids:
102
+ if parsed_activities - activity_ids_kind:
98
103
  logger.warning(
99
104
  f"Resetting heatmap cache for {kind=}/{x=}/{y=}/{z=} because activities have been removed."
100
105
  )
101
106
  tile_counts = np.zeros(tile_pixels, dtype=np.int32)
102
107
  parsed_activities.clear()
103
- for activity_id in activity_ids:
108
+ for activity_id in activity_ids_kind:
104
109
  if activity_id in parsed_activities:
105
110
  continue
106
111
  parsed_activities.add(activity_id)
107
- activity = self._repository.get_activity_by_id(activity_id)
108
- if activity["kind"] != kind:
109
- continue
110
112
  time_series = self._repository.get_time_series(activity_id)
111
113
  for _, group in time_series.groupby("segment_id"):
112
114
  xy_pixels = (
@@ -78,10 +78,10 @@
78
78
  {% for index, activity in activities %}
79
79
  <tr>
80
80
  <td><a href="{{ url_for('activity.show', id=activity['id']) }}">{{ activity['name'] }}</a></td>
81
- <td>{{ activity['start'] }}</td>
81
+ <td>{{ activity['start']|dt }}</td>
82
82
  <td>{{ activity['kind'] }}</td>
83
83
  <td>{{ '%.1f' % activity["distance_km"] }} km</td>
84
- <td>{{ activity.elapsed_time }}</td>
84
+ <td>{{ activity.elapsed_time|td }}</td>
85
85
  </tr>
86
86
  {% endfor %}
87
87
  </tbody>
@@ -21,7 +21,7 @@ class SummaryController:
21
21
  def render(self) -> dict:
22
22
  kind_scale = make_kind_scale(self._repository.meta, self._config)
23
23
  df = embellished_activities(self._repository.meta)
24
- df = df.loc[df["consider_for_achievements"]]
24
+ # df = df.loc[df["consider_for_achievements"]]
25
25
 
26
26
  year_kind_total = (
27
27
  df[["year", "kind", "distance_km", "hours"]]
@@ -123,7 +123,7 @@
123
123
  </p>
124
124
  <p class="card-text"><small class="text-body-secondary"></small>{{ activity.kind }} with {{
125
125
  (activity.distance_km)|round(1) }} km in {{
126
- activity.elapsed_time }} on {{ activity.start }}</small></p>
126
+ activity.elapsed_time|td }} on {{ activity.start|dt }}</small></p>
127
127
  </div>
128
128
  </div>
129
129
  </div>
@@ -49,8 +49,8 @@
49
49
  <h5 class="card-title">{{ elem.activity["name"] }}</h5>
50
50
  </a>
51
51
  <p class="card-text">{{ elem.activity.kind }} with {{ (elem.activity.distance_km)|round(1) }} km in {{
52
- elem.activity.elapsed_time }}</p>
53
- <p class="card-text"><small class="text-body-secondary">{{ elem.activity.start }}</small></p>
52
+ elem.activity.elapsed_time|td }}</p>
53
+ <p class="card-text"><small class="text-body-secondary">{{ elem.activity.start|dt }}</small></p>
54
54
  </div>
55
55
  </div>
56
56
  </div>
@@ -102,8 +102,7 @@ def scan_for_activities(
102
102
  ) -> None:
103
103
  if pathlib.Path("Activities").exists():
104
104
  import_from_directory(
105
- config.metadata_extraction_regexes,
106
- config.num_processes,
105
+ config.metadata_extraction_regexes, config.num_processes, config
107
106
  )
108
107
  if pathlib.Path("Strava Export").exists():
109
108
  import_from_strava_checkout()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.30.0
3
+ Version: 0.32.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -1,13 +1,13 @@
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=6IFfTqWGXRVD-DxKho1Nyc9ZV7kvzBWw3ZninXRM0Nk,6578
5
- geo_activity_playground/core/config.py,sha256=3xSaPKmNT_h0MHDoYgqOtmqj-x831ETiUKdSFXqrzHs,4744
4
+ geo_activity_playground/core/activities.py,sha256=soxMtdijnkZ1bYZU0q6wuM8NPNFoUpLwYp3IvBOaKJY,6927
5
+ geo_activity_playground/core/config.py,sha256=T6u8Ha2yUTmSA-TcI0_yg_vNIh-JdAyKt7vs-cfQmuE,4817
6
6
  geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
7
7
  geo_activity_playground/core/enrichment.py,sha256=fUmk6avy_rqePlHmJQFTQhAxjgIRaxxmq18N2OSXBBg,7771
8
8
  geo_activity_playground/core/heart_rate.py,sha256=IwMt58TpjOYqpAxtsj07zP2ttpN_J3GZeiv-qGhYyJc,1598
9
- geo_activity_playground/core/heatmap.py,sha256=bRLQHzmTEsQbX8XWeg85x_lRGk272UoYRiCnoxZ5da0,4189
10
- geo_activity_playground/core/paths.py,sha256=tazkjcDFkQ1YrFPeWKeh-TqUrCeK-u0fG6U0VGsxIpo,2368
9
+ geo_activity_playground/core/heatmap.py,sha256=KqqXo9ayppwXU3VEkhqtcyC-EPkRUVZ-J0EK6FhR50M,6474
10
+ geo_activity_playground/core/paths.py,sha256=RBeUi38riP_msTGPy1TsPRNiblzE-lFivaJSLULE8b0,2503
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
13
  geo_activity_playground/core/tasks.py,sha256=aMDBWJqp6ek2ao6G6Xa8GOSZbcQqXoWL74SGRowRPIk,2942
@@ -22,7 +22,7 @@ geo_activity_playground/explorer/video.py,sha256=ROAmV9shfJyqTgnXVD41KFORiwnRgVp
22
22
  geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  geo_activity_playground/importers/activity_parsers.py,sha256=XNQs0ziqAcVqIoiLAX5Ndmhb6v__29XdjUPvNvc7oBI,11082
24
24
  geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
25
- geo_activity_playground/importers/directory.py,sha256=IPnr1xk3beznmPVjfWL6AQiEKA4WN5EDIfnzrM0Tzlc,5767
25
+ geo_activity_playground/importers/directory.py,sha256=CA-vFOMm8G4MSM_Q09OwQKduCApL2PWaxLTVxgw_xpw,5908
26
26
  geo_activity_playground/importers/strava_api.py,sha256=cJCZsLemhOlxTtZh0z_npidgae9SD5HyEUry2uvem_A,7775
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
@@ -30,13 +30,14 @@ geo_activity_playground/importers/test_directory.py,sha256=ljXokx7q0OgtHvEdHftcQ
30
30
  geo_activity_playground/importers/test_strava_api.py,sha256=4vX7wDr1a9aRh8myxNrIq6RwDBbP8ZeoXXPc10CAbW4,431
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
- geo_activity_playground/webui/activity/blueprint.py,sha256=upQzZa5sKApj_Fmu6PziFDboi7SBL5Zsi-tNSSNPlEE,1759
34
- geo_activity_playground/webui/activity/controller.py,sha256=iE9JILmfqOWgXWT4KgqrXLpQJ8xSqfiNKeJw6vnWDAc,19459
35
- geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=9jZ1lunfcigRGn2Cp65w6FrWy5EQGSECM_flFNvthrM,2726
33
+ geo_activity_playground/webui/activity/blueprint.py,sha256=Ub2mC9S9TII7CJaaWahnbNtT76uOGKNDWE0-j2C56CA,3893
34
+ geo_activity_playground/webui/activity/controller.py,sha256=zOPoj4N-B-x1O_qp5ZUvlT5gmDjpIGuqKqnv4f1-B54,19570
35
+ geo_activity_playground/webui/activity/templates/activity/day.html.j2,sha256=o18e-TMtgCrY7iroInVhRA267l-o6uGNlstIwsvFnww,2735
36
+ geo_activity_playground/webui/activity/templates/activity/edit.html.j2,sha256=ckcTTxcQOhmvvAGNTEOtWCUG4LhvO4HfQImlIa5qKs8,1530
36
37
  geo_activity_playground/webui/activity/templates/activity/lines.html.j2,sha256=5gB1aDjRgi_RventenRfC10_FtMT4ch_VuWvA9AMlBY,1121
37
- geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=vDEZGJXmrQ6goxmMdVsTSd1QMQXorlZ4X-ZjwsP90yA,2445
38
- geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=yKJSnPKfOxaAqaHsEgpYOWruPZ96Su9h5d41m7uOseA,6859
39
- geo_activity_playground/webui/app.py,sha256=foON49jw8klBJ3GF70DDjgz-_KRDPFvC31_mScaqDXk,4255
38
+ geo_activity_playground/webui/activity/templates/activity/name.html.j2,sha256=npciXBo7_94_tCE0X7RLFHws8mgHzs4VgbOnyA9rsOI,2451
39
+ geo_activity_playground/webui/activity/templates/activity/show.html.j2,sha256=MndgjmSi6T2xbIietSI-5n1hi7E1tD4eCoQP0tD3Ing,6989
40
+ geo_activity_playground/webui/app.py,sha256=zDpLVGa0-_uoy9KQDcY8Z52RENhziDOfxfWv744GQAw,4616
40
41
  geo_activity_playground/webui/auth/blueprint.py,sha256=Lx-ZvMnfHLC1CMre1xPQI3k_pCtQoZvgRhtmafULzoE,812
41
42
  geo_activity_playground/webui/auth/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
42
43
  geo_activity_playground/webui/authenticator.py,sha256=k278OEVuOfAmTGT4F2X4pqSTwwkK_FA87EIhAeysEqc,1416
@@ -60,11 +61,11 @@ geo_activity_playground/webui/explorer/controller.py,sha256=pIzWh0TpLJgKQZlS325-
60
61
  geo_activity_playground/webui/explorer/templates/explorer/index.html.j2,sha256=u2htecx-XwINRiINHFN6EZDaRXVtiape1OCUZexTBU8,7049
61
62
  geo_activity_playground/webui/heatmap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
62
63
  geo_activity_playground/webui/heatmap/blueprint.py,sha256=bjQu-HL3QN5UpJ6tHOifhcLGlPr_hIKvaRu78md4JqM,1470
63
- geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=Q17Ay8hbU5ZlUiz2a9S-ULWrnNGWQHvTTV3kDY5FhNc,7411
64
+ geo_activity_playground/webui/heatmap/heatmap_controller.py,sha256=LMBglDOhcLjvkq-7hr1B6IhW_TiBDZ1NIFP5dqhLCC4,7503
64
65
  geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2,sha256=YLeu6P4djl8G4qAXR6DhetseqrbOodN7aN4coocknc4,1875
65
66
  geo_activity_playground/webui/plot_util.py,sha256=pTTQoqOCkLVjkgOit7mbry28kMruZIL8amZozSzEpxQ,283
66
67
  geo_activity_playground/webui/search/blueprint.py,sha256=7TDsiqEowMyHNlFImk-hCGso69KOieG4rfJnLRHpRz8,3300
67
- geo_activity_playground/webui/search/templates/search/index.html.j2,sha256=Qv_05LFOrLPxto0hPjy0j0PPySYCIRDJQAQcXZSCfZE,3792
68
+ geo_activity_playground/webui/search/templates/search/index.html.j2,sha256=d39uhteoY6JOZePqhLIYLERqkckW3oghMnTUZa7X1J8,3798
68
69
  geo_activity_playground/webui/settings/blueprint.py,sha256=Jeh2MwRmCNF6BfwrHSUixLKOTPFlwZ4Mrb68Botr6Q8,9487
69
70
  geo_activity_playground/webui/settings/controller.py,sha256=MIZVBfoGNvmJnB_ECV_x5eH2i6gDZvkWSQ4PcSKyLKs,9170
70
71
  geo_activity_playground/webui/settings/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
@@ -99,20 +100,20 @@ geo_activity_playground/webui/static/web-app-manifest-192x192.png,sha256=eEImN6i
99
100
  geo_activity_playground/webui/static/web-app-manifest-512x512.png,sha256=vU9oQ4HnQerFDZVzcAT9twj4_Doc6_9v9wVvoRI-f_E,48318
100
101
  geo_activity_playground/webui/summary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
102
  geo_activity_playground/webui/summary/blueprint.py,sha256=tfA2aPF19yKwkQOb5lPQBySoQYYhTn49Iuh0SYvsGP8,593
102
- geo_activity_playground/webui/summary/controller.py,sha256=cWn5szA1o5Vjht0DyhRwBjmwqJryrLcmm4FUdmVpUoo,9443
103
- geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=jaa981rFwf71Sj_ryvB0_bu--HAxbcET3WqG5ChffLU,4466
104
- geo_activity_playground/webui/templates/home.html.j2,sha256=A5DXztAuSIczAdtnGjkZu2QBezrsZ4Rjp7prc7iK4kk,2167
103
+ geo_activity_playground/webui/summary/controller.py,sha256=UU6ClARzdvUQnIaSyuG3mbNxtuCjYCUM22P1aRFwOuQ,9445
104
+ geo_activity_playground/webui/summary/templates/summary/index.html.j2,sha256=S_kpXPldrxIAEBdlG0YlXlvMHI4dQc4QZtejhHM4_N8,4472
105
+ geo_activity_playground/webui/templates/home.html.j2,sha256=EvEgvr_JeppGqLEJzcDc0kL-8e4OUV8aleWTP5eDeh8,2173
105
106
  geo_activity_playground/webui/templates/page.html.j2,sha256=znTbtD2NALrhmUN_Q-F1ElGlKtecoUv8vOCcUtojrdI,11134
106
107
  geo_activity_playground/webui/tile/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
107
108
  geo_activity_playground/webui/tile/blueprint.py,sha256=cK0o2Z3BrLycgF9zw0F8s9qF-JaYDbF5Gog-GXDtUZ8,943
108
109
  geo_activity_playground/webui/tile/controller.py,sha256=PISh4vKs27b-LxFfTARtr5RAwHFresA1Kw1MDcERSRU,1221
109
110
  geo_activity_playground/webui/upload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
110
111
  geo_activity_playground/webui/upload/blueprint.py,sha256=xX9scEmleN_dL03jfhWRh5yI1WsFyhxUFiS_Ul2HWy4,1428
111
- geo_activity_playground/webui/upload/controller.py,sha256=AUcDotTw-h30XgY5Te0kAqRfL7xXqK74iVO13g20pD0,4085
112
+ geo_activity_playground/webui/upload/controller.py,sha256=EvoUnmKBo3QS2TLak7-yVZ16sEDyEB6Nf2MN_scHuhQ,4080
112
113
  geo_activity_playground/webui/upload/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
113
114
  geo_activity_playground/webui/upload/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
114
- geo_activity_playground-0.30.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
115
- geo_activity_playground-0.30.0.dist-info/METADATA,sha256=HasSX5fDpl8x4jZP5hCKkECUiP-cA_FVQRBQwwqa6L4,1612
116
- geo_activity_playground-0.30.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
117
- geo_activity_playground-0.30.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
118
- geo_activity_playground-0.30.0.dist-info/RECORD,,
115
+ geo_activity_playground-0.32.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
116
+ geo_activity_playground-0.32.0.dist-info/METADATA,sha256=02jGl-aJPw8GRimnYlMTNECzPhE0ei6fpiidYbK7UDM,1612
117
+ geo_activity_playground-0.32.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
118
+ geo_activity_playground-0.32.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
119
+ geo_activity_playground-0.32.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.8.1
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any