geo-activity-playground 1.2.0__tar.gz → 1.3.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 (180) hide show
  1. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/PKG-INFO +7 -3
  2. geo_activity_playground-1.3.0/geo_activity_playground/alembic/versions/85fe0348e8a2_add_time_series_uuid_field.py +28 -0
  3. geo_activity_playground-1.3.0/geo_activity_playground/alembic/versions/f2f50843be2d_make_all_fields_in_activity_nullable.py +34 -0
  4. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/coordinates.py +12 -1
  5. geo_activity_playground-1.3.0/geo_activity_playground/core/copernicus_dem.py +95 -0
  6. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/datamodel.py +43 -16
  7. geo_activity_playground-1.3.0/geo_activity_playground/core/enrichment.py +274 -0
  8. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/paths.py +8 -0
  9. geo_activity_playground-1.3.0/geo_activity_playground/core/test_pandas_timezone.py +36 -0
  10. geo_activity_playground-1.3.0/geo_activity_playground/core/test_time_zone_from_location.py +7 -0
  11. geo_activity_playground-1.3.0/geo_activity_playground/core/test_time_zone_import.py +93 -0
  12. geo_activity_playground-1.3.0/geo_activity_playground/core/test_timezone_sqlalchemy.py +44 -0
  13. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/tiles.py +4 -1
  14. geo_activity_playground-1.3.0/geo_activity_playground/core/time_conversion.py +42 -0
  15. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/explorer/tile_visits.py +7 -4
  16. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/activity_parsers.py +21 -22
  17. geo_activity_playground-1.3.0/geo_activity_playground/importers/directory.py +100 -0
  18. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/strava_api.py +53 -36
  19. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/strava_checkout.py +30 -56
  20. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/app.py +40 -2
  21. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/activity_blueprint.py +13 -11
  22. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/entry_views.py +1 -1
  23. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/explorer_blueprint.py +1 -7
  24. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/heatmap_blueprint.py +2 -2
  25. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/settings_blueprint.py +3 -14
  26. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/summary_blueprint.py +6 -6
  27. geo_activity_playground-1.3.0/geo_activity_playground/webui/blueprints/time_zone_fixer_blueprint.py +69 -0
  28. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/upload_blueprint.py +3 -16
  29. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/columns.py +9 -1
  30. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/activity/show.html.j2 +3 -1
  31. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +1 -1
  32. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/home.html.j2 +3 -2
  33. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/page.html.j2 +2 -0
  34. geo_activity_playground-1.3.0/geo_activity_playground/webui/templates/time_zone_fixer/index.html.j2 +31 -0
  35. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/pyproject.toml +7 -2
  36. geo_activity_playground-1.2.0/geo_activity_playground/core/enrichment.py +0 -212
  37. geo_activity_playground-1.2.0/geo_activity_playground/core/test_time_conversion.py +0 -37
  38. geo_activity_playground-1.2.0/geo_activity_playground/core/time_conversion.py +0 -14
  39. geo_activity_playground-1.2.0/geo_activity_playground/importers/directory.py +0 -146
  40. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/LICENSE +0 -0
  41. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/__init__.py +0 -0
  42. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/__main__.py +0 -0
  43. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/README +0 -0
  44. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/env.py +0 -0
  45. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/script.py.mako +0 -0
  46. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/0f02b92c4f94_add_tag_color.py +0 -0
  47. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py +0 -0
  48. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +0 -0
  49. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +0 -0
  50. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/93cc82ad1b60_add_parametricplotspec.py +0 -0
  51. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +0 -0
  52. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +0 -0
  53. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +0 -0
  54. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/dc8073871da7_add_plotspec_group_by.py +0 -0
  55. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +0 -0
  56. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/alembic/versions/script.py.mako +0 -0
  57. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/__init__.py +0 -0
  58. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/activities.py +0 -0
  59. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/config.py +0 -0
  60. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/export.py +0 -0
  61. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/heart_rate.py +0 -0
  62. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/meta_search.py +0 -0
  63. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/missing_values.py +0 -0
  64. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/parametric_plot.py +0 -0
  65. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/privacy_zones.py +0 -0
  66. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/raster_map.py +0 -0
  67. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/similarity.py +0 -0
  68. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/summary_stats.py +0 -0
  69. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/tasks.py +0 -0
  70. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/test_datamodel.py +0 -0
  71. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/test_meta_search.py +0 -0
  72. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/test_missing_values.py +0 -0
  73. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/test_summary_stats.py +0 -0
  74. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/core/test_tiles.py +0 -0
  75. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/explorer/__init__.py +0 -0
  76. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/explorer/grid_file.py +0 -0
  77. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/explorer/video.py +0 -0
  78. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/heatmap_video.py +0 -0
  79. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/__init__.py +0 -0
  80. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/csv_parser.py +0 -0
  81. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/test_csv_parser.py +0 -0
  82. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/test_directory.py +0 -0
  83. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/importers/test_strava_api.py +0 -0
  84. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/__init__.py +0 -0
  85. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/authenticator.py +0 -0
  86. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/__init__.py +0 -0
  87. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/auth_blueprint.py +0 -0
  88. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +0 -0
  89. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/calendar_blueprint.py +0 -0
  90. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/eddington_blueprints.py +0 -0
  91. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/equipment_blueprint.py +0 -0
  92. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/export_blueprint.py +0 -0
  93. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +0 -0
  94. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/photo_blueprint.py +0 -0
  95. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +0 -0
  96. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/search_blueprint.py +0 -0
  97. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/square_planner_blueprint.py +0 -0
  98. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/blueprints/tile_blueprint.py +0 -0
  99. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/flasher.py +0 -0
  100. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/plot_util.py +0 -0
  101. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/search_util.py +0 -0
  102. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/bootstrap/bootstrap-dark-mode.js +0 -0
  103. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/bootstrap/bootstrap.bundle.min.js +0 -0
  104. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/bootstrap/bootstrap.min.css +0 -0
  105. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/android-chrome-192x192.png +0 -0
  106. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/android-chrome-512x512.png +0 -0
  107. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/apple-touch-icon.png +0 -0
  108. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/browserconfig.xml +0 -0
  109. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/favicon-16x16.png +0 -0
  110. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/favicon-32x32.png +0 -0
  111. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/favicon-48x48.png +0 -0
  112. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/favicon.ico +0 -0
  113. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/favicon.svg +0 -0
  114. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/mstile-150x150.png +0 -0
  115. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/site.webmanifest +0 -0
  116. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/web-app-manifest-192x192.png +0 -0
  117. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/favicons/web-app-manifest-512x512.png +0 -0
  118. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/images/layers-2x.png +0 -0
  119. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/images/layers.png +0 -0
  120. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
  121. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/images/marker-icon.png +0 -0
  122. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
  123. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/Leaflet.fullscreen.min.js +0 -0
  124. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/MarkerCluster.Default.css +0 -0
  125. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/MarkerCluster.css +0 -0
  126. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/fullscreen.png +0 -0
  127. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/fullscreen@2x.png +0 -0
  128. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/leaflet.css +0 -0
  129. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/leaflet.fullscreen.css +0 -0
  130. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/leaflet.js +0 -0
  131. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/leaflet/leaflet.markercluster.js +0 -0
  132. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/server-side-explorer.js +0 -0
  133. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/table-sort.min.js +0 -0
  134. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/vega/vega-embed@6.js +0 -0
  135. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/vega/vega-lite@4.js +0 -0
  136. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/static/vega/vega@5.js +0 -0
  137. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/activity/day.html.j2 +0 -0
  138. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/activity/edit.html.j2 +0 -0
  139. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/activity/lines.html.j2 +0 -0
  140. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/activity/name.html.j2 +0 -0
  141. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/activity/trim.html.j2 +0 -0
  142. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/auth/index.html.j2 +0 -0
  143. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +0 -0
  144. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/calendar/index.html.j2 +0 -0
  145. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/calendar/month.html.j2 +0 -0
  146. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/eddington/distance.html.j2 +0 -0
  147. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +0 -0
  148. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +0 -0
  149. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/equipment/index.html.j2 +0 -0
  150. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/explorer/server-side.html.j2 +0 -0
  151. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/export/index.html.j2 +0 -0
  152. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/heatmap/index.html.j2 +0 -0
  153. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/photo/map.html.j2 +0 -0
  154. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/photo/new.html.j2 +0 -0
  155. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/plot-macros.html.j2 +0 -0
  156. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +0 -0
  157. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/plot_builder/import-spec.html.j2 +0 -0
  158. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/plot_builder/index.html.j2 +0 -0
  159. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/search/index.html.j2 +0 -0
  160. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/search_form.html.j2 +0 -0
  161. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/admin-password.html.j2 +0 -0
  162. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/color-schemes.html.j2 +0 -0
  163. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/heart-rate.html.j2 +0 -0
  164. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/index.html.j2 +0 -0
  165. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +0 -0
  166. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +0 -0
  167. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/metadata-extraction.html.j2 +0 -0
  168. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/privacy-zones.html.j2 +0 -0
  169. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/segmentation.html.j2 +0 -0
  170. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/sharepic.html.j2 +0 -0
  171. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/strava.html.j2 +0 -0
  172. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +0 -0
  173. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/tags-list.html.j2 +0 -0
  174. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/tags-new.html.j2 +0 -0
  175. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/settings/tile-source.html.j2 +0 -0
  176. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/square_planner/index.html.j2 +0 -0
  177. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/summary/index.html.j2 +0 -0
  178. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/summary/vega-chart.html.j2 +0 -0
  179. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/upload/index.html.j2 +0 -0
  180. {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.0}/geo_activity_playground/webui/templates/upload/reload.html.j2 +0 -0
