geo-activity-playground 1.3.2__py3-none-any.whl → 1.4.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 (21) hide show
  1. geo_activity_playground/__main__.py +8 -0
  2. geo_activity_playground/core/photos.py +39 -0
  3. geo_activity_playground/core/raster_map.py +6 -0
  4. geo_activity_playground/explorer/tile_visits.py +64 -5
  5. geo_activity_playground/importers/strava_checkout.py +11 -2
  6. geo_activity_playground/webui/app.py +7 -0
  7. geo_activity_playground/webui/blueprints/explorer_blueprint.py +67 -97
  8. geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +15 -4
  9. geo_activity_playground/webui/blueprints/heatmap_blueprint.py +3 -10
  10. geo_activity_playground/webui/blueprints/photo_blueprint.py +1 -26
  11. geo_activity_playground/webui/static/leaflet/images/layers-2x.png +0 -0
  12. geo_activity_playground/webui/static/leaflet/images/layers.png +0 -0
  13. geo_activity_playground/webui/static/server-side-explorer.js +49 -13
  14. geo_activity_playground/webui/templates/explorer/server-side.html.j2 +0 -8
  15. geo_activity_playground/webui/templates/heatmap/index.html.j2 +6 -0
  16. geo_activity_playground/webui/templates/page.html.j2 +4 -1
  17. {geo_activity_playground-1.3.2.dist-info → geo_activity_playground-1.4.1.dist-info}/METADATA +1 -1
  18. {geo_activity_playground-1.3.2.dist-info → geo_activity_playground-1.4.1.dist-info}/RECORD +21 -18
  19. {geo_activity_playground-1.3.2.dist-info → geo_activity_playground-1.4.1.dist-info}/LICENSE +0 -0
  20. {geo_activity_playground-1.3.2.dist-info → geo_activity_playground-1.4.1.dist-info}/WHEEL +0 -0
  21. {geo_activity_playground-1.3.2.dist-info → geo_activity_playground-1.4.1.dist-info}/entry_points.txt +0 -0
@@ -4,6 +4,7 @@ import pathlib
4
4
 
5
5
  import coloredlogs
6
6
 
7
+ from .core.photos import main_inspect_photo
7
8
  from .explorer.video import explorer_video_main
8
9
  from .heatmap_video import main_heatmap_video
9
10
  from .importers.strava_checkout import convert_strava_checkout
@@ -73,6 +74,13 @@ def main() -> None:
73
74
  subparser.add_argument("--video-height", type=int, default=1080)
74
75
  subparser.set_defaults(func=main_heatmap_video)
75
76
 
77
+ subparser = subparsers.add_parser(
78
+ "inspect-photo",
79
+ help="Extract EXIF data from the image to see how it would be imported",
80
+ )
81
+ subparser.add_argument("path", type=pathlib.Path)
82
+ subparser.set_defaults(func=main_inspect_photo)
83
+
76
84
  options = parser.parse_args()
77
85
  coloredlogs.install(
78
86
  fmt="%(asctime)s %(name)s %(levelname)s %(message)s",
@@ -0,0 +1,39 @@
1
+ import argparse
2
+ import datetime
3
+ import pathlib
4
+ import pprint
5
+ import zoneinfo
6
+
7
+ import dateutil.parser
8
+ import exifread
9
+
10
+
11
+ def ratio_to_decimal(numbers: list[exifread.utils.Ratio]) -> float:
12
+ deg, min, sec = numbers.values
13
+ return deg.decimal() + min.decimal() / 60 + sec.decimal() / 3600
14
+
15
+
16
+ def get_metadata_from_image(path: pathlib.Path) -> dict:
17
+ with open(path, "rb") as f:
18
+ tags = exifread.process_file(f)
19
+ metadata = {}
20
+ try:
21
+ metadata["latitude"] = ratio_to_decimal(tags["GPS GPSLatitude"])
22
+ metadata["longitude"] = ratio_to_decimal(tags["GPS GPSLongitude"])
23
+ except KeyError:
24
+ pass
25
+ try:
26
+ metadata["time"] = dateutil.parser.parse(
27
+ str(tags["EXIF DateTimeOriginal"])
28
+ + str(tags.get("EXIF OffsetTime", "+00:00"))
29
+ ).astimezone(zoneinfo.ZoneInfo("UTC"))
30
+ except KeyError:
31
+ pass
32
+
33
+ return metadata
34
+
35
+
36
+ def main_inspect_photo(options: argparse.Namespace) -> None:
37
+ path: pathlib.Path = options.path
38
+ metadata = get_metadata_from_image(path)
39
+ pprint.pprint(metadata)
@@ -283,3 +283,9 @@ class PastelImageTransform(ImageTransform):
283
283
  averaged_tile = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2)
284
284
  grayscale_tile = np.dstack((averaged_tile, averaged_tile, averaged_tile))
285
285
  return self._factor * grayscale_tile + (1 - self._factor) * image
286
+
287
+
288
+ class InverseGrayscaleImageTransform(ImageTransform):
289
+ def transform_image(self, image: np.ndarray) -> np.ndarray:
290
+ image = np.sum(image * [0.2126, 0.7152, 0.0722], axis=2) # to grayscale
291
+ return 1 - np.dstack((image, image, image)) # to rgb
@@ -13,6 +13,8 @@ from tqdm import tqdm
13
13
 
14
14
  from ..core.activities import ActivityRepository
15
15
  from ..core.config import Config
16
+ from ..core.datamodel import Activity
17
+ from ..core.datamodel import DB
16
18
  from ..core.paths import atomic_open
17
19
  from ..core.tasks import try_load_pickle
18
20
  from ..core.tasks import work_tracker_path
@@ -20,10 +22,41 @@ from ..core.tasks import WorkTracker
20
22
  from ..core.tiles import adjacent_to
21
23
  from ..core.tiles import interpolate_missing_tile
22
24
 
25
+ # import sqlalchemy as sa
26
+
27
+ # from sqlalchemy import Column
28
+ # from sqlalchemy import ForeignKey
29
+ # from sqlalchemy import String
30
+ # from sqlalchemy import Table
31
+ # from sqlalchemy.orm import DeclarativeBase
32
+ # from sqlalchemy.orm import Mapped
33
+ # from sqlalchemy.orm import mapped_column
34
+ # from sqlalchemy.orm import relationship
23
35
 
24
36
  logger = logging.getLogger(__name__)
25
37
 
26
38
 
