geo-activity-playground 0.11.0__tar.gz → 0.12.0__tar.gz

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 (53) hide show
  1. geo_activity_playground-0.12.0/LICENSE +21 -0
  2. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/PKG-INFO +1 -1
  3. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/activity_parsers.py +6 -0
  4. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/explorer/clusters.py +30 -18
  5. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/explorer/converters.py +33 -19
  6. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/explorer/grid_file.py +47 -18
  7. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/explorer_controller.py +8 -2
  8. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/explorer.html.j2 +17 -0
  9. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/pyproject.toml +2 -1
  10. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/__init__.py +0 -0
  11. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/__main__.py +0 -0
  12. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/__init__.py +0 -0
  13. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/activities.py +0 -0
  14. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/config.py +0 -0
  15. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/coordinates.py +0 -0
  16. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/heatmap.py +0 -0
  17. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/plots.py +0 -0
  18. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/sources.py +0 -0
  19. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/tasks.py +0 -0
  20. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/test_tiles.py +0 -0
  21. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/core/tiles.py +0 -0
  22. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/explorer/__init__.py +0 -0
  23. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/explorer/video.py +0 -0
  24. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/heatmap.py +0 -0
  25. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/importers/directory.py +0 -0
  26. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/importers/strava_api.py +0 -0
  27. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/activity_controller.py +0 -0
  28. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/app.py +0 -0
  29. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/calendar_controller.py +0 -0
  30. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/eddington_controller.py +0 -0
  31. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/entry_controller.py +0 -0
  32. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/equipment_controller.py +0 -0
  33. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/grayscale_tile_controller.py +0 -0
  34. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/heatmap_controller.py +0 -0
  35. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/android-chrome-192x192.png +0 -0
  36. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/android-chrome-384x384.png +0 -0
  37. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/apple-touch-icon.png +0 -0
  38. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/browserconfig.xml +0 -0
  39. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/favicon-16x16.png +0 -0
  40. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/favicon-32x32.png +0 -0
  41. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/favicon.ico +0 -0
  42. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/mstile-150x150.png +0 -0
  43. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/safari-pinned-tab.svg +0 -0
  44. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/static/site.webmanifest +0 -0
  45. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/activity.html.j2 +0 -0
  46. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/calendar-month.html.j2 +0 -0
  47. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/calendar.html.j2 +0 -0
  48. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/eddington.html.j2 +0 -0
  49. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/equipment.html.j2 +0 -0
  50. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/heatmap.html.j2 +0 -0
  51. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/index.html.j2 +0 -0
  52. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/page.html.j2 +0 -0
  53. {geo_activity_playground-0.11.0 → geo_activity_playground-0.12.0}/geo_activity_playground/webui/templates/summary-statistics.html.j2 +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Martin Ueding
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 0.11.0
3
+ Version: 0.12.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
@@ -159,6 +159,12 @@ def read_activity(path: pathlib.Path) -> pd.DataFrame:
159
159
  except AttributeError as e:
160
160
  print(df)
161
161
  print(df.dtypes)
162
+ types = {}
163
+ for elem in df["time"]:
164
+ t = str(type(elem))
165
+ if t not in types:
166
+ types[t] = elem
167
+ print(types)
162
168
  raise ActivityParseError(
163
169
  "It looks like the date parsing has gone wrong."
164
170
  ) from e
@@ -24,11 +24,12 @@ def adjacent_to(tile: tuple[int, int]) -> Iterator[tuple[int, int]]:
24
24
  class ExplorerClusterState:
25
25
  def __init__(self, zoom: int) -> None:
26
26
  self.num_neighbors: dict[tuple[int, int], int] = {}
27
- self.cluster_tiles: dict[tuple[int, int], list[tuple[int, int]]] = {}
27
+ self.memberships: dict[tuple[int, int], tuple[int, int]] = {}
28
+ self.clusters: dict[tuple[int, int], list[tuple[int, int]]] = {}
28
29
  self.cluster_evolution = pd.DataFrame()
29
30
  self.start = 0
30
31
 
31
- self._state_path = pathlib.Path(f"Cache/explorer_cluster_{zoom}_state.json")
32
+ self._state_path = pathlib.Path(f"Cache/explorer_cluster_{zoom}_state_v2.json")
32
33
  self._cluster_evolution_path = pathlib.Path(
33
34
  f"Cache/explorer_cluster_{zoom}_evolution.parquet"
34
35
  )
