geo-activity-playground 0.33.3__py3-none-any.whl → 0.34.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 (27) hide show
  1. geo_activity_playground/explorer/video.py +2 -1
  2. geo_activity_playground/importers/strava_checkout.py +8 -2
  3. geo_activity_playground/webui/activity/blueprint.py +7 -0
  4. geo_activity_playground/webui/activity/controller.py +85 -15
  5. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +5 -1
  6. geo_activity_playground/webui/equipment/controller.py +1 -1
  7. geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +1 -0
  8. geo_activity_playground/webui/static/MarkerCluster.Default.css +60 -0
  9. geo_activity_playground/webui/static/MarkerCluster.css +14 -0
  10. geo_activity_playground/webui/static/bootstrap.min.css +6 -0
  11. geo_activity_playground/webui/static/fullscreen.png +0 -0
  12. geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
  13. geo_activity_playground/webui/static/leaflet.css +661 -0
  14. geo_activity_playground/webui/static/leaflet.fullscreen.css +40 -0
  15. geo_activity_playground/webui/static/leaflet.js +6 -0
  16. geo_activity_playground/webui/static/leaflet.markercluster.js +3 -0
  17. geo_activity_playground/webui/static/table-sort.min.js +8 -0
  18. geo_activity_playground/webui/static/vega-embed@6 +7 -0
  19. geo_activity_playground/webui/static/vega-lite@4 +2 -0
  20. geo_activity_playground/webui/static/vega@5 +2 -0
  21. geo_activity_playground/webui/summary/controller.py +11 -10
  22. geo_activity_playground/webui/templates/page.html.j2 +14 -16
  23. {geo_activity_playground-0.33.3.dist-info → geo_activity_playground-0.34.1.dist-info}/METADATA +5 -6
  24. {geo_activity_playground-0.33.3.dist-info → geo_activity_playground-0.34.1.dist-info}/RECORD +27 -13
  25. {geo_activity_playground-0.33.3.dist-info → geo_activity_playground-0.34.1.dist-info}/LICENSE +0 -0
  26. {geo_activity_playground-0.33.3.dist-info → geo_activity_playground-0.34.1.dist-info}/WHEEL +0 -0
  27. {geo_activity_playground-0.33.3.dist-info → geo_activity_playground-0.34.1.dist-info}/entry_points.txt +0 -0
@@ -9,13 +9,14 @@ from typing import Tuple
9
9
 
10
10
  import numpy as np
11
11
  import pandas as pd
12
- import scipy.interpolate
13
12
  from PIL import Image
14
13
  from PIL import ImageEnhance
15
14
  from tqdm import tqdm
16
15
 
17
16
  from ..core.tiles import get_tile
18
17
 
18
+ # import scipy.interpolate
19
+
19
20
 
