geo-activity-playground 0.43.3__py3-none-any.whl → 0.45.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 (26) hide show
  1. geo_activity_playground/alembic/versions/0f02b92c4f94_add_tag_color.py +28 -0
  2. geo_activity_playground/core/config.py +1 -0
  3. geo_activity_playground/core/datamodel.py +16 -2
  4. geo_activity_playground/core/enrichment.py +2 -2
  5. geo_activity_playground/core/meta_search.py +1 -1
  6. geo_activity_playground/core/paths.py +1 -1
  7. geo_activity_playground/importers/directory.py +3 -2
  8. geo_activity_playground/importers/strava_checkout.py +2 -1
  9. geo_activity_playground/webui/blueprints/activity_blueprint.py +22 -0
  10. geo_activity_playground/webui/blueprints/eddington_blueprints.py +17 -8
  11. geo_activity_playground/webui/blueprints/entry_views.py +5 -3
  12. geo_activity_playground/webui/blueprints/equipment_blueprint.py +1 -1
  13. geo_activity_playground/webui/blueprints/settings_blueprint.py +83 -3
  14. geo_activity_playground/webui/blueprints/upload_blueprint.py +7 -3
  15. geo_activity_playground/webui/templates/activity/show.html.j2 +6 -2
  16. geo_activity_playground/webui/templates/home.html.j2 +2 -0
  17. geo_activity_playground/webui/templates/page.html.j2 +4 -0
  18. geo_activity_playground/webui/templates/search_form.html.j2 +1 -1
  19. geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +5 -0
  20. geo_activity_playground/webui/templates/settings/tags-list.html.j2 +1 -1
  21. geo_activity_playground/webui/templates/summary/index.html.j2 +2 -0
  22. {geo_activity_playground-0.43.3.dist-info → geo_activity_playground-0.45.0.dist-info}/METADATA +1 -1
  23. {geo_activity_playground-0.43.3.dist-info → geo_activity_playground-0.45.0.dist-info}/RECORD +26 -25
  24. {geo_activity_playground-0.43.3.dist-info → geo_activity_playground-0.45.0.dist-info}/LICENSE +0 -0
  25. {geo_activity_playground-0.43.3.dist-info → geo_activity_playground-0.45.0.dist-info}/WHEEL +0 -0
  26. {geo_activity_playground-0.43.3.dist-info → geo_activity_playground-0.45.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,28 @@
1
+ from typing import Sequence
2
+ from typing import Union
3
+
4
+ import sqlalchemy as sa
5
+ from alembic import op
6
+
7
+
8
+ # revision identifiers, used by Alembic.
9
+ revision: str = "0f02b92c4f94"
10
+ down_revision: Union[str, None] = "da2cba03b71d"
11
+ branch_labels: Union[str, Sequence[str], None] = None
12
+ depends_on: Union[str, Sequence[str], None] = None
13
+
14
+
15
+ def upgrade() -> None:
16
+ # ### commands auto generated by Alembic - please adjust! ###
17
+ with op.batch_alter_table("tags", schema=None) as batch_op:
18
+ batch_op.add_column(sa.Column("color", sa.String(), nullable=True))
19
+
20
+ # ### end Alembic commands ###
21
+
22
+
23
+ def downgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ with op.batch_alter_table("tags", schema=None) as batch_op:
26
+ batch_op.drop_column("color")
27
+
28
+ # ### end Alembic commands ###
@@ -38,6 +38,7 @@ class Config:
38
38
  privacy_zones: dict[str, list[list[float]]] = dataclasses.field(
39
39
  default_factory=dict
40
40
  )
41
+ reliable_elevation_measurements: bool = True
41
42
  sharepic_suppressed_fields: list[str] = dataclasses.field(default_factory=list)
42
43
  strava_client_id: int = 131693
43
44
  strava_client_secret: str = "0ccc0100a2c218512a7ef0cea3b0e322fb4b4365"
@@ -21,12 +21,17 @@ from sqlalchemy.orm import mapped_column
21
21
  from sqlalchemy.orm import relationship
22
22
 
23
23
  from .config import Config
24
- from .paths import time_series_dir
24
+ from .paths import activity_extracted_meta_dir
25
+ from .paths import activity_extracted_time_series_dir
26
+ from .paths import TIME_SERIES_DIR
25
27
 
26
28
 
27
29
  logger = logging.getLogger(__name__)
28
30
 
29
31
 
32
+ DEFAULT_UNKNOWN_NAME = "Unknown"
33
+
34
+
30
35
  class ActivityMeta(TypedDict):
31
36
  average_speed_elapsed_kmh: float
32
37
  average_speed_moving_kmh: float
@@ -141,7 +146,7 @@ class Activity(DB.Model):
141
146
 
142
147
  @property
143
148
  def raw_time_series(self) -> pd.DataFrame:
144
- path = time_series_dir() / f"{self.id}.parquet"
149
+ path = TIME_SERIES_DIR() / f"{self.id}.parquet"
145
150
  try:
146
151
  time_series = pd.read_parquet(path)
147
152
  if "altitude" in time_series.columns:
@@ -160,6 +165,14 @@ class Activity(DB.Model):
160
165
  else:
161
166
  return self.raw_time_series
162
167
 
168
+ def delete_data(self) -> None:
169
+ for path in [
170
+ TIME_SERIES_DIR() / f"{self.id}.parquet",
171
+ activity_extracted_meta_dir() / f"{self.upstream_id}.pickle",
172
+ activity_extracted_time_series_dir() / f"{self.upstream_id}.pickle",
173
+ ]:
174
+ path.unlink(missing_ok=True)
175
+
163
176
 
164
177
  class Tag(DB.Model):
165
178
  __tablename__ = "tags"
@@ -167,6 +180,7 @@ class Tag(DB.Model):
167
180
 
168
181
  id: Mapped[int] = mapped_column(primary_key=True)
169
182
  tag: Mapped[str] = mapped_column(String, unique=True)
183
+ color: Mapped[str] = mapped_column(String, nullable=True)
170
184
 
171
185
  activities: Mapped[list[Activity]] = relationship(
172
186
  secondary=activity_tag_association_table, back_populates="tags"
@@ -18,7 +18,7 @@ from .datamodel import get_or_make_kind
18
18
  from .missing_values import some
19
19
  from .paths import activity_extracted_meta_dir
20
20
  from .paths import activity_extracted_time_series_dir
21
- from .paths import time_series_dir
21
+ from .paths import TIME_SERIES_DIR
22
22
  from .tiles import compute_tile_float
23
23
  from .time_conversion import convert_to_datetime_ns
24
24
 
@@ -101,7 +101,7 @@ def populate_database_from_extracted(config: Config) -> None:
101
101
  )
102
102
  raise
103
103
 
104
- enriched_time_series_path = time_series_dir() / f"{activity.id}.parquet"
104
+ enriched_time_series_path = TIME_SERIES_DIR() / f"{activity.id}.parquet"
105
105
  time_series.to_parquet(enriched_time_series_path)
106
106
 
107
107
 
@@ -120,7 +120,7 @@ class SearchQuery:
120
120
  variables.append(("distance_km_max", self.distance_km_max))
121
121
 
122
122
  return "&".join(
123
- f"{key}={urllib.parse.quote_plus(value)}" for key, value in variables
123
+ f"{key}={urllib.parse.quote_plus(str(value))}" for key, value in variables
124
124
  )
125
125
 
126
126
 
@@ -65,7 +65,7 @@ activity_enriched_time_series_dir = dir_wrapper(_activity_enriched_time_series_d
65
65
  tiles_per_time_series = dir_wrapper(_tiles_per_time_series)
66
66
  strava_api_dir = dir_wrapper(_strava_api_dir)
67
67
  activity_meta_override_dir = dir_wrapper(_activity_meta_override_dir)
68
- time_series_dir = dir_wrapper(_time_series_dir)
68
+ TIME_SERIES_DIR = dir_wrapper(_time_series_dir)
69
69
  PHOTOS_DIR = dir_wrapper(_photos_dir)
70
70
 
71
71
  activities_file = file_wrapper(_activities_file)
@@ -10,6 +10,7 @@ from tqdm import tqdm
10
10
 
11
11
  from ..core.config import Config
12
12
  from ..core.datamodel import ActivityMeta
13
+ from ..core.datamodel import DEFAULT_UNKNOWN_NAME
13
14
  from ..core.paths import activity_extracted_dir
14
15
  from ..core.paths import activity_extracted_meta_dir
15
16
  from ..core.paths import activity_extracted_time_series_dir
@@ -84,8 +85,8 @@ def import_from_directory(
84
85
  # https://stackoverflow.com/a/74718395/653152
85
86
  name=path.name.removesuffix("".join(path.suffixes)),
86
87
  path=str(path),
87
- kind="Unknown",
88
- equipment="Unknown",
88
+ kind=DEFAULT_UNKNOWN_NAME,
89
+ equipment=DEFAULT_UNKNOWN_NAME,
89
90
  consider_for_achievements=True,
90
91
  )
91
92
  activity_meta.update(activity_meta_from_file)
@@ -13,6 +13,7 @@ import pandas as pd
13
13
  from tqdm import tqdm
14
14
 
15
15
  from ..core.datamodel import ActivityMeta
16
+ from ..core.datamodel import DEFAULT_UNKNOWN_NAME
16
17
  from ..core.paths import activity_extracted_meta_dir
17
18
  from ..core.paths import activity_extracted_time_series_dir
18
19
  from ..core.paths import strava_last_activity_date_path
@@ -277,7 +278,7 @@ def convert_strava_checkout(
277
278
  nan_as_none(row["Activity Gear"])
278
279
  or nan_as_none(row["Bike"])
279
280
  or nan_as_none(row["Gear"])
280
- or ""
281
+ or DEFAULT_UNKNOWN_NAME
281
282
  )
282
283
  activity_file = checkout_path / row["Filename"]
283
284
 
@@ -1,6 +1,7 @@
1
1
  import datetime
2
2
  import io
3
3
  import logging
4
+ import pathlib
4
5
  import re
5
6
  from typing import Optional
6
7
 
@@ -431,6 +432,27 @@ def make_activity_blueprint(
431
432
  color_line_geojson=geojson.dumps(fc),
432
433
  )
433
434
 
435
+ @blueprint.route("/delete/<id>")
436
+ @needs_authentication(authenticator)
437
+ def delete(id: int):
438
+ activity = DB.session.get_one(Activity, id)
439
+ activity.delete_data()
440
+ DB.session.delete(activity)
441
+ DB.session.commit()
442
+ return redirect(url_for("index"))
443
+
444
+ @blueprint.route("/download-original/<id>")
445
+ @needs_authentication(authenticator)
446
+ def download_original(id: int):
447
+ activity = DB.session.get_one(Activity, id)
448
+ path = pathlib.Path(activity.path)
449
+ with open(path) as f:
450
+ return Response(
451
+ f.read(),
452
+ mimetype="application/octet-stream",
453
+ headers={"Content-disposition": f'attachment; filename="{path.name}"'},
454
+ )
455
+
434
456
  return blueprint
435
457
 
436
458
 
@@ -26,7 +26,12 @@ def register_eddington_blueprint(
26
26
  @blueprint.route("/")
27
27
  def distance():
28
28
  return _render_eddington_template(
29
- repository, request, search_query_history, "distance", column_distance, [1]
29
+ repository,
30
+ request,
31
+ search_query_history,
32
+ "distance",
33
+ column_distance,
34
+ [1],
30
35
  )
31
36
 
32
37
  @blueprint.route("/elevation_gain")
@@ -64,6 +69,10 @@ def _render_eddington_template(
64
69
  .copy()
65
70
  )
66
71
 
72
+ assert (
73
+ len(activities) > 0
74
+ ), "The filter has selected zero elements, that cannot work here."
75
+
67
76
  activities["year"] = [start.year for start in activities["start"]]
68
77
  activities["date"] = [start.date() for start in activities["start"]]
69
78
  activities["isoyear"] = [start.isocalendar().year for start in activities["start"]]
@@ -111,24 +120,24 @@ def _render_eddington_template(
111
120
  )
112
121
 
113
122
 
114
- def _get_values_per_group(grouped, columnName, divisor) -> tuple[int, pd.DataFrame]:
123
+ def _get_values_per_group(grouped, column_name, divisor) -> tuple[int, pd.DataFrame]:
115
124
  sum_per_group = grouped.apply(
116
- lambda group: int(sum(group[columnName])), include_groups=False
125
+ lambda group: int(sum(group[column_name])), include_groups=False
117
126
  )
118
127
  counts = dict(zip(*np.unique(sorted(sum_per_group), return_counts=True)))
119
128
  eddington = pd.DataFrame(
120
- {columnName: d, "count": counts.get(d, 0)}
129
+ {column_name: d, "count": counts.get(d, 0)}
121
130
  for d in range(max(counts.keys()) + 1)
122
131
  )
123
132
  eddington["total"] = eddington["count"][::-1].cumsum()[::-1]
124
- eddington[f"{columnName}_div"] = eddington[columnName] // divisor
133
+ eddington[f"{column_name}_div"] = eddington[column_name] // divisor
125
134
  en = (
126
- eddington.loc[eddington["total"] >= eddington[f"{columnName}_div"]][
127
- "total"
135
+ eddington.loc[eddington["total"] >= eddington[f"{column_name}_div"]][
136
+ f"{column_name}_div"
128
137
  ].iloc[-1]
129
138
  * divisor
130
139
  )
131
- eddington["missing"] = eddington[f"{columnName}_div"] - eddington["total"]
140
+ eddington["missing"] = eddington[f"{column_name}_div"] - eddington["total"]
132
141
 
133
142
  return en, eddington
134
143
 
@@ -31,9 +31,11 @@ def register_entry_views(
31
31
  context["distance_last_30_days_plot"] = _last_30_days_meta_plot(
32
32
  repository.meta, kind_scale, column_distance
33
33
  )
34
- context["elevation_gain_last_30_days_plot"] = _last_30_days_meta_plot(
35
- repository.meta, kind_scale, column_elevation_gain
36
- )
34
+
35
+ if config.reliable_elevation_measurements:
36
+ context["elevation_gain_last_30_days_plot"] = _last_30_days_meta_plot(
37
+ repository.meta, kind_scale, column_elevation_gain
38
+ )
37
39
 
38
40
  context["latest_activities"] = collections.defaultdict(list)
39
41
  for activity in DB.session.scalars(
@@ -21,7 +21,7 @@ def make_equipment_blueprint(
21
21
  )
22
22
 
23
23
  # Prepare data for the stacked area chart
24
- activities = repository.meta
24
+ activities = repository.meta.dropna(subset=["start"])
25
25
  activities["month"] = (
26
26
  activities["start"].dt.to_period("M").apply(lambda r: r.start_time)
27
27
  )
@@ -13,14 +13,19 @@ from flask import render_template
13
13
  from flask import request
14
14
  from flask import Response
15
15
  from flask import url_for
16
+ from tqdm import tqdm
16
17
 
17
18
  from ...core.config import ConfigAccessor
19
+ from ...core.datamodel import Activity
18
20
  from ...core.datamodel import DB
19
21
  from ...core.datamodel import Equipment
20
22
  from ...core.datamodel import Kind
21
23
  from ...core.datamodel import Tag
24
+ from ...core.enrichment import _embellish_single_time_series
25
+ from ...core.enrichment import update_via_time_series
22
26
  from ...core.heart_rate import HeartRateZoneComputer
23
27
  from ...core.paths import _activity_enriched_dir
28
+ from ...core.paths import TIME_SERIES_DIR
24
29
  from ..authenticator import Authenticator
25
30
  from ..authenticator import needs_authentication
26
31
  from ..flasher import Flasher
@@ -272,7 +277,7 @@ def make_settings_blueprint(
272
277
  if request.method == "POST":
273
278
  zone_names = request.form.getlist("zone_name")
274
279
  zone_geojsons = request.form.getlist("zone_geojson")
275
- strava_login_helper.save_privacy_zones(zone_names, zone_geojsons)
280
+ save_privacy_zones(zone_names, zone_geojsons, config_accessor)
276
281
 
277
282
  assert len(zone_names) == len(zone_geojsons)
278
283
  new_zone_config = {}
@@ -348,8 +353,19 @@ def make_settings_blueprint(
348
353
  config_accessor().time_diff_threshold_seconds = threshold
349
354
  config_accessor.save()
350
355
  flash(f"Threshold set to {threshold}.", category="success")
351
- shutil.rmtree(_activity_enriched_dir)
352
- return redirect(url_for("upload.reload"))
356
+ for activity in tqdm(
357
+ DB.session.scalars(sqlalchemy.select(Activity)).all(),
358
+ desc="Recomputing segments",
359
+ ):
360
+ time_series = _embellish_single_time_series(
361
+ activity.raw_time_series,
362
+ None,
363
+ threshold,
364
+ )
365
+ update_via_time_series(activity, time_series)
366
+ enriched_time_series_path = TIME_SERIES_DIR() / f"{activity.id}.parquet"
367
+ time_series.to_parquet(enriched_time_series_path)
368
+ DB.session.commit()
353
369
  return render_template(
354
370
  "settings/segmentation.html.j2",
355
371
  threshold=config_accessor().time_diff_threshold_seconds,
@@ -395,6 +411,7 @@ def make_settings_blueprint(
395
411
  @needs_authentication(authenticator)
396
412
  def strava_callback():
397
413
  code = request.args.get("code", type=str)
414
+ assert code
398
415
  strava_login_helper.save_strava_code(code)
399
416
  return redirect(url_for(".strava"))
400
417
 
@@ -424,6 +441,7 @@ def make_settings_blueprint(
424
441
  tag = DB.session.get_one(Tag, id)
425
442
  if request.method == "POST":
426
443
  tag.tag = request.form["tag"]
444
+ tag.color = request.form["color"]
427
445
  DB.session.commit()
428
446
  return redirect(url_for(".tags_list"))
429
447
  else:
@@ -478,3 +496,65 @@ class StravaLoginHelper:
478
496
  self._config_accessor().strava_client_code = code
479
497
  self._config_accessor.save()
480
498
  flash("Connected to Strava API", category="success")
499
+
500
+
501
+ def save_privacy_zones(
502
+ zone_names: list[str], zone_geojsons: list[str], config_accessor: ConfigAccessor
503
+ ) -> None:
504
+ assert len(zone_names) == len(zone_geojsons)
505
+ new_zone_config = {}
506
+
507
+ for zone_name, zone_geojson_str in zip(zone_names, zone_geojsons):
508
+ if not zone_name or not zone_geojson_str:
509
+ continue
510
+
511
+ try:
512
+ zone_geojson = json.loads(zone_geojson_str)
513
+ except json.decoder.JSONDecodeError as e:
514
+ flash(
515
+ f"Could not parse GeoJSON for {zone_name} due to the following error: {e}"
516
+ )
517
+ continue
518
+
519
+ if not zone_geojson["type"] == "FeatureCollection":
520
+ flash(
521
+ f"Pasted GeoJSON for {zone_name} must be of type 'FeatureCollection'.",
522
+ category="danger",
523
+ )
524
+ continue
525
+
526
+ features = zone_geojson["features"]
527
+
528
+ if not len(features) == 1:
529
+ flash(
530
+ f"Pasted GeoJSON for {zone_name} must contain exactly one feature. You cannot have multiple shapes for one privacy zone",
531
+ category="danger",
532
+ )
533
+ continue
534
+
535
+ feature = features[0]
536
+ geometry = feature["geometry"]
537
+
538
+ if not geometry["type"] == "Polygon":
539
+ flash(
540
+ f"Geometry for {zone_name} is not a polygon. You need to create a polygon (or circle or rectangle).",
541
+ category="danger",
542
+ )
543
+ continue
544
+
545
+ coordinates = geometry["coordinates"]
546
+
547
+ if not len(coordinates) == 1:
548
+ flash(
549
+ f"Polygon for {zone_name} consists of multiple polygons. Please supply a simple one.",
550
+ category="danger",
551
+ )
552
+ continue
553
+
554
+ points = coordinates[0]
555
+
556
+ new_zone_config[zone_name] = points
557
+
558
+ config_accessor().privacy_zones = new_zone_config
559
+ config_accessor.save()
560
+ flash("Updated privacy zones.", category="success")
@@ -11,6 +11,7 @@ from flask import url_for
11
11
 
12
12
  from ...core.activities import ActivityRepository
13
13
  from ...core.config import Config
14
+ from ...core.datamodel import Activity
14
15
  from ...core.datamodel import DB
15
16
  from ...core.datamodel import Kind
16
17
  from ...core.enrichment import populate_database_from_extracted
@@ -78,9 +79,12 @@ def make_upload_blueprint(
78
79
  config,
79
80
  skip_strava=True,
80
81
  )
81
- activity_id = get_file_hash(target_path)
82
- flash(f"Activity was saved with ID {activity_id}.", "success")
83
- return redirect(f"/activity/{activity_id}")
82
+ latest_activity = DB.session.scalar(
83
+ sqlalchemy.select(Activity).order_by(Activity.id.desc()).limit(1)
84
+ )
85
+ assert latest_activity is not None
86
+ flash(f"Activity was saved with ID {latest_activity.id}.", "success")
87
+ return redirect(f"/activity/{latest_activity.id}")
84
88
 
85
89
  @blueprint.route("/refresh")
86
90
  @needs_authentication(authenticator)
@@ -24,7 +24,7 @@
24
24
  <dt>Tags</dt>
25
25
  <dd>
26
26
  {% for tag in activity.tags %}
27
- <span class="badge text-bg-primary">{{ tag.tag }}</span>
27
+ {{ activity_tag(tag) }}
28
28
  {% endfor %}
29
29
  </dd>
30
30
  {% endif %}
@@ -75,12 +75,16 @@
75
75
 
76
76
  <dt>ID</dt>
77
77
  <dd>{{ activity.id }}</dd>
78
+ <dt>Upstream ID</dt>
79
+ <dd>{{ activity.upstream_id }}</dd>
78
80
  <dt>Source path</dt>
79
- <dd>{{ activity.path }}</dd>
81
+ <dd><a href="{{ url_for('.download_original', id=activity.id) }}">{{ activity.path }}</a></dd>
80
82
  </dl>
81
83
 
82
84
  <a href="{{ url_for('.edit', id=activity['id']) }}" class="btn btn-secondary btn-small">Edit</a>
83
85
  <a href="{{ url_for('.trim', id=activity['id']) }}" class="btn btn-secondary btn-small">Trim</a>
86
+ <a class="btn btn-small btn-danger" href="{{ url_for('.delete', id=activity.id) }}"
87
+ onclick="if(!confirm('Are you sure to Delete This?')){ event.preventDefault() }">Delete</a>
84
88
  </div>
85
89
  <div class="col-sm-12 col-md-8">
86
90
  <div id="activity-map" style="height: 500px;" class="mb-3"></div>
@@ -9,7 +9,9 @@
9
9
  <div class="col">
10
10
  <h2>Last 30 days</h2>
11
11
  {{ vega_direct("distance-last-30-days", distance_last_30_days_plot) }}
12
+ {% if elevation_gain_last_30_days_plot %}
12
13
  {{ vega_direct("elevation-gain-last-30-days", elevation_gain_last_30_days_plot) }}
14
+ {% endif %}
13
15
  </div>
14
16
  </div>
15
17
 
@@ -200,6 +200,10 @@
200
200
  </script>
201
201
  {% endmacro %}
202
202
 
203
+ {% macro activity_tag(tag) %}
204
+ <span class="badge" style="background-color: {{ tag.color or '#0d6efd' }}">{{ tag.tag }}</span>
205
+ {% endmacro %}
206
+
203
207
  {% with messages = get_flashed_messages(with_categories=true) %}
204
208
  {% if messages %}
205
209
  {% for category, message in messages %}
@@ -103,7 +103,7 @@
103
103
  value="{{ tag.id }}" id="tag_{{ tag.id }}" {% if tag.id in query.tag %}
104
104
  checked {% endif %}>
105
105
  <label class="form-check-label" for="tag_{{ tag.id }}">
106
- {{ tag.tag }}
106
+ {{ activity_tag(tag) }}
107
107
  </label>
108
108
  </div>
109
109
  {% endfor %}
@@ -10,6 +10,11 @@
10
10
  <input type="text" class="form-control" id="tag" name="tag" value="{{ tag.tag }}" />
11
11
  </div>
12
12
 
13
+ <div class="mb-3">
14
+ <label for="color" class="form-label">Color</label>
15
+ <input type="color" class="form-control" id="color" name="color" value="{{ tag.color or '#0d6efd' }}" />
16
+ </div>
17
+
13
18
  <button type="submit" class="btn btn-primary">Save</button>
14
19
  </form>
15
20
 
@@ -7,7 +7,7 @@
7
7
  <ul>
8
8
  {% for tag in tags %}
9
9
  <li>
10
- <b>{{ tag.tag }}</b>
10
+ {{ activity_tag(tag) }}
11
11
  <a class="btn btn-sm btn-primary" href="{{ url_for('.tags_edit', id=tag.id) }}">Edit</a>
12
12
  </li>
13
13
  {% endfor %}
@@ -103,10 +103,12 @@
103
103
 
104
104
  <div class="tab-content mb-3" id="myTabContent">
105
105
  {% for year, plot in plot_distance_heatmaps.items() %}
106
+ {% if year %}
106
107
  <div class="tab-pane fade {% if loop.last %} show active {% endif %}" id="heatmap-{{ year }}-pane" role="tabpanel"
107
108
  aria-labelledby="heatmap-{{ year }}" tabindex="0">
108
109
  {{ vega_direct("plot_distance_heatmap_%d"|format(year), plot) }}
109
110
  </div>
111
+ {% endif %}
110
112
  {% endfor %}
111
113
  </div>
112
114
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.43.3
3
+ Version: 0.45.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -3,6 +3,7 @@ geo_activity_playground/__main__.py,sha256=eL7NlKydYrzi4ikTvvKmlwkEFxx0V6CXOHOhR
3
3
  geo_activity_playground/alembic/README,sha256=MVlc9TYmr57RbhXET6QxgyCcwWP7w-vLkEsirENqiIQ,38
4
4
  geo_activity_playground/alembic/env.py,sha256=46oMzwSROaAsYuYWTd46txFdRLD3adm_SCn01A_ex8Q,2081
5
5
  geo_activity_playground/alembic/script.py.mako,sha256=g1k4U3D8y4PPYRzW3DH7GEu6yN4EiAr62GgCu6cRpBo,524
6
+ geo_activity_playground/alembic/versions/0f02b92c4f94_add_tag_color.py,sha256=gWcRPLIo1jH3X_x0hh1rMRuiz2wtS10MEIsG6voBCX4,827
6
7
  geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py,sha256=HmvYgHlVodHB7xyigg7zHkFXSi1znWqKfOHcd6y9sZE,3157
7
8
  geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py,sha256=WrmlDllnJECg6cSOeS05wYCa977_SXbJUV5khDSzntw,1082
8
9
  geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py,sha256=YTnnENkQ8WqLz7PFof7tUWNkWcoHGkAfAM52x1N9umo,3029
@@ -14,15 +15,15 @@ geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmar
14
15
  geo_activity_playground/alembic/versions/script.py.mako,sha256=3qBrHBf7F7ChKDUIdiNItiSXrDpgQdM7sR0YKzpaC50,689
15
16
  geo_activity_playground/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
17
  geo_activity_playground/core/activities.py,sha256=apP_-Rg1ub3lh7RARMGXf2BOmJTiahxqpX_soEnYF3E,4681
17
- geo_activity_playground/core/config.py,sha256=eGWWbNfHa6H64AHCnFYTsAJ7-pWi-PhyxL4hjZ4u03U,5256
18
+ geo_activity_playground/core/config.py,sha256=MhNaVS04rH_1KTUE1IrCTVsKtrSqKgMT7mzgPAri3vk,5305
18
19
  geo_activity_playground/core/coordinates.py,sha256=tDfr9mlXhK6E_MMIJ0vYWVCoH0Lq8uyuaqUgaa8i0jg,966
19
- geo_activity_playground/core/datamodel.py,sha256=fBq2D9N8wjWVSSt0bLqxIOBLouRySO74mcG1Z_vsKVI,12786
20
- geo_activity_playground/core/enrichment.py,sha256=Hs1lB__s5TwPLOWPaaheOJeJIBQFUa68NTbCI5M5wWI,7640
20
+ geo_activity_playground/core/datamodel.py,sha256=Bx15g0TJun1yu_deHs3idSNJDAQpvuU39_VIsWD96MY,13303
21
+ geo_activity_playground/core/enrichment.py,sha256=Tju9sKI-V40CmsS9RiNeGz-Zhp_hx1xjlaWzJMrasXI,7640
21
22
  geo_activity_playground/core/heart_rate.py,sha256=-S3WAhS7AOywrw_Lk5jfuo_fu6zvZQ1VtjwEKSycWpU,1542
22
- geo_activity_playground/core/meta_search.py,sha256=Ygh2uySZ9_q-oddU_2vXfzGfXV9rfTyeQFBT7sIiXCI,6657
23
+ geo_activity_playground/core/meta_search.py,sha256=dFWBMnLhdQgUJz2TPYMNFW7XVpyW4QhsH3kPVIhWFJc,6662
23
24
  geo_activity_playground/core/missing_values.py,sha256=HjonaLV0PFMICnuMrbdUNnK9uy_8PBh_RxI5GuEMQK0,250
24
25
  geo_activity_playground/core/parametric_plot.py,sha256=IefPc6lwthxowvjUDA5wu23oBSw9jq399l04gSaNrOQ,3880
25
- geo_activity_playground/core/paths.py,sha256=CLQYA_6ouxHJYgrt_uGp-9sO7MG60prdD6cvVwhA_2g,2670
26
+ geo_activity_playground/core/paths.py,sha256=GGNpqhQL7etn8-NV6hVGLT7yBZYq7AwXnWfX3l_Cj48,2670
26
27
  geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
27
28
  geo_activity_playground/core/raster_map.py,sha256=Cq8dNLdxVQg3Agzn2bmXVu0-8kZf56QrSe-LKNn3jaU,7994
28
29
  geo_activity_playground/core/similarity.py,sha256=L2de3DPRdDeDY5AxZwLDcH7FjHWRWklr41VNU06q9kQ,3117
@@ -44,9 +45,9 @@ geo_activity_playground/heatmap_video.py,sha256=I8i1uVvbbPUXVtvLAROaLy58nQoUPnuM
44
45
  geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
46
  geo_activity_playground/importers/activity_parsers.py,sha256=yD7L5eDOpiLWf6RHSQf4-Nk2S3vgfVHngc9ZlFSrioM,11090
46
47
  geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
47
- geo_activity_playground/importers/directory.py,sha256=4Q7UAFa7ztkgqf4FvPbH2LlrO-7a8Fu7tkYPHOpHm1g,5210
48
+ geo_activity_playground/importers/directory.py,sha256=eTLfcMdc5mt9iDBinuaXjRIA688i6QZQjXNNhm1PKz4,5282
48
49
  geo_activity_playground/importers/strava_api.py,sha256=J0-VXNrLq22fhTcWkQPE5AVrzy5aegC7SBi-UXFtAy4,7576
49
- geo_activity_playground/importers/strava_checkout.py,sha256=3AEFlNr811MdVz_Ww1zz_alptr_3mDvGr188bM3qU7U,9977
50
+ geo_activity_playground/importers/strava_checkout.py,sha256=NwcOje0XxkccXRY5-3CJorYLFuniaFm8VHg4nHwkji4,10045
50
51
  geo_activity_playground/importers/test_csv_parser.py,sha256=nOTVTdlzIY0TDcbWp7xNyNaIO6Mkeu55hVziVl22QE4,1092
51
52
  geo_activity_playground/importers/test_directory.py,sha256=_fn_-y98ZyElbG0BRxAmGFdtGobUShPU86SdEOpuv-A,691
52
53
  geo_activity_playground/importers/test_strava_api.py,sha256=7b8bl5Rh2BctCmvTPEhCadxtUOq3mfzuadD6F5XxRio,398
@@ -54,23 +55,23 @@ geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
54
55
  geo_activity_playground/webui/app.py,sha256=SyRztm6P0pQuq3Vn4g7gFwg1pwyZsElVNZOeljN5jEY,8139
55
56
  geo_activity_playground/webui/authenticator.py,sha256=jtQqvpVHa_eLTAulmvvJgDRoCWOEege49G9zn3MfYk8,1394
56
57
  geo_activity_playground/webui/blueprints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- geo_activity_playground/webui/blueprints/activity_blueprint.py,sha256=QRkSPDJfBNRJn9UHchQm_Rg_b_Or0AqxUzNNzOxTx30,25688
58
+ geo_activity_playground/webui/blueprints/activity_blueprint.py,sha256=EjaTe6VFkRpO9DEkHHurt5GDzAvHHUqgQgfBdiN239Q,26457
58
59
  geo_activity_playground/webui/blueprints/auth_blueprint.py,sha256=_VZeP3VN626BoOOZUkNVnuw9v-cEOrkHz5lhFPmxqMY,784
59
60
  geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py,sha256=xESHzYxlbhz4oNDuxV0A70eVKpFwz84pYC3q_YVZZg8,2812
60
61
  geo_activity_playground/webui/blueprints/calendar_blueprint.py,sha256=4EIBZ8rdXEu3tbl1faVlRwHb8Qp0JuMc3eyxwMkq6g8,2848
61
- geo_activity_playground/webui/blueprints/eddington_blueprints.py,sha256=Umb2wtKmAXuQZEXt0d985qMILtvqc3vxj_yXymnzmUs,8702
62
- geo_activity_playground/webui/blueprints/entry_views.py,sha256=9OuaxbA-J5D2Qn2OSSIjIb-eRTxXY8i1LhlfUk1W60Q,2910
63
- geo_activity_playground/webui/blueprints/equipment_blueprint.py,sha256=juQ5L2BlrECb00LBbiY2yc0b8W_B9Y3fPwtbiaRfgpo,5634
62
+ geo_activity_playground/webui/blueprints/eddington_blueprints.py,sha256=8ctEZWIc0hWjVr3ezNTjxwDHO5M-XzUoupA69RrovB0,8896
63
+ geo_activity_playground/webui/blueprints/entry_views.py,sha256=Lzd-hkflmN3GY24qm6dHazIe78wxnQFdFTUsoZOUH-M,2978
64
+ geo_activity_playground/webui/blueprints/equipment_blueprint.py,sha256=26L2BM7lZo8rDf5Ipara5zA4tfSrvE18POqAZqjzH38,5659
64
65
  geo_activity_playground/webui/blueprints/explorer_blueprint.py,sha256=jMjLxmUhy9ip4Jjw9ABfkdD3CfqXqcE2C0VwQUBZ3WY,15598
65
66
  geo_activity_playground/webui/blueprints/heatmap_blueprint.py,sha256=iHI5YJYhX7ZOlzTgzl2efIRDzt3UMYCx7X4-LVd0MWk,8702
66
67
  geo_activity_playground/webui/blueprints/photo_blueprint.py,sha256=sYGp2XVGodkAifGHbEqpIY-7bBH5R7G7Dwg4HqgxMSY,7269
67
68
  geo_activity_playground/webui/blueprints/plot_builder_blueprint.py,sha256=7HrjpBM-608HSOh0i31Lmt7yDNMfWlEn6G7DlYqlV9w,3031
68
69
  geo_activity_playground/webui/blueprints/search_blueprint.py,sha256=Sv_KL1Cdai26y51qVfI-5jZLhtElREsEar1dbR_VAC4,2275
69
- geo_activity_playground/webui/blueprints/settings_blueprint.py,sha256=788rbsIjbOrLU6kEl2EzxA6ZUwI-t8KR9VW5klS3i0c,17332
70
+ geo_activity_playground/webui/blueprints/settings_blueprint.py,sha256=FLKQ0JTThUlsCBR2TqRvdDGIJpPDycy47iiQof9gbHA,20077
70
71
  geo_activity_playground/webui/blueprints/square_planner_blueprint.py,sha256=xVaxJxmt8Dysl3UL9f2y__LVLtTH2Np1Ust4OSXKRAk,4746
71
72
  geo_activity_playground/webui/blueprints/summary_blueprint.py,sha256=rK4LGR2Rpioy4wSqNYuyRn4WxaWeBLensJ3PmAd-ouY,11469
72
73
  geo_activity_playground/webui/blueprints/tile_blueprint.py,sha256=YzZf9OrNdjhc1_j4MtO1DMcw1uCv29ueNsYd-mWqgbg,837
73
- geo_activity_playground/webui/blueprints/upload_blueprint.py,sha256=9IeaxS7ObqLhe8eju8nVXUt2FfEOw9mMz9qxFdWUJ3k,4607
74
+ geo_activity_playground/webui/blueprints/upload_blueprint.py,sha256=ZPeUFGQ0gHeX71zVLLrHWpPq1NzCk4GlZIxYSdR1rhA,4799
74
75
  geo_activity_playground/webui/columns.py,sha256=hSW8bFVKRUMBlWZYAN-tZ_tfED3VDQK75e4Zzw7_jZ0,642
75
76
  geo_activity_playground/webui/flasher.py,sha256=Covc1D9cO_jjokRWnvyiXCc2tfp3aZ8XkNqFdA1AXtk,500
76
77
  geo_activity_playground/webui/plot_util.py,sha256=5Uesjj-xcMskQX2z9viDZYHSxLGrH2a5dHA1ogsJW9U,261
@@ -113,7 +114,7 @@ geo_activity_playground/webui/templates/activity/day.html.j2,sha256=CHEvxlZralCm
113
114
  geo_activity_playground/webui/templates/activity/edit.html.j2,sha256=r979JPqaZi_2ymTykxpkjdpw0D2tsB9VJaf7OaGPaME,1961
114
115
  geo_activity_playground/webui/templates/activity/lines.html.j2,sha256=_ZDg1ruW-9UMJfOudy1-uY_-IcSSaagq7tPCih5Bb8g,1079
115
116
  geo_activity_playground/webui/templates/activity/name.html.j2,sha256=7Wbh3IrVL5lMRve467H0P10Shn5FzGpaXLhV0H-X4Hk,2725
116
- geo_activity_playground/webui/templates/activity/show.html.j2,sha256=uSTjr0hm8tjBOWb71xKReKuUsVv_5PaoKaqdYUe1noI,10665
117
+ geo_activity_playground/webui/templates/activity/show.html.j2,sha256=obKXSvRJxpeo53k7aCUrrhJSus9msuPGStIQJQQHpes,10975
117
118
  geo_activity_playground/webui/templates/activity/trim.html.j2,sha256=3oAXQab6QqWjGBC9KCvWNOVn8uRmxoDLj3hx_O63TXc,1836
118
119
  geo_activity_playground/webui/templates/auth/index.html.j2,sha256=ILQ5HvTEYc3OrtOAIFt1VrqWorVD70V9DC342znmP70,579
119
120
  geo_activity_playground/webui/templates/bubble_chart/index.html.j2,sha256=yd7lWjtxxVmJZqCiXb0Y1gMEOQ7LQYJXEdpE7JB1OZY,1616
@@ -126,14 +127,14 @@ geo_activity_playground/webui/templates/equipment/index.html.j2,sha256=wwrGmfCCB
126
127
  geo_activity_playground/webui/templates/explorer/index.html.j2,sha256=3t9ikAF6oMvEaVlS3Kb1tj9ngomIQlatzqPnqVsEDKA,6908
127
128
  geo_activity_playground/webui/templates/explorer/server-side.html.j2,sha256=3eYh3mmgPDmxFjH6Ofx1iw-jptcD9ctZixYh59mka3w,2496
128
129
  geo_activity_playground/webui/templates/heatmap/index.html.j2,sha256=uM-l4gmDKw6307ZH_zb8zroMTKBuOkrR0Bu4fTEJE0s,1231
129
- geo_activity_playground/webui/templates/home.html.j2,sha256=L0wVZ6RA3mel1Wt7ZD7PbaMUEEcdO2XLDy1e1ZdRPUY,2697
130
- geo_activity_playground/webui/templates/page.html.j2,sha256=nwuwWfmFe7RnVmXGEYR8cu6en5gKbi2-xSyRxKQC9sc,12368
130
+ geo_activity_playground/webui/templates/home.html.j2,sha256=EBLKVNgLYgTPzdFutoSkdlbu_UCCat5ItDFlYyRqcAg,2767
131
+ geo_activity_playground/webui/templates/page.html.j2,sha256=QJtRcHciEEWUUf6LHll2RUm0NRXSkNGIYn1FHOKSrM4,12534
131
132
  geo_activity_playground/webui/templates/photo/map.html.j2,sha256=MWhqt5Q8ExiRhgxndcEnwngOj1qw0E0u4hKuiuY24Gg,1437
132
133
  geo_activity_playground/webui/templates/photo/new.html.j2,sha256=GGLejO4ap6ZMe54jZP39ktSLkdw5j67bf5PTlHEK7qc,383
133
134
  geo_activity_playground/webui/templates/plot_builder/edit.html.j2,sha256=x5Ki425me3HY6CcBQ37le9g8rCpbOxFVkdr0N_L84-g,2230
134
135
  geo_activity_playground/webui/templates/plot_builder/index.html.j2,sha256=fBuGLT2HIwlgz5eGeKXOdIDqzDSQoY99w-hyt_0JP-w,832
135
136
  geo_activity_playground/webui/templates/search/index.html.j2,sha256=tZ2RwiaC1cLCLfcxbDvpnLSjPeqnTkByAT6ncVitnLw,1318
136
- geo_activity_playground/webui/templates/search_form.html.j2,sha256=QL0inDTaEHPxoQGs3EB19bD0jAMnUkOrb_r5OoQn7JU,8251
137
+ geo_activity_playground/webui/templates/search_form.html.j2,sha256=BBxT2aAUlOZ41d2hE9EKX0Jcr0FKLCp_9cgWYyrVtE8,8261
137
138
  geo_activity_playground/webui/templates/settings/admin-password.html.j2,sha256=VYwddpObD1RpeTH5Dm4y7VtmT7kwURDCIjxyzJeq08c,495
138
139
  geo_activity_playground/webui/templates/settings/color-schemes.html.j2,sha256=iR91Wxd2_TMuIo9dBDZBrWSUGHNwTwzC6O8oNH-XBt4,1653
139
140
  geo_activity_playground/webui/templates/settings/heart-rate.html.j2,sha256=UPT3MegRgSeff36lhCo0l3ZwhqNSIg5gM6h2s32GkCY,4255
@@ -145,15 +146,15 @@ geo_activity_playground/webui/templates/settings/privacy-zones.html.j2,sha256=Kp
145
146
  geo_activity_playground/webui/templates/settings/segmentation.html.j2,sha256=QV72TZcIxqql-vEsq2lKHzo5UxoxeeXkRA9se46GWKU,1187
146
147
  geo_activity_playground/webui/templates/settings/sharepic.html.j2,sha256=qZkfEpd4CtKKMaSSVadqvNEgMRYLV-0X-pw5-nJvukk,678
147
148
  geo_activity_playground/webui/templates/settings/strava.html.j2,sha256=GCE5gskQ6xJ8AM1qGrrUVLDOiuqg510mWzzsZjia0gk,2211
148
- geo_activity_playground/webui/templates/settings/tags-edit.html.j2,sha256=OAXg_P-ZN7-LLvvzP0xVtadxb0t0TDD8xiWniL4U1FM,378
149
- geo_activity_playground/webui/templates/settings/tags-list.html.j2,sha256=AsFhWyMejbNMvOgWhqRtAiJGK8eqkHgECZ7mKFpQWkA,366
149
+ geo_activity_playground/webui/templates/settings/tags-edit.html.j2,sha256=Lna2QBacuMwaFODGCVulOpHSHHjqCdhNJg2c6i-ogY0,586
150
+ geo_activity_playground/webui/templates/settings/tags-list.html.j2,sha256=6giFWtVCTLXLC_Ojh56XhB_1Rouit9YzIs_YHIiayLg,369
150
151
  geo_activity_playground/webui/templates/settings/tags-new.html.j2,sha256=xi6KbwydDVrUJM4_ty4KbMa74k3QaoyZhZAn2paERnM,358
151
152
  geo_activity_playground/webui/templates/square_planner/index.html.j2,sha256=-OnY2nQCgZCslOzf28ogZwFykwF8tZm7PgFwOE3eBDk,8176
152
- geo_activity_playground/webui/templates/summary/index.html.j2,sha256=soGpTK-pD81YBlMWNWLnkWeEmVIbI5JwmGZQUN2c3DI,9151
153
+ geo_activity_playground/webui/templates/summary/index.html.j2,sha256=1kTuwWvOM3McEdY8N5rKj-R4CC6uqswl7NsGVF8hsDA,9185
153
154
  geo_activity_playground/webui/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
154
155
  geo_activity_playground/webui/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
155
- geo_activity_playground-0.43.3.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
156
- geo_activity_playground-0.43.3.dist-info/METADATA,sha256=ax34y9rdLoVSrC7nWx7bNvudjfo1bQWkZ5VXJee1nMQ,1850
157
- geo_activity_playground-0.43.3.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
158
- geo_activity_playground-0.43.3.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
159
- geo_activity_playground-0.43.3.dist-info/RECORD,,
156
+ geo_activity_playground-0.45.0.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
157
+ geo_activity_playground-0.45.0.dist-info/METADATA,sha256=z-NGEoMJeCU-Zknez48tKzYt4az7GzkQalxWAEpp6JU,1850
158
+ geo_activity_playground-0.45.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
159
+ geo_activity_playground-0.45.0.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
160
+ geo_activity_playground-0.45.0.dist-info/RECORD,,