geo-activity-playground 0.38.2__py3-none-any.whl → 0.39.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. geo_activity_playground/__main__.py +5 -47
  2. geo_activity_playground/alembic/README +1 -0
  3. geo_activity_playground/alembic/env.py +76 -0
  4. geo_activity_playground/alembic/script.py.mako +26 -0
  5. geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +33 -0
  6. geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +73 -0
  7. geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +28 -0
  8. geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +30 -0
  9. geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +28 -0
  10. geo_activity_playground/alembic/versions/script.py.mako +28 -0
  11. geo_activity_playground/core/activities.py +50 -136
  12. geo_activity_playground/core/config.py +3 -3
  13. geo_activity_playground/core/datamodel.py +257 -0
  14. geo_activity_playground/core/enrichment.py +90 -92
  15. geo_activity_playground/core/heart_rate.py +1 -2
  16. geo_activity_playground/core/paths.py +6 -7
  17. geo_activity_playground/core/raster_map.py +43 -4
  18. geo_activity_playground/core/similarity.py +1 -2
  19. geo_activity_playground/core/tasks.py +2 -2
  20. geo_activity_playground/core/test_meta_search.py +3 -3
  21. geo_activity_playground/core/test_summary_stats.py +1 -1
  22. geo_activity_playground/explorer/grid_file.py +2 -2
  23. geo_activity_playground/explorer/tile_visits.py +8 -10
  24. geo_activity_playground/heatmap_video.py +7 -8
  25. geo_activity_playground/importers/activity_parsers.py +2 -2
  26. geo_activity_playground/importers/directory.py +9 -10
  27. geo_activity_playground/importers/strava_api.py +9 -9
  28. geo_activity_playground/importers/strava_checkout.py +12 -13
  29. geo_activity_playground/importers/test_csv_parser.py +3 -3
  30. geo_activity_playground/importers/test_directory.py +1 -1
  31. geo_activity_playground/importers/test_strava_api.py +1 -1
  32. geo_activity_playground/webui/app.py +94 -86
  33. geo_activity_playground/webui/authenticator.py +1 -1
  34. geo_activity_playground/webui/{activity/controller.py → blueprints/activity_blueprint.py} +246 -108
  35. geo_activity_playground/webui/{auth_blueprint.py → blueprints/auth_blueprint.py} +1 -1
  36. geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +61 -0
  37. geo_activity_playground/webui/{calendar/controller.py → blueprints/calendar_blueprint.py} +19 -19
  38. geo_activity_playground/webui/{eddington_blueprint.py → blueprints/eddington_blueprint.py} +5 -5
  39. geo_activity_playground/webui/blueprints/entry_views.py +68 -0
  40. geo_activity_playground/webui/{equipment_blueprint.py → blueprints/equipment_blueprint.py} +37 -4
  41. geo_activity_playground/webui/{explorer/controller.py → blueprints/explorer_blueprint.py} +88 -54
  42. geo_activity_playground/webui/blueprints/heatmap_blueprint.py +233 -0
  43. geo_activity_playground/webui/{search_blueprint.py → blueprints/search_blueprint.py} +7 -11
  44. geo_activity_playground/webui/blueprints/settings_blueprint.py +446 -0
  45. geo_activity_playground/webui/{square_planner_blueprint.py → blueprints/square_planner_blueprint.py} +31 -6
  46. geo_activity_playground/webui/{summary_blueprint.py → blueprints/summary_blueprint.py} +11 -23
  47. geo_activity_playground/webui/blueprints/tile_blueprint.py +27 -0
  48. geo_activity_playground/webui/{upload_blueprint.py → blueprints/upload_blueprint.py} +13 -18
  49. geo_activity_playground/webui/flasher.py +26 -0
  50. geo_activity_playground/webui/plot_util.py +1 -1
  51. geo_activity_playground/webui/search_util.py +4 -6
  52. geo_activity_playground/webui/static/images/layers-2x.png +0 -0
  53. geo_activity_playground/webui/static/images/layers.png +0 -0
  54. geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
  55. geo_activity_playground/webui/static/images/marker-icon.png +0 -0
  56. geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
  57. geo_activity_playground/webui/templates/activity/day.html.j2 +81 -0
  58. geo_activity_playground/webui/templates/activity/edit.html.j2 +38 -0
  59. geo_activity_playground/webui/{activity/templates → templates}/activity/name.html.j2 +29 -27
  60. geo_activity_playground/webui/{activity/templates → templates}/activity/show.html.j2 +57 -33
  61. geo_activity_playground/webui/templates/activity/trim.html.j2 +68 -0
  62. geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +26 -0
  63. geo_activity_playground/webui/templates/calendar/index.html.j2 +48 -0
  64. geo_activity_playground/webui/templates/calendar/month.html.j2 +57 -0
  65. geo_activity_playground/webui/templates/equipment/index.html.j2 +7 -0
  66. geo_activity_playground/webui/templates/home.html.j2 +6 -6
  67. geo_activity_playground/webui/templates/page.html.j2 +2 -1
  68. geo_activity_playground/webui/{settings/templates → templates}/settings/index.html.j2 +9 -20
  69. geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +49 -0
  70. geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +48 -0
  71. geo_activity_playground/webui/{settings/templates → templates}/settings/privacy-zones.html.j2 +2 -0
  72. geo_activity_playground/webui/{settings/templates → templates}/settings/strava.html.j2 +2 -0
  73. geo_activity_playground/webui/templates/square_planner/index.html.j2 +63 -13
  74. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/METADATA +5 -1
  75. geo_activity_playground-0.39.0.dist-info/RECORD +133 -0
  76. geo_activity_playground/__init__.py +0 -0
  77. geo_activity_playground/core/__init__.py +0 -0
  78. geo_activity_playground/explorer/__init__.py +0 -0
  79. geo_activity_playground/importers/__init__.py +0 -0
  80. geo_activity_playground/webui/__init__.py +0 -0
  81. geo_activity_playground/webui/activity/__init__.py +0 -0
  82. geo_activity_playground/webui/activity/blueprint.py +0 -109
  83. geo_activity_playground/webui/activity/templates/activity/day.html.j2 +0 -80
  84. geo_activity_playground/webui/activity/templates/activity/edit.html.j2 +0 -42
  85. geo_activity_playground/webui/calendar/__init__.py +0 -0
  86. geo_activity_playground/webui/calendar/blueprint.py +0 -23
  87. geo_activity_playground/webui/calendar/templates/calendar/index.html.j2 +0 -46
  88. geo_activity_playground/webui/calendar/templates/calendar/month.html.j2 +0 -55
  89. geo_activity_playground/webui/entry_controller.py +0 -63
  90. geo_activity_playground/webui/explorer/__init__.py +0 -0
  91. geo_activity_playground/webui/explorer/blueprint.py +0 -62
  92. geo_activity_playground/webui/heatmap/__init__.py +0 -0
  93. geo_activity_playground/webui/heatmap/blueprint.py +0 -51
  94. geo_activity_playground/webui/heatmap/heatmap_controller.py +0 -216
  95. geo_activity_playground/webui/settings/blueprint.py +0 -262
  96. geo_activity_playground/webui/settings/controller.py +0 -272
  97. geo_activity_playground/webui/settings/templates/settings/equipment-offsets.html.j2 +0 -44
  98. geo_activity_playground/webui/settings/templates/settings/kind-renames.html.j2 +0 -25
  99. geo_activity_playground/webui/settings/templates/settings/kinds-without-achievements.html.j2 +0 -30
  100. geo_activity_playground/webui/tile_blueprint.py +0 -42
  101. geo_activity_playground-0.38.2.dist-info/RECORD +0 -129
  102. /geo_activity_playground/webui/{activity/templates → templates}/activity/lines.html.j2 +0 -0
  103. /geo_activity_playground/webui/{explorer/templates → templates}/explorer/index.html.j2 +0 -0
  104. /geo_activity_playground/webui/{heatmap/templates → templates}/heatmap/index.html.j2 +0 -0
  105. /geo_activity_playground/webui/{settings/templates → templates}/settings/admin-password.html.j2 +0 -0
  106. /geo_activity_playground/webui/{settings/templates → templates}/settings/color-schemes.html.j2 +0 -0
  107. /geo_activity_playground/webui/{settings/templates → templates}/settings/heart-rate.html.j2 +0 -0
  108. /geo_activity_playground/webui/{settings/templates → templates}/settings/metadata-extraction.html.j2 +0 -0
  109. /geo_activity_playground/webui/{settings/templates → templates}/settings/segmentation.html.j2 +0 -0
  110. /geo_activity_playground/webui/{settings/templates → templates}/settings/sharepic.html.j2 +0 -0
  111. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/LICENSE +0 -0
  112. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/WHEEL +0 -0
  113. {geo_activity_playground-0.38.2.dist-info → geo_activity_playground-0.39.0.dist-info}/entry_points.txt +0 -0