@@ -42,12 +43,14 @@ class ExplorerClusterState:
42
43
  tuple(map(int, key.split("/"))): value
43
44
  for key, value in data["num_neighbors"].items()
44
45
  }
45
- self.cluster_tiles = {
46
- tuple(map(int, key.split("/"))): [
47
- tuple(t) for t in data["clusters"][str(value)]
48
- ]
46
+ self.memberships = {
47
+ tuple(map(int, key.split("/"))): tuple(value)
49
48
  for key, value in data["memberships"].items()
50
49
  }
50
+ self.clusters = {
51
+ tuple(map(int, key.split("/"))): [tuple(t) for t in value]
52
+ for key, value in data["clusters"].items()
53
+ }
51
54
  self.start = data["start"]
52
55
 
53
56
  if self._cluster_evolution_path.exists():
@@ -60,11 +63,11 @@ class ExplorerClusterState:
60
63
  f"{x}/{y}": count for (x, y), count in self.num_neighbors.items()
61
64
  },
62
65
  "memberships": {
63
- f"{x}/{y}": id(members)
64
- for (x, y), members in self.cluster_tiles.items()
66
+ f"{x}/{y}": value for (x, y), value in self.memberships.items()
65
67
  },
66
68
  "clusters": {
67
- id(members): members for members in self.cluster_tiles.values()
69
+ f"{x}/{y}": [tuple(member) for member in members]
70
+ for (x, y), members in self.clusters.items()
68
71
  },
69
72
  "start": self.start,
70
73
  }
@@ -107,34 +110,43 @@ def get_explorer_cluster_evolution(zoom: int) -> ExplorerClusterState:
107
110
 
108
111
  # If the current tile has all neighbors, make it it's own cluster.
109
112
  if s.num_neighbors[tile] == 4:
110
- s.cluster_tiles[tile] = [tile]
113
+ s.clusters[tile] = [tile]
114
+ s.memberships[tile] = tile
111
115
 
112
116
  # Also make the adjacent tiles their own clusters, if they are full.
113
117
  this_and_neighbors = [tile] + list(adjacent_to(tile))
114
118
  for other in this_and_neighbors:
115
119
  if s.num_neighbors.get(other, 0) == 4:
116
- s.cluster_tiles[other] = [other]
120
+ s.clusters[other] = [other]
121
+ s.memberships[other] = other
117
122
 
118
123
  for candidate in this_and_neighbors:
119
- if candidate not in s.cluster_tiles:
124
+ # If the the candidate is not a cluster tile, skip.
125
+ if candidate not in s.memberships:
120
126
  continue
121
127
  # The candidate is a cluster tile. Let's see whether any of the neighbors are also cluster tiles but with a different cluster. Then we need to join them.
122
128
  for other in adjacent_to(candidate):
123
- if other not in s.cluster_tiles:
129
+ if other not in s.memberships:
124
130
  continue
125
131
  # The other tile is also a cluster tile.
126
- if s.cluster_tiles[candidate] is s.cluster_tiles[other]:
132
+ if s.memberships[candidate] == s.memberships[other]:
127
133
  continue
128
134
  # The two clusters are not the same. We add the other's cluster tile to this tile.
129
- s.cluster_tiles[candidate].extend(s.cluster_tiles[other])
135
+ this_cluster = s.clusters[s.memberships[candidate]]
136
+ assert isinstance(other, tuple), other
137
+ assert isinstance(s.memberships[other], tuple), s.memberships[other]
138
+ other_cluster = s.clusters[s.memberships[other]]
139
+ other_cluster_name = s.memberships[other]
140
+ this_cluster.extend(other_cluster)
130
141
  # Update the other cluster tiles that they now point to the new cluster. This also updates the other tile.
131
- for member in s.cluster_tiles[other]:
132
- s.cluster_tiles[member] = s.cluster_tiles[candidate]
142
+ for member in other_cluster:
143
+ s.memberships[member] = s.memberships[candidate]
144
+ del s.clusters[other_cluster_name]
133
145
  new_clusters = True
134
146
 
135
147
  if new_clusters:
136
148
  max_cluster_size = max(
137
- (len(members) for members in s.cluster_tiles.values()),
149
+ (len(members) for members in s.clusters.values()),
138
150
  default=0,
139
151
  )