@@ -1,14 +1,13 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: geo-activity-playground
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
4
  Summary: Analysis of geo data activities like rides, runs or hikes.
5
5
  License: MIT
6
6
  Author: Martin Ueding
7
7
  Author-email: mu@martin-ueding.de
8
- Requires-Python: >=3.10,<3.14
8
+ Requires-Python: >=3.11,<3.14
9
9
  Classifier: License :: OSI Approved :: MIT License
10
10
  Classifier: Programming Language :: Python :: 3
11
- Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
14
13
  Classifier: Programming Language :: Python :: 3.13
@@ -16,6 +15,7 @@ Requires-Dist: Pillow (>=11.0.0,<12.0.0)
16
15
  Requires-Dist: alembic (>=1.15.2,<2.0.0)
17
16
  Requires-Dist: altair (>=5.5.0,<6.0.0)
18
17
  Requires-Dist: appdirs (>=1.4.4,<2.0.0)
18
+ Requires-Dist: boto3 (>=1.38.45,<2.0.0)
19
19
  Requires-Dist: charset-normalizer (>=3.3.2,<4.0.0)
20
20
  Requires-Dist: coloredlogs (>=15.0.1,<16.0.0)
21
21
  Requires-Dist: exifread (>=3.2.0,<4.0.0)
@@ -24,15 +24,19 @@ Requires-Dist: flask (>=3.0.0,<4.0.0)
24
24
  Requires-Dist: flask-alembic (>=3.1.1,<4.0.0)