20
21
  def build_image(
21
22
  center_x: float,
@@ -146,7 +146,7 @@ def float_with_comma_or_period(x: str) -> Optional[float]:
146
146
 
147
147
  def import_from_strava_checkout() -> None:
148
148
  checkout_path = pathlib.Path("Strava Export")
149
- with open(checkout_path / "activities.csv") as f:
149
+ with open(checkout_path / "activities.csv", encoding="utf-8") as f:
150
150
  rows = parse_csv(f.read())
151
151
  header = rows[0]
152
152
 
@@ -159,9 +159,15 @@ def import_from_strava_checkout() -> None:
159
159
 
160
160
  if header[0] == EXPECTED_COLUMNS[0]:
161
161
  dayfirst = False
162
- if header[0] == "Aktivitäts-ID":
162
+ elif header[0] == "Aktivitäts-ID":
163
163
  header = EXPECTED_COLUMNS
164
164
  dayfirst = True
165
+ else:
166
+ logger.error(
167
+ f"You are trying to import a Strava checkout where the `activities.csv` contains an unexpected header format. In order to import this, we need to map these to the English ones. Unfortunately Strava often changes the number of columns. This means that the program needs to be updated to match the new Strava export format. Please go to https://github.com/martin-ueding/geo-activity-playground/issues and open a new issue and share the following output in the ticket:"
168
+ )
169
+ print(header)
170
+ sys.exit(1)
165
171
 
166
172
  table = {
167
173
  header[i]: [rows[r][i] for r in range(1, len(rows))] for i in range(len(header))
@@ -55,6 +55,13 @@ def make_activity_blueprint(
55
55
  **activity_controller.render_day(int(year), int(month), int(day)),
56
56
  )
57
57
 
58
+ @blueprint.route("/day-sharepic/<year>/<month>/<day>/sharepic.png")
59
+ def day_sharepic(year: str, month: str, day: str):
60
+ return Response(
61
+ activity_controller.render_day_sharepic(int(year), int(month), int(day)),
62
+ mimetype="image/png",
63
+ )
64
+
58
65
  @blueprint.route("/name/<name>")
59
66
  def name(name: str):
60
67
  return render_template(
@@ -170,8 +170,24 @@ class ActivityController:
170
170
  "date": datetime.date(year, month, day).isoformat(),
171
171
  "total_distance": activities_that_day["distance_km"].sum(),
172
172
  "total_elapsed_time": activities_that_day["elapsed_time"].sum(),
173
+ "day": day,
174
+ "month": month,
175
+ "year": year,
173
176
  }
174
177
 
178
+ def render_day_sharepic(self, year: int, month: int, day: int) -> bytes:
179
+ meta = self._repository.meta
180
+ selection = meta["start"].dt.date == datetime.date(year, month, day)
181
+ activities_that_day = meta.loc[selection]
182
+
183
+ time_series = [
184
+ self._repository.get_time_series(activity_id)
185
+ for activity_id in activities_that_day["id"]
186
+ ]
187
+ assert len(activities_that_day) > 0
188
+ assert len(time_series) > 0
189
+ return (make_day_sharepic(activities_that_day, time_series, self._config),)
190
+
175
191
  def render_all(self) -> dict:
176
192
  cmap = matplotlib.colormaps["Dark2"]
177
193
  fc = geojson.FeatureCollection(
@@ -452,14 +468,10 @@ def pixels_in_bounds(bounds: PixelBounds) -> int:
452
468
  return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
453
469
 
454
470
 
455
- def make_sharepic(
456
- activity: ActivityMeta,
457
- time_series: pd.DataFrame,
458
- sharepic_suppressed_fields: list[str],
459
- config: Config,
460
- ) -> bytes:
461
- tile_x = time_series["x"]
462
- tile_y = time_series["y"]
471
+ def make_sharepic_base(time_series_list: list[pd.DataFrame], config: Config):
472
+ all_time_series = pd.concat(time_series_list)
473
+ tile_x = all_time_series["x"]
474
+ tile_y = all_time_series["y"]
463
475
  tile_width = tile_x.max() - tile_x.min()
464
476
  tile_height = tile_y.max() - tile_y.min()
465
477
 
@@ -495,16 +507,32 @@ def make_sharepic(
495
507
  img = Image.fromarray((background * 255).astype("uint8"), "RGB")
496
508
  draw = ImageDraw.Draw(img, mode="RGBA")
497
509
 
498
- for _, group in time_series.groupby("segment_id"):
499
- yx = list(
500
- zip(
501
- (tile_xz - tile_xz_center[0]) * OSM_TILE_SIZE + target_width / 2,
502
- (tile_yz - tile_xz_center[1]) * OSM_TILE_SIZE + target_map_height / 2,
510
+ for time_series in time_series_list:
511
+ for _, group in time_series.groupby("segment_id"):
512
+ yx = list(
513
+ zip(
514
+ (tile_xz - tile_xz_center[0]) * OSM_TILE_SIZE + target_width / 2,
515
+ (tile_yz - tile_xz_center[1]) * OSM_TILE_SIZE
516
+ + target_map_height / 2,
517
+ )
503
518
  )
504
- )
505
519
 
506
- draw.line(yx, fill="red", width=4)
520
+ draw.line(yx, fill="red", width=4)
521
+
522
+ return img
523
+
507
524
 
525
+ def make_sharepic(
526
+ activity: ActivityMeta,
527
+ time_series: pd.DataFrame,
528
+ sharepic_suppressed_fields: list[str],
529
+ config: Config,
530
+ ) -> bytes:
531
+ footer_height = 100
532
+
533
+ img = make_sharepic_base([time_series], config)
534
+
535
+ draw = ImageDraw.Draw(img, mode="RGBA")
508
536
  draw.rectangle(
509
537
  [0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
510
538
  )
@@ -545,6 +573,48 @@ def make_sharepic(
545
573
  return bytes(f.getbuffer())
546
574
 
547
575
 
576
+ def make_day_sharepic(
577
+ activities: pd.DataFrame,
578
+ time_series_list: list[pd.DataFrame],
579
+ config: Config,
580
+ ) -> bytes:
581
+ footer_height = 100
582
+
583
+ img = make_sharepic_base(time_series_list, config)
584
+
585
+ draw = ImageDraw.Draw(img, mode="RGBA")
586
+ draw.rectangle(
587
+ [0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
588
+ )
589
+
590
+ date = activities.iloc[0]["start"].date()
591
+ distance_km = activities["distance_km"].sum()
592
+ elapsed_time: pd.Timedelta = activities["elapsed_time"].sum()
593
+ elapsed_time = elapsed_time.round("s")
594
+
595
+ facts = {
596
+ "date": f"{date}",
597
+ "distance_km": f"{distance_km:.1f} km",
598
+ "elapsed_time": re.sub(r"^0 days ", "", f"{elapsed_time}"),
599
+ }
600
+
601
+ draw.text(
602
+ (35, img.height - footer_height + 10),
603
+ " ".join(facts.values()),
604
+ font_size=20,
605
+ )
606
+
607
+ draw.text(
608
+ (img.width - 250, img.height - 20),
609
+ "Map: © Open Street Map Contributors",
610
+ font_size=14,
611
+ )
612
+
613
+ f = io.BytesIO()
614
+ img.save(f, format="png")
615
+ return bytes(f.getbuffer())
616
+
617
+
548
618
  def _extract_heart_rate_zones(
549
619
  time_series: pd.DataFrame, heart_rate_zone_computer: HeartRateZoneComputer
550
620
  ) -> Optional[pd.DataFrame]:
@@ -30,7 +30,7 @@
30
30
  <ol>
31
31
  {% for activity in activities %}
32
32
  <li><span style="color: {{ activity['color'] }};">█</span> <a
33
- href="{{ url_for('activity.show', id=activity.id) }}">{{
33
+ href="{{ url_for('.show', id=activity.id) }}">{{
34
34
  activity.name }}</a></li>
35
35
  {% endfor %}
36
36
  </ol>
@@ -80,4 +80,8 @@
80
80
  </div>
81
81
  </div>
82
82
 
83
+ <h2>Share picture</h2>
84
+
85
+ <p><img src="{{ url_for('.day_sharepic', year=year, month=month, day=day) }}" /></p>
86
+
83
87
  {% endblock %}
@@ -87,7 +87,7 @@ class EquipmentController:
87
87
  )
88
88
  .mark_bar()
89
89
  .encode(
90
- alt.X("kind", title="Year"),
90
+ alt.X("kind", title="Kind"),
91
91
  alt.Y("sum(distance_km)", title="Distance / km"),
92
92
  )
93
93
  .to_json(format="vega")
@@ -0,0 +1 @@
1
+ L.Control.Fullscreen=L.Control.extend({options:{position:"topleft",title:{"false":"View Fullscreen","true":"Exit Fullscreen"}},onAdd:function(map){var container=L.DomUtil.create("div","leaflet-control-fullscreen leaflet-bar leaflet-control");this.link=L.DomUtil.create("a","leaflet-control-fullscreen-button leaflet-bar-part",container);this.link.href="#";this._map=map;this._map.on("fullscreenchange",this._toggleTitle,this);this._toggleTitle();L.DomEvent.on(this.link,"click",this._click,this);return container},_click:function(e){L.DomEvent.stopPropagation(e);L.DomEvent.preventDefault(e);this._map.toggleFullscreen(this.options)},_toggleTitle:function(){this.link.title=this.options.title[this._map.isFullscreen()]}});L.Map.include({isFullscreen:function(){return this._isFullscreen||false},toggleFullscreen:function(options){var container=this.getContainer();if(this.isFullscreen()){if(options&&options.pseudoFullscreen){this._disablePseudoFullscreen(container)}else if(document.exitFullscreen){document.exitFullscreen()}else if(document.mozCancelFullScreen){document.mozCancelFullScreen()}else if(document.webkitCancelFullScreen){document.webkitCancelFullScreen()}else if(document.msExitFullscreen){document.msExitFullscreen()}else{this._disablePseudoFullscreen(container)}}else{if(options&&options.pseudoFullscreen){this._enablePseudoFullscreen(container)}else if(container.requestFullscreen){container.requestFullscreen()}else if(container.mozRequestFullScreen){container.mozRequestFullScreen()}else if(container.webkitRequestFullscreen){container.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT)}else if(container.msRequestFullscreen){container.msRequestFullscreen()}else{this._enablePseudoFullscreen(container)}}},_enablePseudoFullscreen:function(container){L.DomUtil.addClass(container,"leaflet-pseudo-fullscreen");this._setFullscreen(true);this.invalidateSize();this.fire("fullscreenchange")},_disablePseudoFullscreen:function(container){L.DomUtil.removeClass(container,"leaflet-pseudo-fullscreen");this._setFullscreen(false);this.invalidateSize();this.fire("fullscreenchange")},_setFullscreen:function(fullscreen){this._isFullscreen=fullscreen;var container=this.getContainer();if(fullscreen){L.DomUtil.addClass(container,"leaflet-fullscreen-on")}else{L.DomUtil.removeClass(container,"leaflet-fullscreen-on")}},_onFullscreenChange:function(e){var fullscreenElement=document.fullscreenElement||document.mozFullScreenElement||document.webkitFullscreenElement||document.msFullscreenElement;if(fullscreenElement===this.getContainer()&&!this._isFullscreen){this._setFullscreen(true);this.fire("fullscreenchange")}else if(fullscreenElement!==this.getContainer()&&this._isFullscreen){this._setFullscreen(false);this.fire("fullscreenchange")}}});L.Map.mergeOptions({fullscreenControl:false});L.Map.addInitHook(function(){if(this.options.fullscreenControl){this.fullscreenControl=new L.Control.Fullscreen(this.options.fullscreenControl);this.addControl(this.fullscreenControl)}var fullscreenchange;if("onfullscreenchange"in document){fullscreenchange="fullscreenchange"}else if("onmozfullscreenchange"in document){fullscreenchange="mozfullscreenchange"}else if("onwebkitfullscreenchange"in document){fullscreenchange="webkitfullscreenchange"}else if("onmsfullscreenchange"in document){fullscreenchange="MSFullscreenChange"}if(fullscreenchange){var onFullscreenChange=L.bind(this._onFullscreenChange,this);this.whenReady(function(){L.DomEvent.on(document,fullscreenchange,onFullscreenChange)});this.on("unload",function(){L.DomEvent.off(document,fullscreenchange,onFullscreenChange)})}});L.control.fullscreen=function(options){return new L.Control.Fullscreen(options)};
@@ -0,0 +1,60 @@
1
+ .marker-cluster-small {
2
+ background-color: rgba(181, 226, 140, 0.6);
3
+ }
4
+ .marker-cluster-small div {
5
+ background-color: rgba(110, 204, 57, 0.6);
6
+ }
7
+
8
+ .marker-cluster-medium {
9
+ background-color: rgba(241, 211, 87, 0.6);
10
+ }
11
+ .marker-cluster-medium div {
12
+ background-color: rgba(240, 194, 12, 0.6);
13
+ }
14
+
15
+ .marker-cluster-large {
16
+ background-color: rgba(253, 156, 115, 0.6);
17
+ }
18
+ .marker-cluster-large div {
19
+ background-color: rgba(241, 128, 23, 0.6);
20
+ }
21
+
22
+ /* IE 6-8 fallback colors */
23
+ .leaflet-oldie .marker-cluster-small {
24
+ background-color: rgb(181, 226, 140);
25
+ }
26
+ .leaflet-oldie .marker-cluster-small div {
27
+ background-color: rgb(110, 204, 57);
28
+ }
29
+
30
+ .leaflet-oldie .marker-cluster-medium {
31
+ background-color: rgb(241, 211, 87);
32
+ }
33
+ .leaflet-oldie .marker-cluster-medium div {
34
+ background-color: rgb(240, 194, 12);
35
+ }
36
+
37
+ .leaflet-oldie .marker-cluster-large {
38
+ background-color: rgb(253, 156, 115);
39
+ }
40
+ .leaflet-oldie .marker-cluster-large div {
41
+ background-color: rgb(241, 128, 23);
42
+ }
43
+
44
+ .marker-cluster {
45
+ background-clip: padding-box;
46
+ border-radius: 20px;
47
+ }
48
+ .marker-cluster div {
49
+ width: 30px;
50
+ height: 30px;
51
+ margin-left: 5px;
52
+ margin-top: 5px;
53
+
54
+ text-align: center;
55
+ border-radius: 15px;
56
+ font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
57
+ }
58
+ .marker-cluster span {
59
+ line-height: 30px;
60
+ }
@@ -0,0 +1,14 @@
1
+ .leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
2
+ -webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
3
+ -moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
4
+ -o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
5
+ transition: transform 0.3s ease-out, opacity 0.3s ease-in;
6
+ }
7
+
8
+ .leaflet-cluster-spider-leg {
9
+ /* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
10
+ -webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
11
+ -moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
12
+ -o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
13
+ transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
14
+ }