140
152
  if max_cluster_size > max_cluster_so_far:
@@ -1,6 +1,7 @@
1
1
  import functools
2
2
  import logging
3
3
  import pathlib
4
+ import pickle
4
5
 
5
6
  import pandas as pd
6
7
 
@@ -75,14 +76,16 @@ def reduce_tile_group(group: pd.DataFrame) -> pd.DataFrame:
75
76
  def get_tile_history(repository: ActivityRepository, zoom: int) -> pd.DataFrame:
76
77
  logger.info("Building explorer tile history from all activities …")
77
78
 
78
- cache_file = pathlib.Path(f"Cache/first_time_per_tile_{zoom}.parquet")
79
+ cache_file = pathlib.Path(f"Cache/first_time_per_tile_{zoom}.pickle")
80
+
79
81
  if cache_file.exists():
80
- tiles = pd.read_parquet(cache_file)
82
+ with open(cache_file, "rb") as f:
83
+ tile_visits = pickle.load(f)
81
84
  else:
82
- tiles = pd.DataFrame()
85
+ tile_visits = {}
83
86
 
84
87
  with work_tracker(
85
- pathlib.Path(f"Cache/task_first_time_per_tile_{zoom}.json")
88
+ pathlib.Path(f"Cache/task_first_time_per_tile_{zoom}_v2.json")
86
89
  ) as parsed_activities:
87
90
  for activity in repository.iter_activities(new_to_old=False):
88
91
  if activity.id in parsed_activities:
@@ -93,22 +96,33 @@ def get_tile_history(repository: ActivityRepository, zoom: int) -> pd.DataFrame:
93
96
  shard = get_first_tiles(activity.id, repository, zoom)
94
97
  if not len(shard):
95
98
  continue
96
- shard2 = pd.DataFrame(
97
- {
98
- "tile_x": shard["tile_x"],
99
- "tile_y": shard["tile_y"],
100
- "first_id": activity.id,
101
- "last_id": activity.id,
102
- "first_time": shard["time"],
103
- "last_time": shard["time"],
104
- "count": 1,
105
- }
106
- )
107
- tiles = pd.concat([tiles, shard2])
108
- logger.info("Consolidating explorer tile history …")
109
- tiles = tiles.groupby(["tile_x", "tile_y"]).apply(reduce_tile_group).reset_index()
99
+ for _, row in shard.iterrows():
100
+ tile = (row["tile_x"], row["tile_y"])
101
+ if tile in tile_visits:
102
+ d = tile_visits[tile]
103
+ d["count"] += 1
104
+ if d["first_time"] > row["time"]:
105
+ d["first_time"] = row["time"]
106
+ d["first_id"] = activity.id
107
+ if d["last_time"] < row["time"]:
108
+ d["last_time"] = row["time"]
109
+ d["last_id"] = activity.id
110
+ else:
111
+ tile_visits[tile] = {
112
+ "count": 1,
113
+ "first_time": row["time"],
114
+ "first_id": activity.id,
115
+ "last_time": row["time"],
116
+ "last_id": activity.id,
117
+ }
110
118
 
111
119
  logger.info("Store explorer tile history to cache file …")
112
- tiles.to_parquet(cache_file)
120
+ with open(cache_file, "wb") as f:
121
+ pickle.dump(tile_visits, f)
113
122
 
123
+ tiles = pd.DataFrame(
124
+ [{"tile_x": x, "tile_y": y, **meta} for (x, y), meta in tile_visits.items()]
125
+ )
126
+ parquet_output = pathlib.Path(f"Cache/first_time_per_tile_{zoom}.parquet")
127
+ tiles.to_parquet(parquet_output)
114
128
  return tiles
@@ -1,7 +1,9 @@
1
1
  import datetime
2
+ import itertools
2
3
  import logging
3
4
  import pathlib
4
5
  from typing import Iterator
6
+ from typing import Optional
5
7
 
6
8
  import geojson
7
9
  import gpxpy
@@ -45,7 +47,7 @@ def get_three_color_tiles(
45
47
  cmap_last(max(1 - last_age_days / (2 * 365), 0.0))
46
48
  ),
47
49
  "cluster": False,
48
- "color": None,
50
+ "color": "#303030",
49
51
  "first_visit": row["first_time"].date().isoformat(),