25
25
  Requires-Dist: flask-sqlalchemy (>=3.1.1,<4.0.0)
26
26
  Requires-Dist: geojson (>=3.0.1,<4.0.0)
27
+ Requires-Dist: geotiff (>=0.2.10,<0.3.0)
27
28
  Requires-Dist: gpxpy (>=1.5.0,<2.0.0)
29
+ Requires-Dist: imagecodecs (>=2025.3.30,<2026.0.0)
28
30
  Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
29
31
  Requires-Dist: matplotlib (>=3.10.1,<4.0.0)
32
+ Requires-Dist: numcodecs (<0.15.0)
30
33
  Requires-Dist: numpy (>=2.2.3,<3.0.0)
31
34
  Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
32
35
  Requires-Dist: pandas (>=2.2.3,<3.0.0)
33
36
  Requires-Dist: pyarrow (>=19.0.1,<20.0.0)
34
37
  Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
35
38
  Requires-Dist: requests (>=2.28.1,<3.0.0)
39
+ Requires-Dist: scipy (>=1.16.0,<2.0.0)
36
40
  Requires-Dist: shapely (>=2.0.5,<3.0.0)
37
41
  Requires-Dist: sqlalchemy (>=2.0.40,<3.0.0)
38
42
  Requires-Dist: stravalib (>=2.0,<3.0)
@@ -0,0 +1,28 @@
1
+ from typing import Sequence
2
+ from typing import Union
3
+
4
+ import sqlalchemy as sa
5
+ from alembic import op
6
+
7
+
8
+ # revision identifiers, used by Alembic.
9
+ revision: str = "85fe0348e8a2"
10
+ down_revision: Union[str, None] = "f2f50843be2d"
11
+ branch_labels: Union[str, Sequence[str], None] = None
12
+ depends_on: Union[str, Sequence[str], None] = None
13
+
14
+
15
+ def upgrade() -> None:
16
+ # ### commands auto generated by Alembic - please adjust! ###
17
+ with op.batch_alter_table("activities", schema=None) as batch_op:
18
+ batch_op.add_column(sa.Column("time_series_uuid", sa.String(), nullable=True))
19
+
20
+ # ### end Alembic commands ###
21
+
22
+
23
+ def downgrade() -> None:
24
+ # ### commands auto generated by Alembic - please adjust! ###
25
+ with op.batch_alter_table("activities", schema=None) as batch_op:
26
+ batch_op.drop_column("time_series_uuid")
27
+
28
+ # ### end Alembic commands ###
@@ -0,0 +1,34 @@
1
+ from typing import Sequence
2
+ from typing import Union
3
+
4
+ import sqlalchemy as sa
5
+ from alembic import op
6
+
7
+
8
+ # revision identifiers, used by Alembic.
9
+ revision: str = "f2f50843be2d"
10
+ down_revision: Union[str, None] = "dc8073871da7"
11
+ branch_labels: Union[str, Sequence[str], None] = None
12
+ depends_on: Union[str, Sequence[str], None] = None
13
+
14
+
15
+ def upgrade() -> None:
16
+ # ### commands auto generated by Alembic - please adjust! ###
17
+ with op.batch_alter_table("activities", schema=None) as batch_op:
18
+ batch_op.add_column(sa.Column("iana_timezone", sa.String(), nullable=True))
19
+ batch_op.add_column(sa.Column("start_country", sa.String(), nullable=True))
20
+ batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=True)
21
+ batch_op.alter_column("distance_km", existing_type=sa.FLOAT(), nullable=True)
22
+
23
+ # ### end Alembic commands ###
24
+
25
+
26
+ def downgrade() -> None:
27
+ # ### commands auto generated by Alembic - please adjust! ###
28
+ with op.batch_alter_table("activities", schema=None) as batch_op:
29
+ batch_op.alter_column("distance_km", existing_type=sa.FLOAT(), nullable=False)
30
+ batch_op.alter_column("name", existing_type=sa.VARCHAR(), nullable=False)
31
+ batch_op.drop_column("start_country")
32
+ batch_op.drop_column("iana_timezone")
33
+
34
+ # ### end Alembic commands ###
@@ -1,4 +1,7 @@
1
+ import typing
2
+
1
3
  import numpy as np