39
+ # class VisitedTile(DB.Model):
40
+ # __tablename__ = "visited_tiles"
41
+
42
+ # zoom: Mapped[int] = mapped_column(primary_key=True)
43
+ # x: Mapped[int] = mapped_column(primary_key=True)
44
+ # y: Mapped[int] = mapped_column(primary_key=True)
45
+
46
+ # first_activity_id: Mapped[int] = mapped_column(
47
+ # ForeignKey("Activity.id", name="first_activity_id"), nullable=True
48
+ # )
49
+ # first_activity: Mapped[Activity] = relationship(back_populates="activities")
50
+ # first_visit: Mapped[Optional[datetime.datetime]] = mapped_column(
51
+ # sa.DateTime, nullable=True
52
+ # )
53
+
54
+ # __table_args__ = (
55
+ # sa.PrimaryKeyConstraint(zoom, x, y),
56
+ # {},
57
+ # )
58
+
59
+
27
60
  class TileInfo(TypedDict):
28
61
  activity_ids: set[int]
29
62
  first_time: datetime.datetime
@@ -58,7 +91,6 @@ class TileState(TypedDict):
58
91
  tile_visits: dict[int, dict[tuple[int, int], TileInfo]]
59
92
  tile_history: dict[int, pd.DataFrame]
60
93
  activities_per_tile: dict[int, dict[tuple[int, int], set[int]]]
61
- processed_activities: set[int]
62
94
  evolution_state: dict[int, TileEvolutionState]
63
95
  version: int
64
96
 
@@ -99,7 +131,6 @@ def make_tile_state() -> TileState:
99
131
  "tile_visits": collections.defaultdict(make_defaultdict_dict),
100
132
  "tile_history": collections.defaultdict(pd.DataFrame),
101
133
  "activities_per_tile": collections.defaultdict(make_defaultdict_set),
102
- "processed_activities": set(),
103
134
  "evolution_state": collections.defaultdict(TileEvolutionState),
104
135
  "version": TILE_STATE_VERSION,
105
136
  }