50
52
  "last_visit": row["last_time"].date().isoformat(),
51
53
  "num_visits": row["count"],
@@ -74,26 +76,32 @@ def get_three_color_tiles(
74
76
  for x in range(square_x, square_x + square_size):
75
77
  for y in range(square_y, square_y + square_size):
76
78
  tile_dict[(x, y)]["square"] = True
77
- tile_dict[(x, y)]["color"] = "blue"
78
79
 
79
80
  # Add cluster information.
80
- for xy, members in cluster_state.cluster_tiles.items():
81
- tile_dict[xy]["this_cluster_size"] = len(members)
82
- tile_dict[xy]["cluster"] = True
81
+ for members in cluster_state.clusters.values():
82
+ for member in members:
83
+ tile_dict[member]["this_cluster_size"] = len(members)
84
+ tile_dict[member]["cluster"] = True
83
85
  if len(cluster_state.cluster_evolution) > 0:
84
86
  max_cluster_size = cluster_state.cluster_evolution["max_cluster_size"].iloc[-1]
85
87
  else:
86
88
  max_cluster_size = 0
87
- num_cluster_tiles = len(cluster_state.cluster_tiles)
89
+ num_cluster_tiles = len(cluster_state.memberships)
88
90
 
89
91
  # Apply cluster colors.
90
- for xy, xy_dict in tile_dict.items():
91
- if xy_dict["square"]:
92
- xy_dict["color"] = "blue"
93
- elif xy_dict["cluster"]:
94
- xy_dict["color"] = "green"
95
- else:
96
- xy_dict["color"] = "red"
92
+ cluster_cmap = matplotlib.colormaps["tab10"]
93
+ for color, members in zip(
94
+ itertools.cycle(map(cluster_cmap, [0, 1, 2, 3, 4, 5, 6, 8, 9])),
95
+ sorted(
96
+ cluster_state.clusters.values(),
97
+ key=lambda members: len(members),
98
+ reverse=True,
99
+ ),
100
+ ):
101
+ hex_color = matplotlib.colors.to_hex(color)
102
+ print(f"Color cluster with {len(members)} tiles in {hex_color}.")
103
+ for member in members:
104
+ tile_dict[member]["color"] = hex_color
97
105
 
98
106
  result = {
99
107
  "explored_geojson": geojson.dumps(
@@ -113,6 +121,19 @@ def get_three_color_tiles(
113
121
  "num_cluster_tiles": num_cluster_tiles,
114
122
  "num_tiles": len(tile_dict),
115
123
  "square_size": square_size,
124
+ "square_geojson": geojson.dumps(
125
+ geojson.FeatureCollection(
126
+ features=[
127
+ make_explorer_rectangle(
128
+ square_x,
129
+ square_y,
130
+ square_x + square_size,
131
+ square_y + square_size,
132
+ zoom,
133
+ )
134
+ ]
135
+ )
136
+ ),
116
137
  }
117
138
  return result
118
139
 
@@ -134,15 +155,23 @@ def get_explored_tiles(tiles: pd.DataFrame, zoom: int) -> list[list[list[float]]
134
155
 
135
156
  def make_explorer_tile(
136
157
  tile_x: int, tile_y: int, properties: dict, zoom: int
158
+ ) -> geojson.Feature:
159
+ return make_explorer_rectangle(
160
+ tile_x, tile_y, tile_x + 1, tile_y + 1, zoom, properties
161
+ )
162
+
163
+
164
+ def make_explorer_rectangle(
165
+ x1: int, y1: int, x2: int, y2: int, zoom: int, properties: Optional[dict] = None
137
166
  ) -> geojson.Feature:
138
167
  corners = [
139
168
  get_tile_upper_left_lat_lon(*args)
140
169
  for args in [
141
- (tile_x, tile_y, zoom),
142
- (tile_x + 1, tile_y, zoom),
143
- (tile_x + 1, tile_y + 1, zoom),
144
- (tile_x, tile_y + 1, zoom),
145
- (tile_x, tile_y, zoom),
170
+ (x1, y1, zoom),
171
+ (x2, y1, zoom),
172
+ (x2, y2, zoom),
173
+ (x1, y2, zoom),
174
+ (x1, y1, zoom),
146
175
  ]
147
176
  ]
148
177
  return geojson.Feature(
@@ -51,9 +51,9 @@ class ExplorerController:
51
51
  "latitude": median_lat,
52
52
  "longitude": median_lon,
53
53
  "bbox": bounding_box_for_biggest_cluster(
54
- cluster_state.cluster_tiles.values(), zoom
54
+ cluster_state.clusters.values(), zoom
55
55
  )
56
- if len(cluster_state.cluster_tiles) > 0
56
+ if len(cluster_state.memberships) > 0
57
57
  else {},
58
58
  },
59
59
  "explored": explored,
@@ -69,6 +69,8 @@ class ExplorerController:
69
69
 
70
70
 
71
71
  def plot_tile_evolution(tiles: pd.DataFrame) -> str:
72
+ if len(tiles) == 0:
73
+ return ""
72
74
  tiles.sort_values("first_time", inplace=True)
73
75
  tiles["count"] = np.arange(1, len(tiles) + 1)
74
76
  return (
@@ -83,6 +85,8 @@ def plot_tile_evolution(tiles: pd.DataFrame) -> str:
83
85
 
84
86
 
85
87
  def plot_cluster_evolution(cluster_evolution: pd.DataFrame) -> str:
88
+ if len(cluster_evolution) == 0:
89
+ return ""
86
90
  return (
87
91
  alt.Chart(cluster_evolution, title="Cluster")
88
92
  .mark_line(interpolate="step-after")
@@ -96,6 +100,8 @@ def plot_cluster_evolution(cluster_evolution: pd.DataFrame) -> str:
96
100
 
97
101
 
98
102
  def plot_square_evolution(square_evolution: pd.DataFrame) -> str:
103
+ if len(square_evolution) == 0:
104
+ return ""
99
105
  return (
100
106
  alt.Chart(square_evolution, title="Square")
101
107
  .mark_line(interpolate="step-after")
@@ -48,6 +48,7 @@
48
48
  }
49
49
 
50
50
  let explorer_geojson = {{ explored.explored_geojson| safe}}
51
+ let square_geojson = {{ explored.square_geojson | safe }}
51
52
  let map = L.map('explorer-map', {
52
53
  fullscreenControl: true,
53
54
  center: [{{ center.latitude }}, {{ center.longitude }}],
@@ -75,6 +76,16 @@
75
76
  map.fitBounds(L.geoJSON(bbox).getBounds())
76
77
  }
77
78
 
79
+ let explorer_square_layer = L.geoJSON(square_geojson,
80
+ {
81
+ style: function (feature) {
82
+ return {
83
+ color: "blue", fill: false, weight: 2
84
+ }
85
+ }
86
+ }
87
+ ).addTo(map)
88
+
78
89
  active_layer = explorer_layer_cluster_color
79
90
 
80
91
  function changeColor(method) {
@@ -100,13 +111,19 @@
100
111
 
101
112
  <div class="row mb-3">
102
113
  <div class="col-md-4">
114
+ {% if plot_tile_evolution|length > 0 %}
103
115
  {{ vega_direct("plot_tile_evolution", plot_tile_evolution) }}
116
+ {% endif %}
104
117
  </div>
105
118
  <div class="col-md-4">
119
+ {% if plot_cluster_evolution|length > 0 %}
106
120
  {{ vega_direct("plot_cluster_evolution", plot_cluster_evolution) }}
121
+ {% endif %}
107
122
  </div>
108
123
  <div class="col-md-4">
124
+ {% if plot_square_evolution|length > 0 %}
109
125
  {{ vega_direct("plot_square_evolution", plot_square_evolution) }}
126
+ {% endif %}
110
127
  </div>
111
128
  </div>
112
129
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "geo-activity-playground"
3
- version = "0.11.0"
3
+ version = "0.12.0"
4
4
  description = "Analysis of geo data activities like rides, runs or hikes."
5
5
  authors = ["Martin Ueding <mu@martin-ueding.de>"]
6
6
  license = "MIT"
@@ -40,6 +40,7 @@ black = "^22.3.0"
40
40
  mkdocs-material = "^9.4.1"
41
41
  mypy = "^0.961"
42
42
  pytest = "^7.1.2"
43
+ py-spy = "^0.3.14"
43
44
 
44
45
  [build-system]
45
46
  requires = ["poetry-core>=1.0.0"]