4
+ import pandas as pd
2
5
 
3
6
 
4
7
  class Bounds:
@@ -15,7 +18,15 @@ class Bounds:
15
18
  return (self.x_min < x < self.x_max) and (self.y_min < y < self.y_max)
16
19
 
17
20
 
18
- def get_distance(lat_1: float, lon_1: float, lat_2: float, lon_2: float) -> float:
21
+ FloatOrSeries = typing.TypeVar("FloatOrSeries", float, np.ndarray, pd.Series)
22
+
23
+
24
+ def get_distance(
25
+ lat_1: FloatOrSeries,
26
+ lon_1: FloatOrSeries,
27
+ lat_2: FloatOrSeries,
28
+ lon_2: FloatOrSeries,
29
+ ) -> FloatOrSeries:
19
30
  """
20
31
  https://en.wikipedia.org/wiki/Haversine_formula
21
32
  """
@@ -0,0 +1,95 @@
1
+ import functools
2
+ import math
3
+ import pathlib
4
+ from typing import Optional
5
+
6
+ import boto3
7
+ import botocore.config
8
+ import botocore.exceptions
9
+ import geotiff
10
+ import numpy as np
11
+ from scipy.interpolate import RegularGridInterpolator
12
+
13
+ from .paths import USER_CACHE_DIR
14
+
15
+
16
+ def s3_path(lat: int, lon: int) -> pathlib.Path:
17
+ lat_str = f"N{(lat):02d}" if lat >= 0 else f"S{(-lat):02d}"
18
+ lon_str = f"E{(lon):03d}" if lon >= 0 else f"W{(-lon):03d}"
19
+ result = (
20
+ USER_CACHE_DIR
21
+ / "Copernicus DEM"
22
+ / f"Copernicus_DSM_COG_30_{lat_str}_00_{lon_str}_00_DEM.tif"
23
+ )
24
+
25
+ result.parent.mkdir(exist_ok=True)
26
+ return result
27
+
28
+
29
+ def ensure_copernicus_file(p: pathlib.Path) -> None:
30
+ if p.exists():
31
+ return
32
+ s3 = boto3.client(
33
+ "s3", config=botocore.config.Config(signature_version=botocore.UNSIGNED)
34
+ )
35
+ try:
36
+ s3.download_file("copernicus-dem-90m", f"{p.stem}/{p.name}", p)
37
+ except botocore.exceptions.ClientError as e:
38
+ pass
39
+
40
+
41
+ @functools.lru_cache(9)
42
+ def get_elevation_arrays(p: pathlib.Path) -> Optional[np.ndarray]:
43
+ ensure_copernicus_file(p)
44
+ if not p.exists():
45
+ return None
46
+ gt = geotiff.GeoTiff(p)
47
+ a = np.array(gt.read())
48
+ lon_array, lat_array = gt.get_coord_arrays()
49
+ return np.stack([a, lat_array, lon_array], axis=0)
50
+
51
+
52
+ @functools.lru_cache(1)
53
+ def get_interpolator(lat: int, lon: int) -> Optional[RegularGridInterpolator]:
54
+ arrays = get_elevation_arrays(s3_path(lat, lon))
55
+ # If we don't have data for the current center, we cannot do anything.
56
+ if arrays is None:
57
+ return None
58
+
59
+ # # Take a look at the neighbors. If all 8 neighbor grid cells are present, we can
60
+ # neighbor_shapes = [
61
+ # get_elevation_arrays(s3_path(lat + lat_offset, lon + lon_offset)).shape
62
+ # for lon_offset in [-1, 0, 1]
63
+ # for lat_offset in [-1, 0, 1]
64
+ # if get_elevation_arrays(s3_path(lat + lat_offset, lon + lon_offset)) is not None
65
+ # ]
66
+ # if len(neighbor_shapes) == 9 and len(set(neighbor_shapes)) == 1:
67
+ # arrays = np.concatenate(
68
+ # [
69
+ # np.concatenate(
70
+ # [
71
+ # get_elevation_arrays(
72
+ # s3_path(lat + lat_offset, lon + lon_offset)
73
+ # )
74
+ # for lon_offset in [-1, 0, 1]
75
+ # ],
76
+ # axis=2,
77
+ # )
78
+ # for lat_offset in [1, 0, -1]
79
+ # ],
80
+ # axis=1,
81
+ # )
82
+ lat_labels = arrays[1, :, 0]
83
+ lon_labels = arrays[2, 0, :]
84
+
85
+ return RegularGridInterpolator(
86
+ (lat_labels, lon_labels), arrays[0], bounds_error=False, fill_value=None
87
+ )
88
+
89
+
90
+ def get_elevation(lat: float, lon: float) -> float:
91
+ interpolator = get_interpolator(math.floor(lat), math.floor(lon))
92
+ if interpolator is not None:
93
+ return float(interpolator((lat, lon)))
94
+ else:
95
+ return 0.0
@@ -1,7 +1,11 @@
1
1
  import datetime