@@ -122,6 +153,9 @@ def _consistency_check(
122
153
 
123
154
  for zoom, tile_visits in tile_visit_accessor.tile_state["tile_visits"].items():
124
155
  for tile, meta in tile_visits.items():
156
+ if not pd.isna(meta["first_time"]) and meta["first_time"].tzinfo is None:
157
+ logger.info("Tile visits are stored without time zone.")
158
+ return False
125
159
  if meta["first_id"] not in present_activity_ids:
126
160
  logger.info(f"Activity {meta['first_id']} have been deleted.")
127
161
  return False
@@ -143,10 +177,35 @@ def compute_tile_visits_new(
143
177
  work_tracker.reset()
144
178
 
145
179
  for activity_id in tqdm(
146
- work_tracker.filter(repository.get_activity_ids()), desc="Tile visits", delay=2
180
+ work_tracker.filter(repository.get_activity_ids()), desc="Tile visits", delay=1
147
181
  ):
148
182
  _process_activity(repository, tile_visit_accessor.tile_state, activity_id)
149
183
  work_tracker.mark_done(activity_id)
184
+
185
+ for zoom in reversed(range(20)):
186
+ tile_state = tile_visit_accessor.tile_state
187
+ if not (
188
+ tile_state["tile_history"][zoom]["time"].diff().dropna()
189
+ >= datetime.timedelta(seconds=0)
190
+ ).all():
191
+ logger.warning(
192
+ f"The order of the tile history at {zoom=} is not chronological, resetting."
193
+ )
194
+ new_tile_history_soa: dict[str, list] = {
195
+ "activity_id": [],
196
+ "time": [],
197
+ "tile_x": [],
198
+ "tile_y": [],
199
+ }
200
+ for tile, visit in tile_state["tile_visits"][zoom].items():
201
+ new_tile_history_soa["activity_id"].append(visit["first_id"])
202
+ new_tile_history_soa["time"].append(visit["first_time"])
203
+ new_tile_history_soa["tile_x"].append(tile[0])
204
+ new_tile_history_soa["tile_y"].append(tile[1])
205
+ tile_state["tile_history"][zoom] = pd.DataFrame(new_tile_history_soa)
206
+ tile_state["tile_history"][zoom].sort_values("time", inplace=True)
207
+ # Reset the evolution state.
208
+ tile_state["evolution_state"] = collections.defaultdict(TileEvolutionState)
150
209
  tile_visit_accessor.save()
151
210
  work_tracker.close()
152
211
 
@@ -258,7 +317,7 @@ def _compute_cluster_evolution(
258
317
  for index, row in tqdm(
259
318
  tiles.iloc[s.cluster_start :].iterrows(),
260
319
  desc=f"Cluster evolution for {zoom=}",
261
- delay=2,
320
+ delay=1,
262
321
  ):
263
322
  new_clusters = False
264
323
  # Current tile.
@@ -337,7 +396,7 @@ def _compute_square_history(
337
396
  for index, row in tqdm(
338
397
  tiles.iloc[s.square_start :].iterrows(),
339
398
  desc=f"Square evolution for {zoom=}",
340
- delay=2,
399
+ delay=1,
341
400
  ):
342
401
  tile = (row["tile_x"], row["tile_y"])
343
402
  x, y = tile
@@ -3,6 +3,7 @@ import logging
3
3
  import pathlib
4
4
  import shutil
5
5
  import sys
6
+ import traceback
6
7
  import urllib.parse
7
8
  import zoneinfo
8
9
  from typing import Optional
@@ -22,6 +23,7 @@ from ..core.tasks import get_state
22
23
  from ..core.tasks import set_state
23
24
  from ..core.tasks import work_tracker_path
24
25
  from ..core.tasks import WorkTracker
26
+ from .activity_parsers import ActivityParseError
25
27
  from .activity_parsers import read_activity
26
28
  from .csv_parser import parse_csv
27
29
 
@@ -182,12 +184,12 @@ def import_from_strava_checkout(config: Config) -> None:
182
184
  ]
183
185
 
184
186
  for activity_id in tqdm(activities_ids_to_parse, desc="Import from Strava export"):
185
- work_tracker.mark_done(activity_id)
186
187
  index = all_activity_ids.index(activity_id)
187
188
  row = {column: table[column][index] for column in header}
188
189
 
189
190
  # Some manually recorded activities have no file name. Pandas reads that as a float. We skip those.
190
191
  if not row["Filename"]:
192
+ work_tracker.mark_done(activity_id)
191
193
  continue
192
194
 
193
195
  start_datetime = dateutil.parser.parse(
@@ -196,7 +198,14 @@ def import_from_strava_checkout(config: Config) -> None:
196
198
 
197
199
  activity_file = checkout_path / row["Filename"]
198
200
 
199
- activity, time_series = read_activity(activity_file)
201
+ try:
202
+ activity, time_series = read_activity(activity_file)
203
+ except ActivityParseError as e:
204
+ logger.error(f"Error while parsing `{activity_file}`:")
205
+ traceback.print_exc()
206
+ continue
207
+
208
+ work_tracker.mark_done(activity_id)
200
209
 
201
210
  if not len(time_series):
202
211
  continue
@@ -6,6 +6,7 @@ import os
6
6
  import pathlib
7
7
  import secrets
8
8
  import shutil
9
+ import sys
9
10
  import threading
10
11
  import urllib.parse
11
12
  import uuid
@@ -32,6 +33,7 @@ from ..core.heart_rate import HeartRateZoneComputer
32
33
  from ..core.paths import TIME_SERIES_DIR
33
34
  from ..core.raster_map import GrayscaleImageTransform
34
35
  from ..core.raster_map import IdentityImageTransform
36
+ from ..core.raster_map import InverseGrayscaleImageTransform
35
37
  from ..core.raster_map import PastelImageTransform
36
38
  from ..core.raster_map import TileGetter
37
39
  from ..explorer.tile_visits import TileVisitAccessor
@@ -85,6 +87,7 @@ def importer_thread(
85
87
  ) -> None:
86
88
  with app.app_context():
87
89
  scan_for_activities(repository, tile_visit_accessor, config)
90
+ logger.info("Importer thread is done.")
88
91
 
89
92
 
90
93
  def web_ui_main(
@@ -182,6 +185,7 @@ def web_ui_main(
182
185
  "color": IdentityImageTransform(),
183
186
  "grayscale": GrayscaleImageTransform(),
184
187
  "pastel": PastelImageTransform(),
188
+ "inverse_grayscale": InverseGrayscaleImageTransform(),
185
189
  }
186
190
  flasher = FlaskFlasher()
187
191
  heart_rate_zone_computer = HeartRateZoneComputer(config)
@@ -265,6 +269,9 @@ def web_ui_main(
265
269
  variables["photo_count"] = DB.session.scalar(
266
270
  sqlalchemy.select(sqlalchemy.func.count()).select_from(Photo)
267
271
  )
272
+ variables["python_version"] = (
273
+ f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
274
+ )
268
275
  return variables
269
276
 
270
277
  app.run(host=host, port=port)
@@ -26,6 +26,7 @@ from ...core.coordinates import Bounds
26
26
  from ...core.datamodel import Activity
27
27
  from ...core.datamodel import DB
28
28
  from ...core.raster_map import ImageTransform
29
+ from ...core.raster_map import OSM_TILE_SIZE
29
30
  from ...core.raster_map import TileGetter
30
31
  from ...core.tiles import compute_tile
31
32
  from ...core.tiles import get_tile_upper_left_lat_lon
@@ -71,13 +72,14 @@ class MaxClusterColorStrategy(ColorStrategy):
71
72
  self, tile_xy: tuple[int, int], grayscale: np.ndarray
72
73
  ) -> np.ndarray:
73
74
  if tile_xy in self.max_cluster_members:
74
- return blend_color(grayscale, np.array([[[55, 126, 184]]]) / 256, 0.3)
75
+ color = np.array([[[55, 126, 184, 70]]]) / 256
75
76
  elif tile_xy in self.evolution_state.memberships:
76
- return blend_color(grayscale, np.array([[[77, 175, 74]]]) / 256, 0.3)
77
+ color = np.array([[[77, 175, 74, 70]]]) / 256
77
78
  elif tile_xy in self.tile_visits:
78
- return blend_color(grayscale, 0.0, 0.3)
79
+ color = np.array([[[0, 0, 0, 70]]]) / 256
79
80
  else:
80
- return grayscale
81
+ color = np.array([[[0, 0, 0, 0]]]) / 256
82
+ return np.broadcast_to(color, grayscale.shape)
81
83
 
82
84
 
83
85
  class ColorfulClusterColorStrategy(ColorStrategy):
@@ -98,11 +100,12 @@ class ColorfulClusterColorStrategy(ColorStrategy):
98
100
  m = hashlib.sha256()
99
101
  m.update(str(cluster_id).encode())
100
102
  d = int(m.hexdigest(), base=16) / (256.0**m.digest_size)
101
- return blend_color(grayscale, np.array([[self._cmap(d)[:3]]]), 0.3)
103
+ color = np.array([[self._cmap(d)[:3] + (0.5,)]])
102
104
  elif tile_xy in self.tile_visits:
103
- return blend_color(grayscale, 0.0, 0.3)
105
+ color = np.array([[[0, 0, 0, 70]]]) / 256
104
106
  else:
105
- return grayscale
107
+ color = np.array([[[0, 0, 0, 0]]]) / 256
108
+ return np.broadcast_to(color, grayscale.shape)
106
109
 
107
110
 
108
111
  class VisitTimeColorStrategy(ColorStrategy):
@@ -120,11 +123,15 @@ class VisitTimeColorStrategy(ColorStrategy):
120
123
  relevant_time = (
121
124
  tile_info["first_time"] if self.use_first else tile_info["last_time"]
122
125
  )
123
- last_age_days = (today - relevant_time.date()).days
124
- color = cmap(max(1 - last_age_days / (2 * 365), 0.0))
125
- return blend_color(grayscale, np.array([[color[:3]]]), 0.3)
126
+ if pd.isna(relevant_time):
127
+ color = np.array([[[0, 0, 0, 70]]]) / 256
128
+ else:
129
+ last_age_days = (today - relevant_time.date()).days
130
+ color = cmap(max(1 - last_age_days / (2 * 365), 0.0))
131
+ color = np.array([[color[:3] + (0.5,)]])
126
132
  else:
127
- return grayscale
133
+ color = np.array([[[0, 0, 0, 0]]]) / 256
134
+ return np.broadcast_to(color, grayscale.shape)
128
135
 
129
136
 
130
137
  class NumVisitsColorStrategy(ColorStrategy):
@@ -138,9 +145,10 @@ class NumVisitsColorStrategy(ColorStrategy):
138
145
  cmap = matplotlib.colormaps["viridis"]
139
146
  tile_info = self.tile_visits[tile_xy]
140
147
  color = cmap(min(len(tile_info["activity_ids"]) / 50, 1.0))
141
- return blend_color(grayscale, np.array([[color[:3]]]), 0.3)
148
+ color = np.array([[color[:3] + (0.5,)]])
142
149
  else:
143
- return grayscale
150
+ color = np.array([[[0, 0, 0, 0]]]) / 256
151
+ return np.broadcast_to(color, grayscale.shape)
144
152
 
145
153
 
146
154
  def make_explorer_blueprint(
@@ -159,7 +167,7 @@ def make_explorer_blueprint(
159
167
  config_accessor().explorer_zoom_levels.append(zoom)
160
168
  config_accessor().explorer_zoom_levels.sort()
161
169
  config_accessor.save()
162
- compute_tile_evolution(tile_visit_accessor, config_accessor())
170
+ compute_tile_evolution(tile_visit_accessor.tile_state, config_accessor())
163
171
  flash(f"Enabled {zoom=} for explorer tiles.", category="success")
164
172
  else:
165
173
  flash(f"{zoom=} is not valid, must be between 0 and 19.", category="danger")
@@ -264,10 +272,11 @@ def make_explorer_blueprint(
264
272
  tile_visits = tile_visit_accessor.tile_state["tile_visits"][zoom]
265
273
  evolution_state = tile_visit_accessor.tile_state["evolution_state"][zoom]
266
274
 
267
- map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
268
- grayscale = image_transforms["grayscale"].transform_image(map_tile)
275
+ # map_tile = np.array(tile_getter.get_tile(z, x, y)) / 255
276
+ # grayscale = image_transforms["grayscale"].transform_image(map_tile)
277
+ grayscale = np.zeros((OSM_TILE_SIZE, OSM_TILE_SIZE, 4), dtype=np.float32)
269
278
  square_line_width = 3
270
- square_color = np.array([[[228, 26, 28]]]) / 256
279
+ square_color = np.array([[[228, 26, 28, 255]]]) / 256
271
280
 
272
281
  color_strategy_name = request.args.get("color_strategy", "colorful_cluster")
273
282
  if color_strategy_name == "default":
@@ -293,60 +302,51 @@ def make_explorer_blueprint(
293
302
  tile_x = x // factor
294
303
  tile_y = y // factor
295
304
  tile_xy = (tile_x, tile_y)
296
- result = color_strategy.color_image(tile_xy, grayscale)
305
+ result = color_strategy.color_image(tile_xy, grayscale).copy()
297
306
 
298
307
  if x % factor == 0:
299
308
  result[:, 0, :] = 0.5
300
309
  if y % factor == 0:
301
310
  result[0, :, :] = 0.5
302
311
 
312
+ if (
313
+ evolution_state.square_x is not None
314
+ and evolution_state.square_y is not None
315
+ ):
303
316
  if (
304
- evolution_state.square_x is not None
305
- and evolution_state.square_y is not None
317
+ x % factor == 0
318
+ and tile_x == evolution_state.square_x
319
+ and evolution_state.square_y
320
+ <= tile_y
321
+ < evolution_state.square_y + evolution_state.max_square_size
306
322
  ):
307
- if (
308
- x % factor == 0
309
- and tile_x == evolution_state.square_x
310
- and evolution_state.square_y
311
- <= tile_y
312
- < evolution_state.square_y + evolution_state.max_square_size
313
- ):
314
- result[:, 0:square_line_width] = blend_color(
315
- result[:, 0:square_line_width], square_color, 0.5
316
- )
317
- if (
318
- y % factor == 0
319
- and tile_y == evolution_state.square_y
320
- and evolution_state.square_x
321
- <= tile_x
322
- < evolution_state.square_x + evolution_state.max_square_size
323
- ):
324
- result[0:square_line_width, :] = blend_color(
325
- result[0:square_line_width, :], square_color, 0.5
326
- )
327
-
328
- if (
329
- (x + 1) % factor == 0
330
- and (x + 1) // factor
331
- == evolution_state.square_x + evolution_state.max_square_size
332
- and evolution_state.square_y
333
- <= tile_y
334
- < evolution_state.square_y + evolution_state.max_square_size
335
- ):
336
- result[:, -square_line_width:] = blend_color(
337
- result[:, -square_line_width:], square_color, 0.5
338
- )
339
- if (
340
- (y + 1) % factor == 0
341
- and (y + 1) // factor
342
- == evolution_state.square_y + evolution_state.max_square_size
343
- and evolution_state.square_x
344
- <= tile_x
345
- < evolution_state.square_x + evolution_state.max_square_size
346
- ):
347
- result[-square_line_width:, :] = blend_color(
348
- result[-square_line_width:, :], square_color, 0.5
349
- )
323
+ result[:, 0:square_line_width] = square_color
324
+ if (
325
+ y % factor == 0
326
+ and tile_y == evolution_state.square_y
327
+ and evolution_state.square_x
328
+ <= tile_x
329
+ < evolution_state.square_x + evolution_state.max_square_size
330
+ ):
331
+ result[0:square_line_width, :] = square_color
332
+ if (
333
+ (x + 1) % factor == 0
334
+ and (x + 1) // factor
335
+ == evolution_state.square_x + evolution_state.max_square_size
336
+ and evolution_state.square_y
337
+ <= tile_y
338
+ < evolution_state.square_y + evolution_state.max_square_size
339
+ ):
340
+ result[:, -square_line_width:] = square_color
341
+ if (
342
+ (y + 1) % factor == 0
343
+ and (y + 1) // factor
344
+ == evolution_state.square_y + evolution_state.max_square_size
345
+ and evolution_state.square_x
346
+ <= tile_x
347
+ < evolution_state.square_x + evolution_state.max_square_size
348
+ ):
349
+ result[-square_line_width:, :] = square_color
350
350
  else:
351
351
  result = grayscale
352
352
  factor = 2 ** (zoom - z)
@@ -381,14 +381,7 @@ def make_explorer_blueprint(
381
381
  result[
382
382
  yo * width : (yo + 1) * width,
383
383
  xo * width : xo * width + square_line_width,
384
- ] = blend_color(
385
- result[
386
- yo * width : (yo + 1) * width,
387
- xo * width : xo * width + square_line_width,
388
- ],
389
- square_color,
390
- 0.5,
391
- )
384
+ ] = square_color
392
385
  if (
393
386
  tile_y == evolution_state.square_y
394
387
  and evolution_state.square_x
@@ -399,14 +392,7 @@ def make_explorer_blueprint(
399
392
  result[
400
393
  yo * width : yo * width + square_line_width,
401
394
  xo * width : (xo + 1) * width,
402
- ] = blend_color(
403
- result[
404
- yo * width : yo * width + square_line_width,
405
- xo * width : (xo + 1) * width,
406
- ],
407
- square_color,
408
- 0.5,
409
- )
395
+ ] = square_color
410
396
 
411
397
  if (
412
398
  tile_x + 1
@@ -421,15 +407,7 @@ def make_explorer_blueprint(
421
407
  yo * width : (yo + 1) * width,
422
408
  (xo + 1) * width
423
409
  - square_line_width : (xo + 1) * width,
424
- ] = blend_color(
425
- result[
426
- yo * width : (yo + 1) * width,
427
- (xo + 1) * width
428
- - square_line_width : (xo + 1) * width,
429
- ],
430
- square_color,
431
- 0.5,
432
- )
410
+ ] = square_color
433
411
 
434
412
  if (
435
413
  tile_y + 1
@@ -444,15 +422,7 @@ def make_explorer_blueprint(
444
422
  (yo + 1) * width
445
423
  - square_line_width : (yo + 1) * width,
446
424
  xo * width : (xo + 1) * width,
447
- ] = blend_color(
448
- result[
449
- (yo + 1) * width
450
- - square_line_width : (yo + 1) * width,
451
- xo * width : (xo + 1) * width,
452
- ],
453
- square_color,
454
- 0.5,
455
- )
425
+ ] = square_color
456
426
  if width >= 64:
457
427
  result[yo * width, :, :] = 0.5
458
428
  result[:, xo * width, :] = 0.5
@@ -1,4 +1,5 @@
1
1
  import collections
2
+ import logging
2
3
 
3
4
  import pandas as pd
4
5
  from flask import Blueprint
@@ -12,6 +13,9 @@ from ..search_util import search_query_from_form
12
13
  from ..search_util import SearchQueryHistory
13
14
 
14
15
 
16
+ logger = logging.getLogger(__name__)
17
+
18
+
15
19
  def make_hall_of_fame_blueprint(
16
20
  repository: ActivityRepository,
17
21
  search_query_history: SearchQueryHistory,
@@ -73,7 +77,14 @@ def _nominate_activities_inner(
73
77
 
74
78
  for variable, title, format_str in ratings:
75
79
  if variable in meta.columns and not pd.isna(meta[variable]).all():
76
- i = meta[variable].idxmax()
77
- value = meta.loc[i, variable]
78
- format_applied = format_str.format(value)
79
- nominations[i].append(f"{title}{title_suffix}: {format_applied}")
80
+ try:
81
+ i = meta[variable].idxmax()
82
+ except ValueError as e:
83
+ print(meta[variable].tolist())
84
+ print(f"{meta[variable].dtype=}")
85
+ logger.error(f"Trying to work with {variable=}.")
86
+ logger.error(f"We got a ValueError: {e}")
87
+ else:
88
+ value = meta.loc[i, variable]
89
+ format_applied = format_str.format(value)
90
+ nominations[i].append(f"{title}{title_suffix}: {format_applied}")
@@ -221,13 +221,6 @@ def _render_tile_image(
221
221
 
222
222
  cmap = pl.get_cmap(config.color_scheme_for_heatmap)
223
223
  data_color = cmap(tile_counts)
224
- data_color[data_color == cmap(0.0)] = 0.0 # remove background color
225
-
226
- map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
227
- map_tile = convert_to_grayscale(map_tile)
228
- map_tile = 1.0 - map_tile # invert colors
229
- for c in range(3):
230
- map_tile[:, :, c] = (1.0 - data_color[:, :, c]) * map_tile[
231
- :, :, c
232
- ] + data_color[:, :, c]
233
- return map_tile
224
+ data_color[tile_counts > 0, 3] = 0.8
225
+ data_color[tile_counts == 0, 3] = 0.0
226
+ return data_color
@@ -2,8 +2,6 @@ import datetime
2
2
  import pathlib
3
3
  import uuid
4
4
 
5
- import dateutil.parser
6
- import exifread
7
5
  import geojson
8
6
  import sqlalchemy
9
7
  from flask import Blueprint
@@ -21,36 +19,13 @@ from ...core.datamodel import Activity
21
19
  from ...core.datamodel import DB
22
20
  from ...core.datamodel import Photo
23
21
  from ...core.paths import PHOTOS_DIR
22
+ from ...core.photos import get_metadata_from_image
24
23
  from ..authenticator import Authenticator
25
24
  from ..authenticator import needs_authentication
26
25
  from ..flasher import Flasher
27
26
  from ..flasher import FlashTypes
28
27
 
29
28
 
30
- def ratio_to_decimal(numbers: list[exifread.utils.Ratio]) -> float:
31
- deg, min, sec = numbers.values
32
- return deg.decimal() + min.decimal() / 60 + sec.decimal() / 3600
33
-
34
-
35
- def get_metadata_from_image(path: pathlib.Path) -> dict:
36
- with open(path, "rb") as f:
37
- tags = exifread.process_file(f)
38
- metadata = {}
39
- try:
40
- metadata["latitude"] = ratio_to_decimal(tags["GPS GPSLatitude"])
41
- metadata["longitude"] = ratio_to_decimal(tags["GPS GPSLongitude"])
42
- except KeyError:
43
- pass
44
- try:
45
- metadata["time"] = datetime.datetime.strptime(
46
- str(tags["EXIF DateTimeOriginal"]), "%Y:%m:%d %H:%M:%S"
47
- )
48
- except KeyError:
49
- pass
50
-
51
- return metadata
52
-
53
-
54
29
  def make_photo_blueprint(
55
30
  config_accessor: ConfigAccessor, authenticator: Authenticator, flasher: Flasher
56
31
  ) -> Blueprint:
@@ -1,23 +1,59 @@
1
+ let map = L.map('explorer-map', {
2
+ fullscreenControl: true,
3
+ center: [center_latitude, center_longitude],
4
+ zoom: 12
5
+ });
1
6
 
2
- let tile_layer = null
7
+ let base_maps = {
8
+ "Grayscale": L.tileLayer("/tile/grayscale/{z}/{x}/{y}.png", {
9
+ maxZoom: 19,
10
+ attribution: map_tile_attribution
11
+ }),
12
+ "Pastel": L.tileLayer("/tile/pastel/{z}/{x}/{y}.png", {
13
+ maxZoom: 19,
14
+ attribution: map_tile_attribution
15
+ }),
16
+ "Color": L.tileLayer("/tile/color/{z}/{x}/{y}.png", {
17
+ maxZoom: 19,
18
+ attribution: map_tile_attribution
19
+ }),
20
+ "Inverse Grayscale": L.tileLayer("/tile/inverse_grayscale/{z}/{x}/{y}.png", {
21
+ maxZoom: 19,
22
+ attribution: map_tile_attribution
23
+ }),
24
+ }
3
25
 
4
- function changeColor(method) {
5
- if (tile_layer) {
6
- map.removeLayer(tile_layer)
7
- }
8
- tile_layer = L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=${method}`, {
26
+ let overlay_maps = {
27
+ "Colorful Cluster": L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=colorful_cluster`, {
9
28
  maxZoom: 19,
10
29
  attribution: map_tile_attribution
11
- }).addTo(map)
30
+ }),
31
+ "Max Cluster": L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=max_cluster`, {
32
+ maxZoom: 19,
33
+ attribution: map_tile_attribution
34
+ }),
35
+ "First Visit": L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=first`, {
36
+ maxZoom: 19,
37
+ attribution: map_tile_attribution
38
+ }),
39
+ "Last Visit": L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=last`, {
40
+ maxZoom: 19,
41
+ attribution: map_tile_attribution
42
+ }),
43
+ "Number of Visits": L.tileLayer(`/explorer/${zoom}/tile/{z}/{x}/{y}.png?color_strategy=visits`, {
44
+ maxZoom: 19,
45
+ attribution: map_tile_attribution
46
+ }),
47
+ "Heatmap": L.tileLayer("/heatmap/tile/{z}/{x}/{y}.png", {
48
+ maxZoom: 19,
49
+ attribution: map_tile_attribution
50
+ }),
12
51
  }
13
52
 
14
- let map = L.map('explorer-map', {
15
- fullscreenControl: true,
16
- center: [center_latitude, center_longitude],
17
- zoom: 12
18
- });
53
+ base_maps['Grayscale'].addTo(map)
54
+ overlay_maps["Colorful Cluster"].addTo(map)
19
55
 
20
- changeColor('default')
56
+ var layerControl = L.control.layers(base_maps, overlay_maps).addTo(map);
21
57
 
22
58
  if (bbox) {
23
59
  map.fitBounds(L.geoJSON(bbox).getBounds())
@@ -24,14 +24,6 @@
24
24
  </div>
25
25
  </div>
26
26
 
27
- <div class="btn-group mb-3" role="group">
28
- <button type="button" class="btn btn-primary" onclick="changeColor('colorful_cluster')">Colorful Cluster</button>
29
- <button type="button" class="btn btn-primary" onclick="changeColor('max_cluster')">Max Cluster</button>
30
- <button type="button" class="btn btn-primary" onclick="changeColor('first')">First Visit</button>
31
- <button type="button" class="btn btn-primary" onclick="changeColor('last')">Last Visit</button>
32
- <button type="button" class="btn btn-primary" onclick="changeColor('visits')">Number of Visits</button>
33
- </div>
34
-
35
27
  <div class="row mb-3">
36
28
  <div class="col">
37
29
  <div id="explorer-map" class="mb-1" style="height: 800px;"></div>
@@ -18,6 +18,12 @@
18
18
  center: [{{ center.latitude }}, {{ center.longitude }}],
19
19
  zoom: 12
20
20
  });
21
+
22
+ L.tileLayer("/tile/inverse_grayscale/{z}/{x}/{y}.png", {
23
+ maxZoom: 19,
24
+ attribution: '{{ map_tile_attribution|safe }}'
25
+ }).addTo(map)
26
+
21
27
  L.tileLayer('/heatmap/tile/{z}/{x}/{y}.png?{{ extra_args|safe }}', {
22
28
  maxZoom: 19,
23
29
  attribution: '{{ map_tile_attribution|safe }}'
@@ -204,9 +204,12 @@
204
204
 
205
205
  <div class="row border-top py-3 my-4">
206
206
  <ul class="nav col-4">
207
- <li class="nav-item px-2 nav-link"><a
207
+ <li class="nav-item"><a
208
208
  href="https://github.com/martin-ueding/geo-activity-playground/blob/main/docs/changelog.md"
209
209
  class="nav-link px-2 text-muted" target="_blank">Version {{ version }}</a></li>
210
+
211
+ <li class="nav-item"><span class="nav-link px-2 text-muted">Python {{ python_version
212
+ }}</span></li>
210
213
  </ul>
211
214
  <ul class="nav col-8 justify-content-end">
212
215
  <li class="nav-item"><a href="https://github.com/martin-ueding/geo-activity-playground"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 1.3.2
3
+ Version: 1.4.1
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -1,5 +1,5 @@
1
1
  geo_activity_playground/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- geo_activity_playground/__main__.py,sha256=eL7NlKydYrzi4ikTvvKmlwkEFxx0V6CXOHOhRtazmd8,2907
2
+ geo_activity_playground/__main__.py,sha256=y49XBk0PmAiunx2pZvxLjng1C-AMgAB-f_DljtTCb34,3209
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
@@ -29,8 +29,9 @@ geo_activity_playground/core/meta_search.py,sha256=nyvCuR7v0pd6KjA8W5Kr71bBafRdE
29
29
  geo_activity_playground/core/missing_values.py,sha256=HjonaLV0PFMICnuMrbdUNnK9uy_8PBh_RxI5GuEMQK0,250
30
30
  geo_activity_playground/core/parametric_plot.py,sha256=8CKB8dey7EmZtQnl6IOgBhpxkw0UCpQPWeiBw5PqW8k,5737
31
31
  geo_activity_playground/core/paths.py,sha256=qQ4ujaIHmsxTGEWzf-76XS8FclEI2RC5COTUeuLEbDI,2938
32
+ geo_activity_playground/core/photos.py,sha256=pUnEfVesEKESlew6_KkYAF582ejLupnWTqs_RcvnRsA,1070
32
33
  geo_activity_playground/core/privacy_zones.py,sha256=4TumHsVUN1uW6RG3ArqTXDykPVipF98DCxVBe7YNdO8,512
33
- geo_activity_playground/core/raster_map.py,sha256=Cq8dNLdxVQg3Agzn2bmXVu0-8kZf56QrSe-LKNn3jaU,7994
34
+ geo_activity_playground/core/raster_map.py,sha256=y7maiC_bmUwXsULC_XCZ1m8nGgU2jFe47QFB7TpC_V4,8257
34
35
  geo_activity_playground/core/similarity.py,sha256=L2de3DPRdDeDY5AxZwLDcH7FjHWRWklr41VNU06q9kQ,3117
35
36
  geo_activity_playground/core/summary_stats.py,sha256=v5FtWnE1imDF5axI6asVN55wCrlD73oZ6lvqzxsTN2c,1006
36
37
  geo_activity_playground/core/tasks.py,sha256=-_9cxekoHSWzCW4XblNeqrwi2tTqr5AE7_-p8fdqhwc,2886
@@ -47,7 +48,7 @@ geo_activity_playground/core/tiles.py,sha256=LBn2V6WAvMxZeXSIQ8ruZL71iyvOXoFZMz7
47
48
  geo_activity_playground/core/time_conversion.py,sha256=F-vQ-MdbTOqPTAELplDjT5m7kdaf1RsqBXELfsR5eOU,1329
48
49
  geo_activity_playground/explorer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
50
  geo_activity_playground/explorer/grid_file.py,sha256=YNL_c4O1-kxaajATJwj4ZLywCL5Hpj9qy2h-F7rk8Yg,3260
50
- geo_activity_playground/explorer/tile_visits.py,sha256=WK_H5fdy3S69BT4zZGP0R92qq5MlNRJMp25BrMj6O2Q,13884
51
+ geo_activity_playground/explorer/tile_visits.py,sha256=NUzC4jNb_vQExAIALrO2H1MiNs5JJKsOQKGietAcATE,16271
51
52
  geo_activity_playground/explorer/video.py,sha256=7j6Qv3HG6On7Tn7xh7Olwrx_fbQnfzS7CeRg3TEApHg,4397
52
53
  geo_activity_playground/heatmap_video.py,sha256=I8i1uVvbbPUXVtvLAROaLy58nQoUPnuMCZkERWNkQjg,3318
53
54
  geo_activity_playground/importers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -55,12 +56,12 @@ geo_activity_playground/importers/activity_parsers.py,sha256=zWgLkHHd8rjWnKs-COB
55
56
  geo_activity_playground/importers/csv_parser.py,sha256=O1pP5GLhWhnWcy2Lsrr9g17Zspuibpt-GtZ3ZS5eZF4,2143
56
57
  geo_activity_playground/importers/directory.py,sha256=ucnB5sPBvXzLdaza2v8GVU75ArfGG4E7d5OXrCgoFq4,3562
57
58
  geo_activity_playground/importers/strava_api.py,sha256=Fiqlc-VeuzsvgDcWt71JoPMri221cMjkeL4SH80gC5s,8426
58
- geo_activity_playground/importers/strava_checkout.py,sha256=6RJoGRN3OdEXODeB8yzhT1OJglBGgsaZdNath1rzooA,8937
59
+ geo_activity_playground/importers/strava_checkout.py,sha256=Pugtv0nbgfuVzBDC5e5Tfv1jShUYmMcDbpCQp2ULXow,9232
59
60
  geo_activity_playground/importers/test_csv_parser.py,sha256=nOTVTdlzIY0TDcbWp7xNyNaIO6Mkeu55hVziVl22QE4,1092
60
61
  geo_activity_playground/importers/test_directory.py,sha256=_fn_-y98ZyElbG0BRxAmGFdtGobUShPU86SdEOpuv-A,691
61
62
  geo_activity_playground/importers/test_strava_api.py,sha256=7b8bl5Rh2BctCmvTPEhCadxtUOq3mfzuadD6F5XxRio,398
62
63
  geo_activity_playground/webui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- geo_activity_playground/webui/app.py,sha256=4dCfzxPk7KJTAwEXyw3NPiQQz-udvgECqeL8CtOj0Es,10334
64
+ geo_activity_playground/webui/app.py,sha256=qsnwE20S-tWBbD_MgkIS8HVsRh6TO6mrwW_gWYHk0Bo,10653
64
65
  geo_activity_playground/webui/authenticator.py,sha256=dhREYOu_TCD_nzFNuSlHIbf5K6TmwKdXtr1wxD8fBcc,1491
65
66
  geo_activity_playground/webui/blueprints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
67
  geo_activity_playground/webui/blueprints/activity_blueprint.py,sha256=tFy0GpOBhIP8xlmYc9PF4kAng-0MosXMJudVupGz2Yw,26771
@@ -70,11 +71,11 @@ geo_activity_playground/webui/blueprints/calendar_blueprint.py,sha256=SmOu5AfNNo
70
71
  geo_activity_playground/webui/blueprints/eddington_blueprints.py,sha256=Ya5GJxfVESwmRlgMTYe9g75g8JHHTAAvYFmSD-3Uz4Q,8987
71
72
  geo_activity_playground/webui/blueprints/entry_views.py,sha256=SDCzpUSb1FAb84tM0SnmrZQvtaTlO-Rqdj94hyIMDSc,2936
72
73
  geo_activity_playground/webui/blueprints/equipment_blueprint.py,sha256=8L_7NZGErvu4jyigi2gg7HN_gegZRdsSFahUH7Dz6Lw,5727
73
- geo_activity_playground/webui/blueprints/explorer_blueprint.py,sha256=wlInaRbh1Wp-Va9mqcaHsVelBUQsqHusoTvkiynoU2E,22985
74
+ geo_activity_playground/webui/blueprints/explorer_blueprint.py,sha256=bqRG11EHJ67ZVrDVu9KhriCFihw9910fC6PuPmhFZDc,21352
74
75
  geo_activity_playground/webui/blueprints/export_blueprint.py,sha256=C9yFH5gEJs2YtWE-EhcGDEyGwwaLgC1umybgIRi6duE,1036
75
- geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py,sha256=zNYKw7ps9Yx9995Zsj4psAlOLnt4tFi2Hwp74-kjmzw,2806
76
- geo_activity_playground/webui/blueprints/heatmap_blueprint.py,sha256=qPwvcZLZgAH1g8FQAzjIc5LxKp2bkc7YbZwkjxVJVWc,8730
77
- geo_activity_playground/webui/blueprints/photo_blueprint.py,sha256=ZBh7Gt5vEzeW8JDK3t-3RcLTT40mbOqRkttkicgcIRM,7885
76
+ geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py,sha256=bOJ6ejDS6rw8-GEGo1Lihn5DS6j0t9e8CbcbRi44Pts,3168
77
+ geo_activity_playground/webui/blueprints/heatmap_blueprint.py,sha256=5LlYKMeOMIE7c3xGRZ52ld4Jxtdc3GNcb6lvt3v7NVA,8435
78
+ geo_activity_playground/webui/blueprints/photo_blueprint.py,sha256=eK3JSvOAsiTVDy5wardtqTDGIZ79jmwuXpdIPBI-GjU,7186
78
79
  geo_activity_playground/webui/blueprints/plot_builder_blueprint.py,sha256=nGtYblRTJ0rasJvl_L35cs1Iry4LONPy_9TY4ytXB-Q,3838
79
80
  geo_activity_playground/webui/blueprints/search_blueprint.py,sha256=Sv_KL1Cdai26y51qVfI-5jZLhtElREsEar1dbR_VAC4,2275
80
81
  geo_activity_playground/webui/blueprints/settings_blueprint.py,sha256=cwes3QmRrC_HMP1g-Yc-x2BJycF4jF3StJl75v9acWo,20377
@@ -113,11 +114,13 @@ geo_activity_playground/webui/static/leaflet/MarkerCluster.Default.css,sha256=LW
113
114
  geo_activity_playground/webui/static/leaflet/MarkerCluster.css,sha256=-bdWuWOXMFkX0v9Cvr3OWClPiYefDQz9GGZP_7xZxdc,886
114
115
  geo_activity_playground/webui/static/leaflet/fullscreen.png,sha256=yDtz-dhjuAoo6q9xc00-_XNTrGwEWrN80pOneFdol4g,299
115
116
  geo_activity_playground/webui/static/leaflet/fullscreen@2x.png,sha256=HVi2guZO6sekf2NggilbzjUTvJDweXpSMBS81fhtnX0,420
117
+ geo_activity_playground/webui/static/leaflet/images/layers-2x.png,sha256=Bm2sqFDY_77wB68AsG6sABVyje4nnFHzy2xxbffELt8,1259
118
+ geo_activity_playground/webui/static/leaflet/images/layers.png,sha256=Hbvp0CjikvNvy6j4s6KNXokydU_CIVuaxp5M3s9RB8Y,696
116
119
  geo_activity_playground/webui/static/leaflet/leaflet.css,sha256=p4NxAoJBhIIN-hmNHrzRCf9tD_miZyoHS5obTRR9BMY,14806
117
120
  geo_activity_playground/webui/static/leaflet/leaflet.fullscreen.css,sha256=YTbhDGEH5amI_JfotPMN7IByFpsN9e4tCBnv5oNdvHU,994
118
121
  geo_activity_playground/webui/static/leaflet/leaflet.js,sha256=20nQCchB9co0qIjJZRGuk2_Z9VM-kNiyxNV1lvTlZBo,147552
119
122
  geo_activity_playground/webui/static/leaflet/leaflet.markercluster.js,sha256=WL6HHfYfbFEkZOFdsJQeY7lJG_E5airjvqbznghUzRw,33724
120
- geo_activity_playground/webui/static/server-side-explorer.js,sha256=5_I1OnHjKE4j9KFS7LP6LTpLPMW0buanlNfI-_xhrGo,1845
123
+ geo_activity_playground/webui/static/server-side-explorer.js,sha256=dmGTTCZ9OchOc18SJM6qE_EgG-6LEZhckpVIsBvCkv8,3262
121
124
  geo_activity_playground/webui/static/table-sort.min.js,sha256=sFeDrgkXTePr2ciJU9_mLh-Z8qtYhPIQMgOZtj0LwBY,8506
122
125
  geo_activity_playground/webui/static/vega/vega-embed@6.js,sha256=EtAqz74-xZ75o33UgiouBOKWG1u7Zxu-Zh0iIXFbmdo,60630
123
126
  geo_activity_playground/webui/static/vega/vega-lite@4.js,sha256=roXmcY9bUF91uB9V-eSEUHEgfwoXe6B1xoDPuIe5ou8,267999
@@ -136,12 +139,12 @@ geo_activity_playground/webui/templates/eddington/distance.html.j2,sha256=9cLlIr
136
139
  geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2,sha256=h2mI1Uc1-P7rN_SeCVP_uadpQqX09ZpBG3Z6N8QWNLw,4723
137
140
  geo_activity_playground/webui/templates/elevation_eddington/index.html.j2,sha256=WjquRFWaMzIZrvByhRIuhJbSCUW2HTfMck6THQHZI-I,4743
138
141
  geo_activity_playground/webui/templates/equipment/index.html.j2,sha256=6pzSCJACMXA1fKgsO_KrCTvpumAKlelzj5f9dReey14,1742
139
- geo_activity_playground/webui/templates/explorer/server-side.html.j2,sha256=ynejXeUjb-ZwkvPtbD20R8R1KLUIFsyM5VJgwW7vzM8,3133
142
+ geo_activity_playground/webui/templates/explorer/server-side.html.j2,sha256=CrTFVghloFJcN25H18FZ6KQY7QgIqvyCauFzgYG_yCo,2547
140
143
  geo_activity_playground/webui/templates/export/index.html.j2,sha256=vxqpAm9KnT405Qz7q0_td-HZ4mCjcPR4Lp6EnIEWisg,1652
141
144
  geo_activity_playground/webui/templates/hall_of_fame/index.html.j2,sha256=P15fVPjXf0Wf6K_hd_lCMuw6-Q8_qfNqsBOWNpMfoXw,1804
142
- geo_activity_playground/webui/templates/heatmap/index.html.j2,sha256=uM-l4gmDKw6307ZH_zb8zroMTKBuOkrR0Bu4fTEJE0s,1231
145
+ geo_activity_playground/webui/templates/heatmap/index.html.j2,sha256=Q99v4LP5EnuvDYKayL52qujWaIroLsD89ly2cM2YvTI,1420
143
146
  geo_activity_playground/webui/templates/home.html.j2,sha256=L6U44p05IagWERPxJ99cUMZzZCyhmA5IV7PL-QaO_mg,2258
144
- geo_activity_playground/webui/templates/page.html.j2,sha256=X3dXpmcV4486St3BLHIBbhZYcb6X_dYBOBSZi_M3Aio,12188
147
+ geo_activity_playground/webui/templates/page.html.j2,sha256=4vtvq7cV5YIXIUsjG44grHMqnjKWUlVy0eBwqW8UIzg,12315
145
148
  geo_activity_playground/webui/templates/photo/map.html.j2,sha256=MWhqt5Q8ExiRhgxndcEnwngOj1qw0E0u4hKuiuY24Gg,1437
146
149
  geo_activity_playground/webui/templates/photo/new.html.j2,sha256=0BO4ZJgJQM1Hlp9SHylEOfthpQlywDc-xFs8K_Spptc,392
147
150
  geo_activity_playground/webui/templates/plot-macros.html.j2,sha256=lzsu8c8fcsVjgpdcmpwCa1e6EPALZtCS9RbvQ-DAtAs,2861
@@ -171,8 +174,8 @@ geo_activity_playground/webui/templates/summary/vega-chart.html.j2,sha256=mw8Hti
171
174
  geo_activity_playground/webui/templates/time_zone_fixer/index.html.j2,sha256=s9r6BJMXmd7kLSyjkvH4xLi6e01S5bpGRcMgMMJyCAE,1760
172
175
  geo_activity_playground/webui/templates/upload/index.html.j2,sha256=I1Ix8tDS3YBdi-HdaNfjkzYXVVCjfUTe5PFTnap1ydc,775
173
176
  geo_activity_playground/webui/templates/upload/reload.html.j2,sha256=YZWX5eDeNyqKJdQAywDBcU8DZBm22rRBbZqFjrFrCvQ,556
174
- geo_activity_playground-1.3.2.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
175
- geo_activity_playground-1.3.2.dist-info/METADATA,sha256=0VFH55TyvrqQZOz60YN1odpofpP3iT7d5w8y2v6O268,1890
176
- geo_activity_playground-1.3.2.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
177
- geo_activity_playground-1.3.2.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
178
- geo_activity_playground-1.3.2.dist-info/RECORD,,
177
+ geo_activity_playground-1.4.1.dist-info/LICENSE,sha256=4RpAwKO8bPkfXH2lnpeUW0eLkNWglyG4lbrLDU_MOwY,1070
178
+ geo_activity_playground-1.4.1.dist-info/METADATA,sha256=TlfR52Fp08hHZ5jDPHtHeQQ7dqcsqiUbI6aU1a94nv0,1890
179
+ geo_activity_playground-1.4.1.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
180
+ geo_activity_playground-1.4.1.dist-info/entry_points.txt,sha256=pbNlLI6IIZIp7nPYCfAtiSiz2oxJSCl7DODD6SPkLKk,81
181
+ geo_activity_playground-1.4.1.dist-info/RECORD,,