geo-activity-playground 0.31.0__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.
- geo_activity_playground/__main__.py +1 -1
- geo_activity_playground/core/activities.py +1 -1
- geo_activity_playground/core/config.py +5 -1
- geo_activity_playground/core/heatmap.py +61 -15
- geo_activity_playground/core/tiles.py +8 -5
- geo_activity_playground/explorer/video.py +2 -1
- geo_activity_playground/importers/directory.py +6 -2
- geo_activity_playground/importers/strava_checkout.py +8 -2
- geo_activity_playground/webui/activity/blueprint.py +7 -0
- geo_activity_playground/webui/activity/controller.py +127 -43
- geo_activity_playground/webui/activity/templates/activity/day.html.j2 +6 -2
- geo_activity_playground/webui/activity/templates/activity/lines.html.j2 +1 -1
- geo_activity_playground/webui/activity/templates/activity/name.html.j2 +3 -2
- geo_activity_playground/webui/activity/templates/activity/show.html.j2 +2 -2
- geo_activity_playground/webui/app.py +15 -3
- geo_activity_playground/webui/entry_controller.py +1 -1
- geo_activity_playground/webui/equipment/controller.py +1 -1
- geo_activity_playground/webui/explorer/templates/explorer/index.html.j2 +20 -44
- geo_activity_playground/webui/heatmap/blueprint.py +5 -2
- geo_activity_playground/webui/heatmap/heatmap_controller.py +14 -4
- geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +1 -1
- geo_activity_playground/webui/settings/blueprint.py +44 -33
- geo_activity_playground/webui/settings/templates/settings/color-schemes.html.j2 +11 -2
- geo_activity_playground/webui/settings/templates/settings/privacy-zones.html.j2 +1 -1
- geo_activity_playground/webui/square_planner/templates/square_planner/index.html.j2 +1 -1
- geo_activity_playground/webui/static/Leaflet.fullscreen.min.js +1 -0
- geo_activity_playground/webui/static/MarkerCluster.Default.css +60 -0
- geo_activity_playground/webui/static/MarkerCluster.css +14 -0
- geo_activity_playground/webui/static/bootstrap.min.css +6 -0
- geo_activity_playground/webui/static/fullscreen.png +0 -0
- geo_activity_playground/webui/static/fullscreen@2x.png +0 -0
- geo_activity_playground/webui/static/leaflet.css +661 -0
- geo_activity_playground/webui/static/leaflet.fullscreen.css +40 -0
- geo_activity_playground/webui/static/leaflet.js +6 -0
- geo_activity_playground/webui/static/leaflet.markercluster.js +3 -0
- geo_activity_playground/webui/static/table-sort.min.js +8 -0
- geo_activity_playground/webui/static/vega-embed@6 +7 -0
- geo_activity_playground/webui/static/vega-lite@4 +2 -0
- geo_activity_playground/webui/static/vega@5 +2 -0
- geo_activity_playground/webui/summary/controller.py +20 -25
- geo_activity_playground/webui/summary/templates/summary/index.html.j2 +18 -5
- geo_activity_playground/webui/templates/home.html.j2 +4 -3
- geo_activity_playground/webui/templates/page.html.j2 +14 -16
- geo_activity_playground/webui/tile/blueprint.py +3 -2
- geo_activity_playground/webui/tile/controller.py +7 -3
- geo_activity_playground/webui/{upload/controller.py → upload_blueprint.py} +41 -35
- {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/METADATA +5 -6
- {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/RECORD +53 -41
- geo_activity_playground/webui/upload/__init__.py +0 -0
- geo_activity_playground/webui/upload/blueprint.py +0 -44
- /geo_activity_playground/webui/{upload/templates → templates}/upload/index.html.j2 +0 -0
- /geo_activity_playground/webui/{upload/templates → templates}/upload/reload.html.j2 +0 -0
- {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.31.0.dist-info → geo_activity_playground-0.34.1.dist-info}/entry_points.txt +0 -0
@@ -13,7 +13,7 @@ from geo_activity_playground.core.config import import_old_strava_config
|
|
13
13
|
from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
|
14
14
|
from geo_activity_playground.explorer.video import explorer_video_main
|
15
15
|
from geo_activity_playground.webui.app import web_ui_main
|
16
|
-
from geo_activity_playground.webui.
|
16
|
+
from geo_activity_playground.webui.upload_blueprint import scan_for_activities
|
17
17
|
|
18
18
|
logger = logging.getLogger(__name__)
|
19
19
|
|
@@ -21,14 +21,16 @@ logger = logging.getLogger(__name__)
|
|
21
21
|
@dataclasses.dataclass
|
22
22
|
class Config:
|
23
23
|
birth_year: Optional[int] = None
|
24
|
-
color_scheme_for_counts: str = "
|
24
|
+
color_scheme_for_counts: str = "teals"
|
25
25
|
color_scheme_for_kind: str = "category10"
|
26
|
+
color_scheme_for_heatmap: str = "hot"
|
26
27
|
equipment_offsets: dict[str, float] = dataclasses.field(default_factory=dict)
|
27
28
|
explorer_zoom_levels: list[int] = dataclasses.field(
|
28
29
|
default_factory=lambda: [14, 17]
|
29
30
|
)
|
30
31
|
heart_rate_resting: int = 0
|
31
32
|
heart_rate_maximum: Optional[int] = None
|
33
|
+
ignore_suffixes: list[str] = dataclasses.field(default_factory=list)
|
32
34
|
kind_renames: dict[str, str] = dataclasses.field(default_factory=dict)
|
33
35
|
kinds_without_achievements: list[str] = dataclasses.field(default_factory=list)
|
34
36
|
metadata_extraction_regexes: list[str] = dataclasses.field(default_factory=list)
|
@@ -42,6 +44,8 @@ class Config:
|
|
42
44
|
strava_client_code: Optional[str] = None
|
43
45
|
time_diff_threshold_seconds: Optional[int] = 30
|
44
46
|
upload_password: Optional[str] = None
|
47
|
+
map_tile_url: str = "https://tile.openstreetmap.org/{zoom}/{x}/{y}.png"
|
48
|
+
map_tile_attribution: str = '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> | <a href="https://www.openstreetmap.org/fixthemap">Correct Map</a>'
|
45
49
|
|
46
50
|
|
47
51
|
class ConfigAccessor:
|
@@ -6,6 +6,7 @@ import logging
|
|
6
6
|
|
7
7
|
import numpy as np
|
8
8
|
|
9
|
+
from geo_activity_playground.core.config import Config
|
9
10
|
from geo_activity_playground.core.tiles import compute_tile_float
|
10
11
|
from geo_activity_playground.core.tiles import get_tile
|
11
12
|
from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
|
@@ -123,21 +124,66 @@ def get_sensible_zoom_level(
|
|
123
124
|
)
|
124
125
|
|
125
126
|
|
126
|
-
def
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
127
|
+
def build_map_from_tiles_around_center(
|
128
|
+
center: tuple[float, float],
|
129
|
+
zoom: int,
|
130
|
+
target: tuple[int, int],
|
131
|
+
inner_target: tuple[int, int],
|
132
|
+
config: Config,
|
133
|
+
) -> np.ndarray:
|
134
|
+
background = np.zeros((target[1], target[0], 3))
|
135
|
+
|
136
|
+
# We will work with the center point and have it in terms of tiles `t` and also in terms of pixels `p`. At the start we know that the tile center must be in the middle of the image.
|
137
|
+
t = np.array(center)
|
138
|
+
p = np.array([inner_target[0] / 2, inner_target[1] / 2])
|
139
|
+
|
140
|
+
# Shift both such that they are in the top-left corner of an even tile.
|
141
|
+
t_offset = np.array([center[0] % 1, center[1] % 1])
|
142
|
+
t -= t_offset
|
143
|
+
p -= t_offset * OSM_TILE_SIZE
|
144
|
+
|
145
|
+
# Shift until we have left the image.
|
146
|
+
shift = np.ceil(p / OSM_TILE_SIZE)
|
147
|
+
p -= shift * OSM_TILE_SIZE
|
148
|
+
t -= shift
|
149
|
+
|
150
|
+
num_tiles = np.ceil(np.array(target) / OSM_TILE_SIZE) + 1
|
151
|
+
|
152
|
+
for x in range(int(t[0]), int(t[0] + num_tiles[0])):
|
153
|
+
for y in range(int(t[1]), int(t[1]) + int(num_tiles[1])):
|
154
|
+
source_x_min = 0
|
155
|
+
source_y_min = 0
|
156
|
+
source_x_max = source_x_min + OSM_TILE_SIZE
|
157
|
+
source_y_max = source_y_min + OSM_TILE_SIZE
|
158
|
+
|
159
|
+
target_x_min = (x - int(t[0])) * OSM_TILE_SIZE + int(p[0])
|
160
|
+
target_y_min = (y - int(t[1])) * OSM_TILE_SIZE + int(p[1])
|
161
|
+
target_x_max = target_x_min + OSM_TILE_SIZE
|
162
|
+
target_y_max = target_y_min + OSM_TILE_SIZE
|
163
|
+
|
164
|
+
if target_x_min < 0:
|
165
|
+
source_x_min -= target_x_min
|
166
|
+
target_x_min = 0
|
167
|
+
if target_y_min < 0:
|
168
|
+
source_y_min -= target_y_min
|
169
|
+
target_y_min = 0
|
170
|
+
if target_x_max > target[0]:
|
171
|
+
a = target_x_max - target[0]
|
172
|
+
target_x_max -= a
|
173
|
+
source_x_max -= a
|
174
|
+
if target_y_max > target[1]:
|
175
|
+
a = target_y_max - target[1]
|
176
|
+
target_y_max -= a
|
177
|
+
source_y_max -= a
|
178
|
+
|
179
|
+
if source_x_max < 0 or source_y_max < 0:
|
180
|
+
continue
|
181
|
+
|
182
|
+
tile = np.array(get_tile(zoom, x, y, config.map_tile_url)) / 255
|
183
|
+
|
184
|
+
background[target_y_min:target_y_max, target_x_min:target_x_max] = tile[
|
185
|
+
source_y_min:source_y_max, source_x_min:source_x_max, :3
|
186
|
+
]
|
141
187
|
|
142
188
|
return background
|
143
189
|
|
@@ -3,6 +3,7 @@ import logging
|
|
3
3
|
import math
|
4
4
|
import pathlib
|
5
5
|
import time
|
6
|
+
import urllib.parse
|
6
7
|
from typing import Iterator
|
7
8
|
from typing import Optional
|
8
9
|
|
@@ -13,8 +14,10 @@ from PIL import Image
|
|
13
14
|
logger = logging.getLogger(__name__)
|
14
15
|
|
15
16
|
|
16
|
-
def osm_tile_path(x: int, y: int, zoom: int) -> pathlib.Path:
|
17
|
-
|
17
|
+
def osm_tile_path(x: int, y: int, zoom: int, url_template: str) -> pathlib.Path:
|
18
|
+
base_dir = pathlib.Path("Open Street Map Tiles")
|
19
|
+
dir_for_source = base_dir / urllib.parse.quote_plus(url_template)
|
20
|
+
path = dir_for_source / f"{zoom}/{x}/{y}.png"
|
18
21
|
path.parent.mkdir(parents=True, exist_ok=True)
|
19
22
|
return path
|
20
23
|
|
@@ -62,11 +65,11 @@ def download_file(url: str, destination: pathlib.Path):
|
|
62
65
|
|
63
66
|
|
64
67
|
@functools.lru_cache()
|
65
|
-
def get_tile(zoom: int, x: int, y: int) -> Image.Image:
|
66
|
-
destination = osm_tile_path(x, y, zoom)
|
68
|
+
def get_tile(zoom: int, x: int, y: int, url_template: str) -> Image.Image:
|
69
|
+
destination = osm_tile_path(x, y, zoom, url_template)
|
67
70
|
if not destination.exists():
|
68
71
|
logger.info(f"Downloading OSM tile {x=}, {y=}, {zoom=} …")
|
69
|
-
url =
|
72
|
+
url = url_template.format(x=x, y=y, zoom=zoom)
|
70
73
|
download_file(url, destination)
|
71
74
|
with Image.open(destination) as image:
|
72
75
|
image.load()
|
@@ -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,
|
@@ -10,6 +10,7 @@ from typing import Optional
|
|
10
10
|
from tqdm import tqdm
|
11
11
|
|
12
12
|
from geo_activity_playground.core.activities import ActivityMeta
|
13
|
+
from geo_activity_playground.core.config import Config
|
13
14
|
from geo_activity_playground.core.paths import activity_extracted_dir
|
14
15
|
from geo_activity_playground.core.paths import activity_extracted_meta_dir
|
15
16
|
from geo_activity_playground.core.paths import activity_extracted_time_series_dir
|
@@ -24,13 +25,16 @@ ACTIVITY_DIR = pathlib.Path("Activities")
|
|
24
25
|
|
25
26
|
|
26
27
|
def import_from_directory(
|
27
|
-
metadata_extraction_regexes: list[str], num_processes: Optional[int]
|
28
|
+
metadata_extraction_regexes: list[str], num_processes: Optional[int], config: Config
|
28
29
|
) -> None:
|
29
30
|
|
30
31
|
activity_paths = [
|
31
32
|
path
|
32
33
|
for path in ACTIVITY_DIR.rglob("*.*")
|
33
|
-
if path.is_file()
|
34
|
+
if path.is_file()
|
35
|
+
and path.suffixes
|
36
|
+
and not path.stem.startswith(".")
|
37
|
+
and not path.suffix in config.ignore_suffixes
|
34
38
|
]
|
35
39
|
work_tracker = WorkTracker(activity_extracted_dir() / "work-tracker-extract.pickle")
|
36
40
|
new_activity_paths = work_tracker.filter(activity_paths)
|
@@ -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
|
-
|
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(
|
@@ -21,11 +21,9 @@ from geo_activity_playground.core.activities import make_geojson_from_time_serie
|
|
21
21
|
from geo_activity_playground.core.activities import make_speed_color_bar
|
22
22
|
from geo_activity_playground.core.config import Config
|
23
23
|
from geo_activity_playground.core.heart_rate import HeartRateZoneComputer
|
24
|
-
from geo_activity_playground.core.heatmap import
|
25
|
-
from geo_activity_playground.core.heatmap import build_map_from_tiles
|
24
|
+
from geo_activity_playground.core.heatmap import build_map_from_tiles_around_center
|
26
25
|
from geo_activity_playground.core.heatmap import GeoBounds
|
27
|
-
from geo_activity_playground.core.heatmap import
|
28
|
-
from geo_activity_playground.core.heatmap import get_sensible_zoom_level
|
26
|
+
from geo_activity_playground.core.heatmap import OSM_MAX_ZOOM
|
29
27
|
from geo_activity_playground.core.heatmap import OSM_TILE_SIZE
|
30
28
|
from geo_activity_playground.core.heatmap import PixelBounds
|
31
29
|
from geo_activity_playground.core.heatmap import TileBounds
|
@@ -128,7 +126,7 @@ class ActivityController:
|
|
128
126
|
if len(time_series) == 0:
|
129
127
|
time_series = self._repository.get_time_series(id)
|
130
128
|
return make_sharepic(
|
131
|
-
activity, time_series, self._config.sharepic_suppressed_fields
|
129
|
+
activity, time_series, self._config.sharepic_suppressed_fields, self._config
|
132
130
|
)
|
133
131
|
|
134
132
|
def render_day(self, year: int, month: int, day: int) -> dict:
|
@@ -172,8 +170,24 @@ class ActivityController:
|
|
172
170
|
"date": datetime.date(year, month, day).isoformat(),
|
173
171
|
"total_distance": activities_that_day["distance_km"].sum(),
|
174
172
|
"total_elapsed_time": activities_that_day["elapsed_time"].sum(),
|
173
|
+
"day": day,
|
174
|
+
"month": month,
|
175
|
+
"year": year,
|
175
176
|
}
|
176
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
|
+
|
177
191
|
def render_all(self) -> dict:
|
178
192
|
cmap = matplotlib.colormaps["Dark2"]
|
179
193
|
fc = geojson.FeatureCollection(
|
@@ -454,47 +468,74 @@ def pixels_in_bounds(bounds: PixelBounds) -> int:
|
|
454
468
|
return (bounds.x_max - bounds.x_min) * (bounds.y_max - bounds.y_min)
|
455
469
|
|
456
470
|
|
457
|
-
def
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
)
|
462
|
-
|
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"]
|
475
|
+
tile_width = tile_x.max() - tile_x.min()
|
476
|
+
tile_height = tile_y.max() - tile_y.min()
|
463
477
|
|
464
|
-
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
background = build_map_from_tiles(tile_bounds)
|
469
|
-
# background = convert_to_grayscale(background)
|
478
|
+
target_width = 600
|
479
|
+
target_height = 600
|
480
|
+
footer_height = 100
|
481
|
+
target_map_height = target_height - footer_height
|
470
482
|
|
471
|
-
|
472
|
-
|
483
|
+
zoom = int(
|
484
|
+
min(
|
485
|
+
np.log2(target_width / tile_width / OSM_TILE_SIZE),
|
486
|
+
np.log2(target_map_height / tile_height / OSM_TILE_SIZE),
|
487
|
+
OSM_MAX_ZOOM,
|
488
|
+
)
|
489
|
+
)
|
473
490
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
491
|
+
tile_xz = tile_x * 2**zoom
|
492
|
+
tile_yz = tile_y * 2**zoom
|
493
|
+
|
494
|
+
tile_xz_center = (
|
495
|
+
(tile_xz.max() + tile_xz.min()) / 2,
|
496
|
+
(tile_yz.max() + tile_yz.min()) / 2,
|
497
|
+
)
|
498
|
+
|
499
|
+
background = build_map_from_tiles_around_center(
|
500
|
+
tile_xz_center,
|
501
|
+
zoom,
|
502
|
+
(target_width, target_height),
|
503
|
+
(target_width, target_map_height),
|
504
|
+
config,
|
505
|
+
)
|
479
506
|
|
480
507
|
img = Image.fromarray((background * 255).astype("uint8"), "RGB")
|
481
508
|
draw = ImageDraw.Draw(img, mode="RGBA")
|
482
509
|
|
483
|
-
for
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
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
|
+
)
|
491
518
|
)
|
492
|
-
for x, y in zip(xs, ys)
|
493
|
-
)
|
494
519
|
|
495
|
-
|
520
|
+
draw.line(yx, fill="red", width=4)
|
521
|
+
|
522
|
+
return img
|
496
523
|
|
497
|
-
|
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")
|
536
|
+
draw.rectangle(
|
537
|
+
[0, img.height - footer_height, img.width, img.height], fill=(0, 0, 0, 180)
|
538
|
+
)
|
498
539
|
|
499
540
|
facts = {
|
500
541
|
"kind": f"{activity['kind']}",
|
@@ -515,19 +556,62 @@ def make_sharepic(
|
|
515
556
|
if not key in sharepic_suppressed_fields
|
516
557
|
}
|
517
558
|
|
518
|
-
draw.text(
|
559
|
+
draw.text(
|
560
|
+
(35, img.height - footer_height + 10),
|
561
|
+
" ".join(facts.values()),
|
562
|
+
font_size=20,
|
563
|
+
)
|
564
|
+
|
565
|
+
draw.text(
|
566
|
+
(img.width - 250, img.height - 20),
|
567
|
+
"Map: © Open Street Map Contributors",
|
568
|
+
font_size=14,
|
569
|
+
)
|
570
|
+
|
571
|
+
f = io.BytesIO()
|
572
|
+
img.save(f, format="png")
|
573
|
+
return bytes(f.getbuffer())
|
574
|
+
|
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
|
519
582
|
|
520
|
-
|
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
|
+
)
|
521
589
|
|
522
|
-
|
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")
|
523
594
|
|
524
|
-
|
525
|
-
|
526
|
-
|
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
|
+
)
|
527
612
|
|
528
613
|
f = io.BytesIO()
|
529
614
|
img.save(f, format="png")
|
530
|
-
# pl.imsave(f, background, format="png")
|
531
615
|
return bytes(f.getbuffer())
|
532
616
|
|
533
617
|
|
@@ -17,7 +17,7 @@
|
|
17
17
|
});
|
18
18
|
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
19
19
|
maxZoom: 19,
|
20
|
-
attribution: '
|
20
|
+
attribution: '{{ map_tile_attribution|safe }}'
|
21
21
|
}).addTo(map);
|
22
22
|
|
23
23
|
let geojson = L.geoJSON({{ geojson| safe }}, {
|
@@ -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('
|
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 %}
|
@@ -21,7 +21,7 @@
|
|
21
21
|
});
|
22
22
|
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
23
23
|
maxZoom: 19,
|
24
|
-
attribution: '
|
24
|
+
attribution: '{{ map_tile_attribution|safe }}'
|
25
25
|
}).addTo(map);
|
26
26
|
|
27
27
|
let geojson = L.geoJSON({{ geojson| safe }}, {
|
@@ -17,7 +17,7 @@
|
|
17
17
|
});
|
18
18
|
L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
|
19
19
|
maxZoom: 19,
|
20
|
-
attribution: '
|
20
|
+
attribution: '{{ map_tile_attribution|safe }}'
|
21
21
|
}).addTo(map);
|
22
22
|
|
23
23
|
let geojson = L.geoJSON({{ geojson| safe }}, {
|
@@ -64,7 +64,8 @@
|
|
64
64
|
<tbody>
|
65
65
|
{% for activity in activities %}
|
66
66
|
<tr>
|
67
|
-
<td><span style="color: {{ activity['color'] }};">█</span> <a
|
67
|
+
<td><span style="color: {{ activity['color'] }};">█</span> <a
|
68
|
+
href="{{ url_for('activity.show', id=activity.id) }}">{{
|
68
69
|
activity.name }}</a></td>
|
69
70
|
<td>{{ activity.start|dt }}</td>
|
70
71
|
<td>{{ activity.distance_km | round(1) }}</td>
|
@@ -52,7 +52,7 @@
|
|
52
52
|
});
|
53
53
|
L.tileLayer('/tile/pastel/{z}/{x}/{y}.png', {
|
54
54
|
maxZoom: 19,
|
55
|
-
attribution: '
|
55
|
+
attribution: '{{ map_tile_attribution|safe }}'
|
56
56
|
}).addTo(map);
|
57
57
|
|
58
58
|
let geojson = L.geoJSON({{ color_line_geojson| safe }}, {
|
@@ -160,7 +160,7 @@
|
|
160
160
|
})
|
161
161
|
L.tileLayer('/tile/color/{z}/{x}/{y}.png', {
|
162
162
|
maxZoom: 19,
|
163
|
-
attribution: '
|
163
|
+
attribution: '{{ map_tile_attribution|safe }}'
|
164
164
|
}).addTo(map)
|
165
165
|
|
166
166
|
let geojson_layer = L.geoJSON(geojson).addTo(map)
|
@@ -3,6 +3,8 @@ import importlib
|
|
3
3
|
import json
|
4
4
|
import pathlib
|
5
5
|
import secrets
|
6
|
+
import shutil
|
7
|
+
import urllib.parse
|
6
8
|
|
7
9
|
from flask import Flask
|
8
10
|
from flask import render_template
|
@@ -20,7 +22,7 @@ from .search.blueprint import make_search_blueprint
|
|
20
22
|
from .square_planner.blueprint import make_square_planner_blueprint
|
21
23
|
from .summary.blueprint import make_summary_blueprint
|
22
24
|
from .tile.blueprint import make_tile_blueprint
|
23
|
-
from .
|
25
|
+
from .upload_blueprint import make_upload_blueprint
|
24
26
|
from geo_activity_playground.core.config import Config
|
25
27
|
from geo_activity_playground.core.config import ConfigAccessor
|
26
28
|
from geo_activity_playground.webui.auth.blueprint import make_auth_blueprint
|
@@ -97,7 +99,8 @@ def web_ui_main(
|
|
97
99
|
url_prefix="/explorer",
|
98
100
|
)
|
99
101
|
app.register_blueprint(
|
100
|
-
make_heatmap_blueprint(repository, tile_visit_accessor),
|
102
|
+
make_heatmap_blueprint(repository, tile_visit_accessor, config_accessor()),
|
103
|
+
url_prefix="/heatmap",
|
101
104
|
)
|
102
105
|
app.register_blueprint(
|
103
106
|
make_settings_blueprint(config_accessor, authenticator),
|
@@ -115,7 +118,7 @@ def web_ui_main(
|
|
115
118
|
make_summary_blueprint(repository, config_accessor()),
|
116
119
|
url_prefix="/summary",
|
117
120
|
)
|
118
|
-
app.register_blueprint(make_tile_blueprint(), url_prefix="/tile")
|
121
|
+
app.register_blueprint(make_tile_blueprint(config_accessor()), url_prefix="/tile")
|
119
122
|
app.register_blueprint(
|
120
123
|
make_upload_blueprint(
|
121
124
|
repository, tile_visit_accessor, config_accessor(), authenticator
|
@@ -123,11 +126,20 @@ def web_ui_main(
|
|
123
126
|
url_prefix="/upload",
|
124
127
|
)
|
125
128
|
|
129
|
+
base_dir = pathlib.Path("Open Street Map Tiles")
|
130
|
+
dir_for_source = base_dir / urllib.parse.quote_plus(config_accessor().map_tile_url)
|
131
|
+
if base_dir.exists() and not dir_for_source.exists():
|
132
|
+
subdirs = base_dir.glob("*")
|
133
|
+
dir_for_source.mkdir()
|
134
|
+
for subdir in subdirs:
|
135
|
+
shutil.move(subdir, dir_for_source)
|
136
|
+
|
126
137
|
@app.context_processor
|
127
138
|
def inject_global_variables() -> dict:
|
128
139
|
return {
|
129
140
|
"version": _try_get_version(),
|
130
141
|
"num_activities": len(repository),
|
142
|
+
"map_tile_attribution": config_accessor().map_tile_attribution,
|
131
143
|
}
|
132
144
|
|
133
145
|
app.run(host=host, port=port)
|
@@ -16,10 +16,10 @@ class EntryController:
|
|
16
16
|
self._config = config
|
17
17
|
|
18
18
|
def render(self) -> dict:
|
19
|
-
kind_scale = make_kind_scale(self._repository.meta, self._config)
|
20
19
|
result = {"latest_activities": []}
|
21
20
|
|
22
21
|
if len(self._repository):
|
22
|
+
kind_scale = make_kind_scale(self._repository.meta, self._config)
|
23
23
|
result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
|
24
24
|
self._repository.meta, kind_scale
|
25
25
|
)
|