2
2
  import json
3
3
  import logging
4
+ import os
4
5
  import pathlib
6
+ import shutil
7
+ import uuid
8
+ import zoneinfo
5
9
  from typing import Any
6
10
  from typing import Optional
7
11
  from typing import TypedDict
@@ -49,6 +53,7 @@ class ActivityMeta(TypedDict):
49
53
  calories: float
50
54
  commute: bool
51
55
  consider_for_achievements: bool
56
+ copernicus_elevation_gain: float
52
57
  distance_km: float
53
58
  elapsed_time: datetime.timedelta
54
59
  elevation_gain: float
@@ -85,27 +90,36 @@ class Activity(DB.Model):
85
90
 
86
91
  # Housekeeping data:
87
92
  id: Mapped[int] = mapped_column(primary_key=True)
88
- name: Mapped[str] = mapped_column(sa.String, nullable=False)
89
- distance_km: Mapped[float] = mapped_column(sa.Float, nullable=False)
93
+ name: Mapped[Optional[str]] = mapped_column(sa.String, nullable=True)
94
+ distance_km: Mapped[Optional[float]] = mapped_column(sa.Float, nullable=True)
95
+ time_series_uuid: Mapped[Optional[str]] = mapped_column(sa.String, nullable=True)
90
96
 
91
97
  # Where it comes from:
92
- path: Mapped[str] = mapped_column(sa.String, nullable=True)
93
- upstream_id: Mapped[str] = mapped_column(sa.String, nullable=True)
98
+ path: Mapped[Optional[str]] = mapped_column(sa.String, nullable=True)
99
+ upstream_id: Mapped[Optional[str]] = mapped_column(sa.String, nullable=True)
94
100
 
95
101
  # Crop data:
96
102
  index_begin: Mapped[int] = mapped_column(sa.Integer, nullable=True)
97
103
  index_end: Mapped[int] = mapped_column(sa.Integer, nullable=True)
98
104
 
99
105
  # Temporal data:
100
- start: Mapped[datetime.datetime] = mapped_column(sa.DateTime, nullable=True)
101
- elapsed_time: Mapped[datetime.timedelta] = mapped_column(sa.Interval, nullable=True)
102
- moving_time: Mapped[datetime.timedelta] = mapped_column(sa.Interval, nullable=True)
106
+ start: Mapped[Optional[datetime.datetime]] = mapped_column(
107
+ sa.DateTime, nullable=True
108
+ )
109
+ iana_timezone: Mapped[Optional[str]] = mapped_column(sa.String, nullable=True)
110
+ elapsed_time: Mapped[Optional[datetime.timedelta]] = mapped_column(
111
+ sa.Interval, nullable=True
112
+ )
113
+ moving_time: Mapped[Optional[datetime.timedelta]] = mapped_column(
114
+ sa.Interval, nullable=True
115
+ )
103
116
 
104
117
  # Geographic data:
105
118
  start_latitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
106
119
  start_longitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
107
120
  end_latitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
108
121
  end_longitude: Mapped[float] = mapped_column(sa.Float, nullable=True)
122
+ start_country: Mapped[Optional[str]] = mapped_column(sa.String, nullable=True)
109
123
 
110
124
  # Elevation data:
111
125
  elevation_gain: Mapped[float] = mapped_column(sa.Float, nullable=True)
@@ -143,30 +157,36 @@ class Activity(DB.Model):
143
157
 
144
158
  @property
145
159
  def average_speed_moving_kmh(self) -> Optional[float]:
146
- if self.moving_time:
160
+ if self.distance_km and self.moving_time:
147
161
  return self.distance_km / (self.moving_time.total_seconds() / 3_600)
148
162
  else:
149
163
  return None
150
164
 
151
165
  @property
152
166
  def average_speed_elapsed_kmh(self) -> Optional[float]:
153
- if self.elapsed_time:
167
+ if self.distance_km and self.elapsed_time:
154
168
  return self.distance_km / (self.elapsed_time.total_seconds() / 3_600)