@@ -1,80 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
- <div class="row mb-3">
5
- <div class="col">
6
- <h1>{{ date }}</h1>
7
- </div>
8
- </div>
9
-
10
-
11
- <div class="row mb-3">
12
- <div class="col-12">
13
- <div id="activity-map" style="height: 500px;"></div>
14
- <script>
15
- var map = L.map('activity-map', {
16
- fullscreenControl: true
17
- });
18
- L.tileLayer('/tile/grayscale/{z}/{x}/{y}.png', {
19
- maxZoom: 19,
20
- attribution: '{{ map_tile_attribution|safe }}'
21
- }).addTo(map);
22
-
23
- let geojson = L.geoJSON({{ geojson| safe }}, {
24
- style: function (feature) { return { color: feature.properties.color } }
25
- }).addTo(map)
26
- map.fitBounds(geojson.getBounds());
27
- </script>
28
- </div>
29
- </div>
30
-
31
- <div class="row mb-3">
32
- <div class="col">
33
- <h2>Activities</h2>
34
-
35
- <table class="table table-sort table-arrows">
36
- <thead>
37
- <tr>
38
- <th>Name</th>
39
- <th>Date</th>
40
- <th>Distance / km</th>
41
- <th>Elapsed time</th>
42
- <th>Speed / km/h</th>
43
- <th>Equipment</th>
44
- <th>Kind</th>
45
- </tr>
46
- </thead>
47
- <tbody>
48
- {% for activity in activities %}
49
- <tr>
50
- <td><span style="color: {{ activity['color'] }};">█</span> <a
51
- href="{{ url_for('activity.show', id=activity.id) }}">{{
52
- activity.name }}</a></td>
53
- <td>{{ activity.start|dt }}</td>
54
- <td>{{ activity.distance_km | round(1) }}</td>
55
- <td>{{ activity.elapsed_time|td }}</td>
56
- <td>{{ activity.average_speed_moving_kmh|round(1) }}</td>
57
- <td>{{ activity["equipment"] }}</td>
58
- <td>{{ activity["kind"] }}</td>
59
- </tr>
60
- {% endfor %}
61
- {% if activities|length > 1 %}
62
- <tr>
63
- <td><b>Total</b></td>
64
- <td></td>
65
- <td><b>{{ total_distance | round(1) }}</b></td>
66
- <td><b>{{ total_elapsed_time|td }}</b></td>
67
- <td></td>
68
- <td></td>
69
- </tr>
70
- {% endif %}
71
- </tbody>
72
- </table>
73
- </div>
74
- </div>
75
-
76
- <h2>Share picture</h2>
77
-
78
- <p><img class="img-fluid" src="{{ url_for('.day_sharepic', year=year, month=month, day=day) }}" /></p>
79
-
80
- {% endblock %}
@@ -1,42 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
-
5
- <h1 class="mb-3">Edit Activity</h1>
6
-
7
- <form method="POST">
8
- <div class="mb-3">
9
- <label for="name" class="form-label">Name</label>
10
- <input type="text" class="form-control" id="name" name="name" value="{{ override['name'] }}" />
11
- </div>
12
-
13
- <div class="mb-3">
14
- <label for="kind" class="form-label">Kind</label>
15
- <input type="text" class="form-control" id="kind" name="kind" value="{{ override['kind'] }}" />
16
- </div>
17
-
18
- <div class="mb-3">
19
- <label for="equipment" class="form-label">Equipment</label>
20
- <input type="text" class="form-control" id="equipment" name="equipment" value="{{ override['equipment'] }}" />
21
- </div>
22
-
23
- <div class="mb-3">
24
- <div class="form-check">
25
- <input type="checkbox" class="form-check-input" id="commute" name="commute" {% if override['commute'] %}
26
- checked {% endif %} />
27
- <label for="commute" class="form-check-label">Commute</label>
28
- </div>
29
- </div>
30
-
31
- <div class="mb-3">
32
- <div class="form-check">
33
- <input type="checkbox" class="form-check-input" id="consider_for_achievements"
34
- name="consider_for_achievements" {% if override['consider_for_achievements'] %} checked {% endif %} />
35
- <label for="consider_for_achievements" class="form-check-label">Consider for achievements</label>
36
- </div>
37
- </div>
38
-
39
- <button type="submit" class="btn btn-primary">Save</button>
40
- </form>
41
-
42
- {% endblock %}
File without changes
@@ -1,23 +0,0 @@
1
- from flask import Blueprint
2
- from flask import render_template
3
-
4
- from geo_activity_playground.webui.calendar.controller import CalendarController
5
-
6
-
7
- def make_calendar_blueprint(calendar_controller: CalendarController) -> Blueprint:
8
- blueprint = Blueprint("calendar", __name__, template_folder="templates")
9
-
10
- @blueprint.route("/")
11
- def index():
12
- return render_template(
13
- "calendar/index.html.j2", **calendar_controller.render_overview()
14
- )
15
-
16
- @blueprint.route("/<year>/<month>")
17
- def month(year: str, month: str):
18
- return render_template(
19
- "calendar/month.html.j2",
20
- **calendar_controller.render_month(int(year), int(month))
21
- )
22
-
23
- return blueprint
@@ -1,46 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
- <div class="row mb-1">
5
- <div class="col">
6
- <h1>Calendar</h1>
7
- </div>
8
- </div>
9
-
10
- <div class="row mb-1">
11
- <div class="col">
12
- <table class="table">
13
- <thead>
14
- <tr>
15
- <th>Year</th>
16
- {% for i in range(1, 13) %}
17
- <th style="text-align: right;">{{ i }}</th>
18
- {% endfor %}
19
- <th style="text-align: right;">Total</th>
20
- </tr>
21
- </thead>
22
- <tbody>
23
- {% for year, month_data in monthly_distances.items() %}
24
- {% set year_int = year|round(0)|int %}
25
- <tr>
26
- <td>{{ year_int }}</td>
27
- {% for month in range(1, 13) %}
28
- <td align="right">
29
- {% set distance = month_data[month] %}
30
- {% if distance %}
31
- <a href="{{ url_for(".month", year=year_int, month=month) }}">{{ distance|int() }} km</a>
32
- {% else %}
33
- 0 km
34
- {% endif %}
35
- </td>
36
- {% endfor %}
37
- <td align="right">{{ yearly_distances[year]|int() }} km</td>
38
- </tr>
39
- {% endfor %}
40
- </tbody>
41
- </table>
42
- </div>
43
- </div>
44
-
45
-
46
- {% endblock %}
@@ -1,55 +0,0 @@
1
- {% extends "page.html.j2" %}
2
-
3
- {% block container %}
4
- <div class="row mb-1">
5
- <div class="col">
6
- <h1>Calendar {{ year }}-{{ "{0:02d}".format(month) }}</h1>
7
- </div>
8
- </div>
9
-
10
- <div class="row mb-1">
11
- <div class="col">
12
- <table class="table">
13
- <thead>
14
- <tr>
15
- <th>Week</th>
16
- <th>Monday</th>
17
- <th>Tuesday</th>
18
- <th>Wednesday</th>
19
- <th>Thursday</th>
20
- <th>Friday</th>
21
- <th>Saturday</th>
22
- <th>Sunday</th>
23
- </tr>
24
- </thead>
25
- <tbody>
26
- {% for week, week_data in weeks.items() %}
27
- <tr>
28
- <td>{{ week }}</td>
29
- {% for day in range(1, 8) %}
30
- <td>
31
- {% if weeks[week][day] %}
32
- <a href="{{ url_for('activity.day', year=year, month=month, day=day_of_month[week][day]) }}"><b>{{
33
- day_of_month[week][day] }}.</b></a>
34
- {% elif day_of_month[week][day] %}
35
- <b>{{ day_of_month[week][day] }}.</b>
36
- {% endif %}
37
-
38
- {% if weeks[week][day] %}
39
- <ul>
40
- {% for activity in weeks[week][day] %}
41
- <li><a href="{{ url_for('activity.show', id=activity.id) }}">{{ activity.name }}</a></li>
42
- {% endfor %}
43
- </ul>
44
- {% endif %}
45
- </td>
46
- {% endfor %}
47
- </tr>
48
- {% endfor %}
49
- </tbody>
50
- </table>
51
- </div>
52
- </div>
53
-
54
-
55
- {% endblock %}
@@ -1,63 +0,0 @@
1
- import datetime
2
- import itertools
3
-
4
- import altair as alt
5
- import pandas as pd
6
-
7
- from geo_activity_playground.core.activities import ActivityRepository
8
- from geo_activity_playground.core.activities import make_geojson_from_time_series
9
- from geo_activity_playground.core.config import Config
10
- from geo_activity_playground.webui.plot_util import make_kind_scale
11
-
12
-
13
- class EntryController:
14
- def __init__(self, repository: ActivityRepository, config: Config) -> None:
15
- self._repository = repository
16
- self._config = config
17
-
18
- def render(self) -> dict:
19
- result = {"latest_activities": []}
20
-
21
- if len(self._repository):
22
- kind_scale = make_kind_scale(self._repository.meta, self._config)
23
- result["distance_last_30_days_plot"] = distance_last_30_days_meta_plot(
24
- self._repository.meta, kind_scale
25
- )
26
-
27
- for activity in itertools.islice(
28
- self._repository.iter_activities(dropna=True), 15
29
- ):
30
- time_series = self._repository.get_time_series(activity["id"])
31
- result["latest_activities"].append(
32
- {
33
- "line_geojson": make_geojson_from_time_series(time_series),
34
- "activity": activity,
35
- }
36
- )
37
- return result
38
-
39
-
40
- def distance_last_30_days_meta_plot(meta: pd.DataFrame, kind_scale: alt.Scale) -> str:
41
- before_30_days = pd.to_datetime(
42
- datetime.datetime.now() - datetime.timedelta(days=31)
43
- )
44
- return (
45
- alt.Chart(
46
- meta.loc[meta["start"] > before_30_days],
47
- width=700,
48
- height=200,
49
- title="Distance per day",
50
- )
51
- .mark_bar()
52
- .encode(
53
- alt.X("yearmonthdate(start)", title="Date"),
54
- alt.Y("sum(distance_km)", title="Distance / km"),
55
- alt.Color("kind", scale=kind_scale, title="Kind"),
56
- [
57
- alt.Tooltip("yearmonthdate(start)", title="Date"),
58
- alt.Tooltip("kind", title="Kind"),
59
- alt.Tooltip("sum(distance_km)", format=".1f", title="Distance / km"),
60
- ],
61
- )
62
- .to_json(format="vega")
63
- )
File without changes
@@ -1,62 +0,0 @@
1
- from flask import Blueprint
2
- from flask import redirect
3
- from flask import render_template
4
- from flask import Response
5
- from flask import url_for
6
-
7
- from geo_activity_playground.webui.authenticator import Authenticator
8
- from geo_activity_playground.webui.authenticator import needs_authentication
9
- from geo_activity_playground.webui.explorer.controller import ExplorerController
10
-
11
-
12
- def make_explorer_blueprint(
13
- explorer_controller: ExplorerController,
14
- authenticator: Authenticator,
15
- ) -> Blueprint:
16
- blueprint = Blueprint("explorer", __name__, template_folder="templates")
17
-
18
- @blueprint.route("/<zoom>")
19
- def map(zoom: str):
20
- return render_template(
21
- "explorer/index.html.j2", **explorer_controller.render(int(zoom))
22
- )
23
-
24
- @blueprint.route("/enable-zoom-level/<zoom>")
25
- @needs_authentication(authenticator)
26
- def enable_zoom_level(zoom: str):
27
- explorer_controller.enable_zoom_level(int(zoom))
28
- return redirect(url_for(".map", zoom=zoom))
29
-
30
- @blueprint.route("/<zoom>/<north>/<east>/<south>/<west>/explored.<suffix>")
31
- def download(zoom: str, north: str, east: str, south: str, west: str, suffix: str):
32
- mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
33
- return Response(
34
- explorer_controller.export_explored_tiles(
35
- int(zoom),
36
- float(north),
37
- float(east),
38
- float(south),
39
- float(west),
40
- suffix,
41
- ),
42
- mimetype=mimetypes[suffix],
43
- headers={"Content-disposition": "attachment"},
44
- )
45
-
46
- @blueprint.route("/<zoom>/<north>/<east>/<south>/<west>/missing.<suffix>")
47
- def missing(zoom: str, north: str, east: str, south: str, west: str, suffix: str):
48
- mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
49
- return Response(
50
- explorer_controller.export_missing_tiles(
51
- int(zoom),
52
- float(north),
53
- float(east),
54
- float(south),
55
- float(west),
56
- suffix,
57
- ),
58
- mimetype=mimetypes[suffix],
59
- headers={"Content-disposition": "attachment"},
60
- )
61
-
62
- return blueprint
File without changes
@@ -1,51 +0,0 @@
1
- import dateutil.parser
2
- from flask import Blueprint
3
- from flask import render_template
4
- from flask import request
5
- from flask import Response
6
-
7
- from geo_activity_playground.core.activities import ActivityRepository
8
- from geo_activity_playground.core.config import Config
9
- from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
10
- from geo_activity_playground.webui.heatmap.heatmap_controller import HeatmapController
11
- from geo_activity_playground.webui.search_util import search_query_from_form
12
- from geo_activity_playground.webui.search_util import SearchQueryHistory
13
-
14
-
15
- def make_heatmap_blueprint(
16
- repository: ActivityRepository,
17
- tile_visit_accessor: TileVisitAccessor,
18
- config: Config,
19
- search_query_history: SearchQueryHistory,
20
- ) -> Blueprint:
21
- heatmap_controller = HeatmapController(repository, tile_visit_accessor, config)
22
- blueprint = Blueprint("heatmap", __name__, template_folder="templates")
23
-
24
- @blueprint.route("/")
25
- def index():
26
- query = search_query_from_form(request.args)
27
- search_query_history.register_query(query)
28
- return render_template(
29
- "heatmap/index.html.j2", **heatmap_controller.render(query)
30
- )
31
-
32
- @blueprint.route("/tile/<int:z>/<int:x>/<int:y>.png")
33
- def tile(x: int, y: int, z: int):
34
- query = search_query_from_form(request.args)
35
- return Response(
36
- heatmap_controller.render_tile(x, y, z, query),
37
- mimetype="image/png",
38
- )
39
-
40
- @blueprint.route(
41
- "/download/<float:north>/<float:east>/<float:south>/<float:west>/heatmap.png"
42
- )
43
- def download(north: float, east: float, south: float, west: float):
44
- query = search_query_from_form(request.args)
45
- return Response(
46
- heatmap_controller.download_heatmap(north, east, south, west, query),
47
- mimetype="image/png",
48
- headers={"Content-disposition": 'attachment; filename="heatmap.png"'},
49
- )
50
-
51
- return blueprint
@@ -1,216 +0,0 @@
1
- import datetime
2
- import io
3
- import logging
4
- import pathlib
5
- from typing import Optional
6
-
7
- import matplotlib.pylab as pl
8
- import numpy as np
9
- from PIL import Image
10
- from PIL import ImageDraw
11
-
12
- from geo_activity_playground.core.activities import ActivityRepository
13
- from geo_activity_playground.core.config import Config
14
- from geo_activity_playground.core.meta_search import apply_search_query
15
- from geo_activity_playground.core.meta_search import SearchQuery
16
- from geo_activity_playground.core.raster_map import convert_to_grayscale
17
- from geo_activity_playground.core.raster_map import GeoBounds
18
- from geo_activity_playground.core.raster_map import get_sensible_zoom_level
19
- from geo_activity_playground.core.raster_map import get_tile
20
- from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
21
- from geo_activity_playground.core.raster_map import PixelBounds
22
- from geo_activity_playground.core.tasks import work_tracker
23
- from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
24
- from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
25
- from geo_activity_playground.webui.explorer.controller import (
26
- bounding_box_for_biggest_cluster,
27
- )
28
-
29
-
30
- logger = logging.getLogger(__name__)
31
-
32
-
33
- class HeatmapController:
34
- def __init__(
35
- self,
36
- repository: ActivityRepository,
37
- tile_visit_accessor: TileVisitAccessor,
38
- config: Config,
39
- ) -> None:
40
- self._repository = repository
41
- self._tile_visit_accessor = tile_visit_accessor
42
- self._config = config
43
-
44
- self.tile_histories = self._tile_visit_accessor.tile_state["tile_history"]
45
- self.tile_evolution_states = self._tile_visit_accessor.tile_state[
46
- "evolution_state"
47
- ]
48
- self.tile_visits = self._tile_visit_accessor.tile_state["tile_visits"]
49
- self.activities_per_tile = self._tile_visit_accessor.tile_state[
50
- "activities_per_tile"
51
- ]
52
-
53
- def render(self, query: SearchQuery) -> dict:
54
- zoom = 14
55
- tiles = self.tile_histories[zoom]
56
- medians = tiles.median(skipna=True)
57
- median_lat, median_lon = get_tile_upper_left_lat_lon(
58
- medians["tile_x"], medians["tile_y"], zoom
59
- )
60
- cluster_state = self.tile_evolution_states[zoom]
61
-
62
- values = {
63
- "center": {
64
- "latitude": median_lat,
65
- "longitude": median_lon,
66
- "bbox": (
67
- bounding_box_for_biggest_cluster(
68
- cluster_state.clusters.values(), zoom
69
- )
70
- if len(cluster_state.memberships) > 0
71
- else {}
72
- ),
73
- },
74
- "extra_args": query.to_url_str(),
75
- "query": query.to_jinja(),
76
- }
77
-
78
- return values
79
-
80
- def _get_counts(
81
- self,
82
- x: int,
83
- y: int,
84
- z: int,
85
- query: SearchQuery,
86
- ) -> np.ndarray:
87
- tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
88
- tile_counts = np.zeros(tile_pixels, dtype=np.int32)
89
- if not query.active:
90
- tile_count_cache_path = pathlib.Path(f"Cache/Heatmap/{z}/{x}/{y}.npy")
91
- if tile_count_cache_path.exists():
92
- try:
93
- tile_counts = np.load(tile_count_cache_path)
94
- except ValueError:
95
- logger.warning(
96
- f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
97
- )
98
- tile_count_cache_path.unlink()
99
- tile_counts = np.zeros(tile_pixels, dtype=np.int32)
100
- tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
101
- activity_ids = self.activities_per_tile[z].get((x, y), set())
102
-
103
- with work_tracker(
104
- tile_count_cache_path.with_suffix(".json")
105
- ) as parsed_activities:
106
- if parsed_activities - activity_ids:
107
- logger.warning(
108
- f"Resetting heatmap cache for {x=}/{y=}/{z=} because activities have been removed."
109
- )
110
- tile_counts = np.zeros(tile_pixels, dtype=np.int32)
111
- parsed_activities.clear()
112
- for activity_id in activity_ids:
113
- if activity_id in parsed_activities:
114
- continue
115
- parsed_activities.add(activity_id)
116
- time_series = self._repository.get_time_series(activity_id)
117
- for _, group in time_series.groupby("segment_id"):
118
- xy_pixels = (
119
- np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
120
- * OSM_TILE_SIZE
121
- )
122
- im = Image.new("L", tile_pixels)
123
- draw = ImageDraw.Draw(im)
124
- pixels = list(map(int, xy_pixels.flatten()))
125
- draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
126
- aim = np.array(im)
127
- tile_counts += aim
128
- tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
129
- np.save(tmp_path, tile_counts)
130
- tile_count_cache_path.unlink(missing_ok=True)
131
- tmp_path.rename(tile_count_cache_path)
132
- else:
133
- activities = apply_search_query(self._repository.meta, query)
134
- activity_ids = self.activities_per_tile[z].get((x, y), set())
135
- for activity_id in activity_ids:
136
- if activity_id not in activities["id"]:
137
- continue
138
- time_series = self._repository.get_time_series(activity_id)
139
- for _, group in time_series.groupby("segment_id"):
140
- xy_pixels = (
141
- np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
142
- * OSM_TILE_SIZE
143
- )
144
- im = Image.new("L", tile_pixels)
145
- draw = ImageDraw.Draw(im)
146
- pixels = list(map(int, xy_pixels.flatten()))
147
- draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
148
- aim = np.array(im)
149
- tile_counts += aim
150
- return tile_counts
151
-
152
- def _render_tile_image(
153
- self,
154
- x: int,
155
- y: int,
156
- z: int,
157
- query: SearchQuery,
158
- ) -> np.ndarray:
159
- tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
160
- tile_counts = np.zeros(tile_pixels)
161
- tile_counts += self._get_counts(x, y, z, query)
162
-
163
- tile_counts = np.sqrt(tile_counts) / 5
164
- tile_counts[tile_counts > 1.0] = 1.0
165
-
166
- cmap = pl.get_cmap(self._config.color_scheme_for_heatmap)
167
- data_color = cmap(tile_counts)
168
- data_color[data_color == cmap(0.0)] = 0.0 # remove background color
169
-
170
- map_tile = np.array(get_tile(z, x, y, self._config.map_tile_url)) / 255
171
- map_tile = convert_to_grayscale(map_tile)
172
- map_tile = 1.0 - map_tile # invert colors
173
- for c in range(3):
174
- map_tile[:, :, c] = (1.0 - data_color[:, :, c]) * map_tile[
175
- :, :, c
176
- ] + data_color[:, :, c]
177
- return map_tile
178
-
179
- def render_tile(self, x: int, y: int, z: int, query: SearchQuery) -> bytes:
180
- f = io.BytesIO()
181
- pl.imsave(
182
- f,
183
- self._render_tile_image(x, y, z, query),
184
- format="png",
185
- )
186
- return bytes(f.getbuffer())
187
-
188
- def download_heatmap(
189
- self, north: float, east: float, south: float, west: float, query: SearchQuery
190
- ) -> bytes:
191
- geo_bounds = GeoBounds(south, west, north, east)
192
- tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
193
- pixel_bounds = PixelBounds.from_tile_bounds(tile_bounds)
194
-
195
- background = np.zeros((*pixel_bounds.shape, 3))
196
- for x in range(tile_bounds.x1, tile_bounds.x2):
197
- for y in range(tile_bounds.y1, tile_bounds.y2):
198
- tile = (
199
- np.array(
200
- get_tile(tile_bounds.zoom, x, y, self._config.map_tile_url)
201
- )
202
- / 255
203
- )
204
-
205
- i = y - tile_bounds.y1
206
- j = x - tile_bounds.x1
207
-
208
- background[
209
- i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
210
- j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
211
- :,
212
- ] = self._render_tile_image(x, y, tile_bounds.zoom, query)
213
-
214
- f = io.BytesIO()
215
- pl.imsave(f, background, format="png")
216
- return bytes(f.getbuffer())