155
169
  else:
156
170
  return None
157
171
 
172
+ @property
173
+ def time_series_path(self) -> pathlib.Path:
174
+ return TIME_SERIES_DIR() / f"{self.time_series_uuid}.parquet"
175
+
158
176
  @property
159
177
  def raw_time_series(self) -> pd.DataFrame:
160
- path = TIME_SERIES_DIR() / f"{self.id}.parquet"
161
178
  try:
162
- time_series = pd.read_parquet(path)
179
+ time_series = pd.read_parquet(self.time_series_path)
163
180
  if "altitude" in time_series.columns:
164
181
  time_series.rename(columns={"altitude": "elevation"}, inplace=True)
165
182
  return time_series
166
183
  except OSError as e:
167
- logger.error(f"Error while reading {path}.")
184
+ logger.error(f"Error while reading {self.time_series_path}.")
168
185
  raise
169
186
 
187
+ def replace_time_series(self, time_series: pd.DataFrame) -> None:
188
+ time_series.to_parquet(self.time_series_path)
189
+
170
190
  @property
171
191
  def time_series(self) -> pd.DataFrame:
172
192
  if self.index_begin or self.index_end:
@@ -201,6 +221,15 @@ class Activity(DB.Model):
201
221
  ]:
202
222
  path.unlink(missing_ok=True)
203
223
 
224
+ @property
225
+ def start_local_tz(self) -> Optional[datetime.datetime]:
226
+ if self.start and self.iana_timezone:
227
+ return self.start.replace(
228
+ microsecond=0, tzinfo=zoneinfo.ZoneInfo("UTC")
229
+ ).astimezone(zoneinfo.ZoneInfo(self.iana_timezone))
230
+ else:
231
+ return self.start
232
+
204
233
 
205
234
  class Tag(DB.Model):
206
235
  __tablename__ = "tags"
@@ -329,7 +358,6 @@ def get_or_make_equipment(name: str, config: Config) -> Equipment:
329
358
  equipment = Equipment(
330
359
  name=name, offset_km=config.equipment_offsets.get(name, 0)
331
360
  )
332
- DB.session.add(equipment)
333
361
  return equipment
334
362
 
335
363
 
@@ -356,7 +384,7 @@ class Kind(DB.Model):
356
384
  __table_args__ = (sa.UniqueConstraint("name", name="kinds_name"),)
357
385
 
358
386
 
359
- def get_or_make_kind(name: str, config: Config) -> Kind:
387
+ def get_or_make_kind(name: str) -> Kind:
360
388
  kinds = DB.session.scalars(sqlalchemy.select(Kind).where(Kind.name == name)).all()
361
389
  if kinds:
362
390
  assert len(kinds) == 1, f"There must be only one kind with name '{name}'."
@@ -364,9 +392,8 @@ def get_or_make_kind(name: str, config: Config) -> Kind:
364
392
  else:
365
393
  kind = Kind(
366
394
  name=name,
367
- consider_for_achievements=name in config.kinds_without_achievements,
395
+ consider_for_achievements=True,
368
396
  )
369
- DB.session.add(kind)
370
397
  return kind
371
398
 
372
399
 
@@ -0,0 +1,274 @@
1
+ import datetime
2
+ import logging
3
+ import zoneinfo
4
+ from typing import Callable
5
+
6
+ import numpy as np
7
+ import pandas as pd
8
+
9
+ from .config import Config
10
+ from .coordinates import get_distance
11
+ from .copernicus_dem import get_elevation
12
+ from .datamodel import Activity
13
+ from .datamodel import DB
14
+ from .missing_values import some
15
+ from .tiles import compute_tile_float
16
+ from .time_conversion import get_country_timezone
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def enrichment_set_timezone(
22
+ activity: Activity, time_series: pd.DataFrame, config: Config
23
+ ) -> bool:
24
+ assert (
25
+ len(time_series) > 0
26
+ ), f"You cannot import an activity without points. {activity=}"
27
+ latitude, longitude = time_series[["latitude", "longitude"]].iloc[0].to_list()
28
+ if activity.iana_timezone is None or activity.start_country is None:
29
+ country, tz_str = get_country_timezone(latitude, longitude)
30
+ activity.iana_timezone = tz_str
31
+ activity.start_country = country
32
+ return True
33
+ else:
34
+ return False
35
+
36
+
37
+ def enrichment_normalize_time(
38
+ activity: Activity, time_series: pd.DataFrame, config: Config
39
+ ) -> bool:
40
+ # Routes (as opposed to tracks) don't have time information. We cannot do anything with time here.
41
+ if (
42
+ "time" in time_series.columns
43
+ and pd.isna(time_series["time"]).all()
44
+ and not pd.api.types.is_datetime64_any_dtype(time_series["time"].dtype)
45
+ ):
46
+ time_series["time"] = pd.NaT
47
+ return True
48
+
49
+ changed = False
50
+ tz_utc = zoneinfo.ZoneInfo("UTC")
51
+ # If the time is naive, assume that it is UTC.
52
+ if time_series["time"].dt.tz is None:
53
+ time_series["time"] = time_series["time"].dt.tz_localize(tz_utc)
54
+ changed = True
55
+
56
+ if time_series["time"].dt.tz.utcoffset(None) != tz_utc.utcoffset(None):
57
+ time_series["time"] = time_series["time"].dt.tz_convert(tz_utc)
58
+ changed = True
59
+
60
+ if not pd.api.types.is_dtype_equal(
61
+ time_series["time"].dtype, "datetime64[ns, UTC]"
62
+ ):
63
+ time_series["time"] = time_series["time"].dt.tz_convert(tz_utc)
64
+ changed = True
65
+
66
+ assert pd.api.types.is_dtype_equal(
67
+ time_series["time"].dtype, "datetime64[ns, UTC]"
68
+ ), (
69
+ time_series["time"].dtype,
70
+ time_series["time"].iloc[0],
71
+ )
72
+
73
+ new_start = some(time_series["time"].iloc[0])
74
+ if new_start != activity.start:
75
+ activity.start = new_start
76
+ changed = True
77
+
78
+ new_elapsed_time = some(time_series["time"].iloc[-1] - time_series["time"].iloc[0])
79
+ if new_elapsed_time != activity.elapsed_time:
80
+ activity.elapsed_time = new_elapsed_time
81
+ changed = True
82
+
83
+ return changed
84
+
85
+
86
+ def enrichment_rename_altitude(
87
+ activity: Activity, time_series: pd.DataFrame, config: Config
88
+ ) -> bool:
89
+ if "altitude" in time_series.columns:
90
+ time_series.rename(columns={"altitude": "elevation"}, inplace=True)
91
+ return True
92
+ else:
93
+ return False
94
+
95
+
96
+ def enrichment_compute_tile_xy(
97
+ activity: Activity, time_series: pd.DataFrame, config: Config
98
+ ) -> bool:
99
+ if "x" not in time_series.columns:
100
+ x, y = compute_tile_float(time_series["latitude"], time_series["longitude"], 0)
101
+ time_series["x"] = x
102
+ time_series["y"] = y
103
+ return True
104
+ else:
105
+ return False
106
+
107
+
108
+ def enrichment_copernicus_elevation(
109
+ activity: Activity, time_series: pd.DataFrame, config: Config
110
+ ) -> bool:
111
+ if "copernicus_elevation" not in time_series.columns:
112
+ time_series["copernicus_elevation"] = [
113
+ get_elevation(lat, lon)
114
+ for lat, lon in zip(time_series["latitude"], time_series["longitude"])
115
+ ]
116
+ return True
117
+ else:
118
+ return False
119
+
120
+
121
+ def enrichment_elevation_gain(
122
+ activity: Activity, time_series: pd.DataFrame, config: Config
123
+ ) -> bool:
124
+ if (
125
+ "elevation" in time_series.columns
126
+ or "copernicus_elevation" in time_series.columns
127
+ ) and "elevation_gain_cum" not in time_series.columns:
128
+ elevation = (
129
+ time_series["elevation"]
130
+ if "elevation" in time_series.columns
131
+ else time_series["copernicus_elevation"]
132
+ )
133
+ elevation_diff = elevation.diff()
134
+ elevation_diff = elevation_diff.ewm(span=5, min_periods=5).mean()
135
+ elevation_diff.loc[elevation_diff.abs() > 30] = 0
136
+ elevation_diff.loc[elevation_diff < 0] = 0
137
+ time_series["elevation_gain_cum"] = elevation_diff.cumsum().fillna(0)
138
+
139
+ activity.elevation_gain = (
140
+ time_series["elevation_gain_cum"].iloc[-1]
141
+ - time_series["elevation_gain_cum"].iloc[0]
142
+ )
143
+ return True
144
+ else:
145
+ return False
146
+
147
+
148
+ def enrichment_add_calories(
149
+ activity: Activity, time_series: pd.DataFrame, config: Config
150
+ ) -> bool:
151
+ if activity.calories is None and "calories" in time_series.columns:
152
+ activity.calories = (
153
+ time_series["calories"].iloc[-1] - time_series["calories"].iloc[0]
154
+ )
155
+ return True
156
+ else:
157
+ return False
158
+
159
+
160
+ def enrichment_distance(
161
+ activity: Activity, time_series: pd.DataFrame, config: Config
162
+ ) -> bool:
163
+ changed = False
164
+
165
+ distances = get_distance(
166
+ time_series["latitude"].shift(1),
167
+ time_series["longitude"].shift(1),
168
+ time_series["latitude"],
169
+ time_series["longitude"],
170
+ ).fillna(0.0)
171
+
172
+ if config.time_diff_threshold_seconds:
173
+ time_diff = (
174
+ time_series["time"] - time_series["time"].shift(1)
175
+ ).dt.total_seconds()
176
+ jump_indices = time_diff >= config.time_diff_threshold_seconds
177
+ distances.loc[jump_indices] = 0.0
178
+
179
+ if "distance_km" not in time_series.columns:
180
+ time_series["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
181
+ changed = True
182
+
183
+ if "speed" not in time_series.columns:
184
+ time_series["speed"] = (
185
+ time_series["distance_km"].diff()
186
+ / (time_series["time"].diff().dt.total_seconds() + 1e-3)
187
+ * 3600
188
+ )
189
+ changed = True
190
+
191
+ potential_jumps = (time_series["speed"] > 40) & (time_series["speed"].diff() > 10)
192
+ if np.any(potential_jumps):
193
+ time_series.replace(time_series.loc[~potential_jumps])
194
+ changed = True
195
+
196
+ if "segment_id" not in time_series.columns:
197
+ if config.time_diff_threshold_seconds:
198
+ time_series["segment_id"] = np.cumsum(jump_indices)
199
+ else:
200
+ time_series["segment_id"] = 0
201
+ changed = True
202
+
203
+ new_distance_km = (
204
+ time_series["distance_km"].iloc[-1] - time_series["distance_km"].iloc[0]
205
+ )
206
+ if new_distance_km != activity.distance_km:
207
+ activity.distance_km = new_distance_km
208
+ changed = True
209
+
210
+ return changed
211
+
212
+
213
+ def enrichment_moving_time(
214
+ activity: Activity, time_series: pd.DataFrame, config: Config
215
+ ) -> bool:
216
+ def moving_time(group) -> datetime.timedelta:
217
+ selection = group["speed"] > 1.0
218
+ time_diff = group["time"].diff().loc[selection]
219
+ return time_diff.sum()
220
+
221
+ new_moving_time = (
222
+ time_series.groupby("segment_id").apply(moving_time, include_groups=False).sum()
223
+ )
224
+ if new_moving_time != activity.moving_time:
225
+ activity.moving_time = new_moving_time
226
+ return True
227
+ else:
228
+ return False
229
+
230
+
231
+ def enrichment_copy_latlon(
232
+ activity: Activity, time_series: pd.DataFrame, config: Config
233
+ ) -> bool:
234
+ if activity.start_latitude is None:
235
+ activity.start_latitude = time_series["latitude"].iloc[0]
236
+ activity.end_latitude = time_series["latitude"].iloc[-1]
237
+ activity.start_longitude = time_series["longitude"].iloc[0]
238
+ activity.end_longitude = time_series["longitude"].iloc[-1]
239
+ return True
240
+ else:
241
+ return False
242
+
243
+
244
+ enrichments: list[Callable[[Activity, pd.DataFrame, Config], bool]] = [
245
+ enrichment_set_timezone,
246
+ enrichment_normalize_time,
247
+ enrichment_rename_altitude,
248
+ enrichment_compute_tile_xy,
249
+ enrichment_copernicus_elevation,
250
+ enrichment_elevation_gain,
251
+ enrichment_add_calories,
252
+ enrichment_distance,
253
+ enrichment_moving_time,
254
+ enrichment_copy_latlon,
255
+ ]
256
+
257
+
258
+ def apply_enrichments(
259
+ activity: Activity, time_series: pd.DataFrame, config: Config
260
+ ) -> bool:
261
+ was_changed = False
262
+ for enrichment in enrichments:
263
+ was_changed |= enrichment(activity, time_series, config)
264
+ return was_changed
265
+
266
+
267
+ def update_and_commit(
268
+ activity: Activity, time_series: pd.DataFrame, config: Config
269
+ ) -> None:
270
+ changed = apply_enrichments(activity, time_series, config)
271
+ if changed:
272
+ activity.replace_time_series(time_series)
273
+ DB.session.add(activity)
274
+ DB.session.commit()
@@ -3,10 +3,18 @@ import functools
3
3
  import pathlib
4
4
  import typing
5
5
 
6
+ import appdirs
7
+
6
8
  """
7
9
  Paths within the playground and cache.
8
10
  """
9
11
 
12
+ APPDIRS = appdirs.AppDirs(appname="Geo Activity Playground", appauthor="Martin Ueding")
13
+
14
+ USER_CACHE_DIR = pathlib.Path(APPDIRS.user_cache_dir)
15
+ USER_CONFIG_DIR = pathlib.Path(APPDIRS.user_config_dir)
16
+ USER_DATA_DIR = pathlib.Path(APPDIRS.user_data_dir)
17
+
10
18
 
11
19
  def dir_wrapper(path: pathlib.Path) -> typing.Callable[[], pathlib.Path]:
12
20
  def wrapper() -> pathlib.Path: