geo-activity-playground 1.2.0__tar.gz → 1.3.1__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.
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/PKG-INFO +8 -3
- geo_activity_playground-1.3.1/geo_activity_playground/alembic/versions/85fe0348e8a2_add_time_series_uuid_field.py +28 -0
- geo_activity_playground-1.3.1/geo_activity_playground/alembic/versions/f2f50843be2d_make_all_fields_in_activity_nullable.py +34 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/coordinates.py +12 -1
- geo_activity_playground-1.3.1/geo_activity_playground/core/copernicus_dem.py +95 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/datamodel.py +43 -16
- geo_activity_playground-1.3.1/geo_activity_playground/core/enrichment.py +277 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/paths.py +8 -0
- geo_activity_playground-1.3.1/geo_activity_playground/core/test_pandas_timezone.py +36 -0
- geo_activity_playground-1.3.1/geo_activity_playground/core/test_time_zone_from_location.py +7 -0
- geo_activity_playground-1.3.1/geo_activity_playground/core/test_time_zone_import.py +93 -0
- geo_activity_playground-1.3.1/geo_activity_playground/core/test_timezone_sqlalchemy.py +44 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/tiles.py +4 -1
- geo_activity_playground-1.3.1/geo_activity_playground/core/time_conversion.py +42 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/explorer/tile_visits.py +7 -4
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/activity_parsers.py +31 -23
- geo_activity_playground-1.3.1/geo_activity_playground/importers/directory.py +107 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/strava_api.py +55 -36
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/strava_checkout.py +32 -57
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/app.py +46 -2
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/activity_blueprint.py +13 -11
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/entry_views.py +1 -1
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/explorer_blueprint.py +1 -7
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/heatmap_blueprint.py +2 -2
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/settings_blueprint.py +3 -14
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/summary_blueprint.py +6 -6
- geo_activity_playground-1.3.1/geo_activity_playground/webui/blueprints/time_zone_fixer_blueprint.py +69 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/upload_blueprint.py +3 -16
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/columns.py +9 -1
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/activity/show.html.j2 +5 -1
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/hall_of_fame/index.html.j2 +1 -1
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/home.html.j2 +3 -2
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/page.html.j2 +2 -0
- geo_activity_playground-1.3.1/geo_activity_playground/webui/templates/time_zone_fixer/index.html.j2 +31 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/pyproject.toml +8 -2
- geo_activity_playground-1.2.0/geo_activity_playground/core/enrichment.py +0 -212
- geo_activity_playground-1.2.0/geo_activity_playground/core/test_time_conversion.py +0 -37
- geo_activity_playground-1.2.0/geo_activity_playground/core/time_conversion.py +0 -14
- geo_activity_playground-1.2.0/geo_activity_playground/importers/directory.py +0 -146
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/LICENSE +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/__init__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/__main__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/README +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/env.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/script.py.mako +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/0f02b92c4f94_add_tag_color.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/38882503dc7c_add_tags_to_activities.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/451e7836b53d_add_square_planner_bookmark.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/63d3b7f6f93c_initial_version.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/93cc82ad1b60_add_parametricplotspec.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/ab83b9d23127_add_upstream_id.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/b03491c593f6_add_crop_indices.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/da2cba03b71d_add_photos.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/dc8073871da7_add_plotspec_group_by.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/e02e27876deb_add_square_planner_bookmark_name.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/alembic/versions/script.py.mako +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/__init__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/activities.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/config.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/export.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/heart_rate.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/meta_search.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/missing_values.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/parametric_plot.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/privacy_zones.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/raster_map.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/similarity.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/summary_stats.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/tasks.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/test_datamodel.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/test_meta_search.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/test_missing_values.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/test_summary_stats.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/core/test_tiles.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/explorer/__init__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/explorer/grid_file.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/explorer/video.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/heatmap_video.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/__init__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/csv_parser.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/test_csv_parser.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/test_directory.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/importers/test_strava_api.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/__init__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/authenticator.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/__init__.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/auth_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/bubble_chart_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/calendar_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/eddington_blueprints.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/equipment_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/export_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/hall_of_fame_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/photo_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/plot_builder_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/search_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/square_planner_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/blueprints/tile_blueprint.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/flasher.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/plot_util.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/search_util.py +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/bootstrap/bootstrap-dark-mode.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/bootstrap/bootstrap.bundle.min.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/bootstrap/bootstrap.min.css +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/android-chrome-192x192.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/android-chrome-512x512.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/apple-touch-icon.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/browserconfig.xml +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/favicon-16x16.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/favicon-32x32.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/favicon-48x48.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/favicon.ico +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/favicon.svg +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/mstile-150x150.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/site.webmanifest +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/web-app-manifest-192x192.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/favicons/web-app-manifest-512x512.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/images/layers-2x.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/images/layers.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/images/marker-icon-2x.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/images/marker-icon.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/images/marker-shadow.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/Leaflet.fullscreen.min.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/MarkerCluster.Default.css +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/MarkerCluster.css +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/fullscreen.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/fullscreen@2x.png +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/leaflet.css +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/leaflet.fullscreen.css +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/leaflet.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/leaflet/leaflet.markercluster.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/server-side-explorer.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/table-sort.min.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/vega/vega-embed@6.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/vega/vega-lite@4.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/static/vega/vega@5.js +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/activity/day.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/activity/edit.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/activity/lines.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/activity/name.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/activity/trim.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/auth/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/bubble_chart/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/calendar/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/calendar/month.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/eddington/distance.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/eddington/elevation_gain.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/elevation_eddington/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/equipment/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/explorer/server-side.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/export/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/heatmap/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/photo/map.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/photo/new.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/plot-macros.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/plot_builder/edit.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/plot_builder/import-spec.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/plot_builder/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/search/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/search_form.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/admin-password.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/color-schemes.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/heart-rate.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/manage-equipments.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/manage-kinds.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/metadata-extraction.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/privacy-zones.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/segmentation.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/sharepic.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/strava.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/tags-edit.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/tags-list.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/tags-new.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/settings/tile-source.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/square_planner/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/summary/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/summary/vega-chart.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/geo_activity_playground/webui/templates/upload/index.html.j2 +0 -0
- {geo_activity_playground-1.2.0 → geo_activity_playground-1.3.1}/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.
|
3
|
+
Version: 1.3.1
|
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.
|
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,19 +24,24 @@ 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)
|
39
43
|
Requires-Dist: tcxreader (>=0.4.5,<0.5.0)
|
44
|
+
Requires-Dist: tifffile (==2025.5.10)
|
40
45
|
Requires-Dist: tomli (>=2.0.1,<3.0.0) ; python_version < "3.11"
|
41
46
|
Requires-Dist: tqdm (>=4.64.0,<5.0.0)
|
42
47
|
Requires-Dist: vegafusion-python-embed (>=1.4.3,<2.0.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
|
-
|
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=
|
89
|
-
distance_km: Mapped[float] = mapped_column(sa.Float, nullable=
|
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(
|
101
|
-
|
102
|
-
|
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(
|
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 {
|
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
|
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=
|
395
|
+
consider_for_achievements=True,
|
368
396
|
)
|
369
|
-
DB.session.add(kind)
|
370
397
|
return kind
|
371
398
|
|
372
399
|
|
@@ -0,0 +1,277 @@
|
|
1
|
+
import datetime
|
2
|
+
import logging
|
3
|
+
import uuid
|
4
|
+
import zoneinfo
|
5
|
+
from typing import Callable
|
6
|
+
|
7
|
+
import numpy as np
|
8
|
+
import pandas as pd
|
9
|
+
|
10
|
+
from .config import Config
|
11
|
+
from .coordinates import get_distance
|
12
|
+
from .copernicus_dem import get_elevation
|
13
|
+
from .datamodel import Activity
|
14
|
+
from .datamodel import DB
|
15
|
+
from .missing_values import some
|
16
|
+
from .tiles import compute_tile_float
|
17
|
+
from .time_conversion import get_country_timezone
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
def enrichment_set_timezone(
|
23
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
24
|
+
) -> bool:
|
25
|
+
assert (
|
26
|
+
len(time_series) > 0
|
27
|
+
), f"You cannot import an activity without points. {activity=}"
|
28
|
+
latitude, longitude = time_series[["latitude", "longitude"]].iloc[0].to_list()
|
29
|
+
if activity.iana_timezone is None or activity.start_country is None:
|
30
|
+
country, tz_str = get_country_timezone(latitude, longitude)
|
31
|
+
activity.iana_timezone = tz_str
|
32
|
+
activity.start_country = country
|
33
|
+
return True
|
34
|
+
else:
|
35
|
+
return False
|
36
|
+
|
37
|
+
|
38
|
+
def enrichment_normalize_time(
|
39
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
40
|
+
) -> bool:
|
41
|
+
# Routes (as opposed to tracks) don't have time information. We cannot do anything with time here.
|
42
|
+
if (
|
43
|
+
"time" in time_series.columns
|
44
|
+
and pd.isna(time_series["time"]).all()
|
45
|
+
and not pd.api.types.is_datetime64_any_dtype(time_series["time"].dtype)
|
46
|
+
):
|
47
|
+
time_series["time"] = pd.NaT
|
48
|
+
return True
|
49
|
+
|
50
|
+
changed = False
|
51
|
+
tz_utc = zoneinfo.ZoneInfo("UTC")
|
52
|
+
# If the time is naive, assume that it is UTC.
|
53
|
+
if time_series["time"].dt.tz is None:
|
54
|
+
time_series["time"] = time_series["time"].dt.tz_localize(tz_utc)
|
55
|
+
changed = True
|
56
|
+
|
57
|
+
if time_series["time"].dt.tz.utcoffset(None) != tz_utc.utcoffset(None):
|
58
|
+
time_series["time"] = time_series["time"].dt.tz_convert(tz_utc)
|
59
|
+
changed = True
|
60
|
+
|
61
|
+
if not pd.api.types.is_dtype_equal(
|
62
|
+
time_series["time"].dtype, "datetime64[ns, UTC]"
|
63
|
+
):
|
64
|
+
time_series["time"] = time_series["time"].dt.tz_convert(tz_utc)
|
65
|
+
changed = True
|
66
|
+
|
67
|
+
assert pd.api.types.is_dtype_equal(
|
68
|
+
time_series["time"].dtype, "datetime64[ns, UTC]"
|
69
|
+
), (
|
70
|
+
time_series["time"].dtype,
|
71
|
+
time_series["time"].iloc[0],
|
72
|
+
)
|
73
|
+
|
74
|
+
new_start = some(time_series["time"].iloc[0])
|
75
|
+
if new_start != activity.start:
|
76
|
+
activity.start = new_start
|
77
|
+
changed = True
|
78
|
+
|
79
|
+
new_elapsed_time = some(time_series["time"].iloc[-1] - time_series["time"].iloc[0])
|
80
|
+
if new_elapsed_time != activity.elapsed_time:
|
81
|
+
activity.elapsed_time = new_elapsed_time
|
82
|
+
changed = True
|
83
|
+
|
84
|
+
return changed
|
85
|
+
|
86
|
+
|
87
|
+
def enrichment_rename_altitude(
|
88
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
89
|
+
) -> bool:
|
90
|
+
if "altitude" in time_series.columns:
|
91
|
+
time_series.rename(columns={"altitude": "elevation"}, inplace=True)
|
92
|
+
return True
|
93
|
+
else:
|
94
|
+
return False
|
95
|
+
|
96
|
+
|
97
|
+
def enrichment_compute_tile_xy(
|
98
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
99
|
+
) -> bool:
|
100
|
+
if "x" not in time_series.columns:
|
101
|
+
x, y = compute_tile_float(time_series["latitude"], time_series["longitude"], 0)
|
102
|
+
time_series["x"] = x
|
103
|
+
time_series["y"] = y
|
104
|
+
return True
|
105
|
+
else:
|
106
|
+
return False
|
107
|
+
|
108
|
+
|
109
|
+
def enrichment_copernicus_elevation(
|
110
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
111
|
+
) -> bool:
|
112
|
+
if "copernicus_elevation" not in time_series.columns:
|
113
|
+
time_series["copernicus_elevation"] = [
|
114
|
+
get_elevation(lat, lon)
|
115
|
+
for lat, lon in zip(time_series["latitude"], time_series["longitude"])
|
116
|
+
]
|
117
|
+
return True
|
118
|
+
else:
|
119
|
+
return False
|
120
|
+
|
121
|
+
|
122
|
+
def enrichment_elevation_gain(
|
123
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
124
|
+
) -> bool:
|
125
|
+
if (
|
126
|
+
"elevation" in time_series.columns
|
127
|
+
or "copernicus_elevation" in time_series.columns
|
128
|
+
) and "elevation_gain_cum" not in time_series.columns:
|
129
|
+
elevation = (
|
130
|
+
time_series["elevation"]
|
131
|
+
if "elevation" in time_series.columns
|
132
|
+
else time_series["copernicus_elevation"]
|
133
|
+
)
|
134
|
+
elevation_diff = elevation.diff()
|
135
|
+
elevation_diff = elevation_diff.ewm(span=5, min_periods=5).mean()
|
136
|
+
elevation_diff.loc[elevation_diff.abs() > 30] = 0
|
137
|
+
elevation_diff.loc[elevation_diff < 0] = 0
|
138
|
+
time_series["elevation_gain_cum"] = elevation_diff.cumsum().fillna(0)
|
139
|
+
|
140
|
+
activity.elevation_gain = (
|
141
|
+
time_series["elevation_gain_cum"].iloc[-1]
|
142
|
+
- time_series["elevation_gain_cum"].iloc[0]
|
143
|
+
)
|
144
|
+
return True
|
145
|
+
else:
|
146
|
+
return False
|
147
|
+
|
148
|
+
|
149
|
+
def enrichment_add_calories(
|
150
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
151
|
+
) -> bool:
|
152
|
+
if activity.calories is None and "calories" in time_series.columns:
|
153
|
+
activity.calories = (
|
154
|
+
time_series["calories"].iloc[-1] - time_series["calories"].iloc[0]
|
155
|
+
)
|
156
|
+
return True
|
157
|
+
else:
|
158
|
+
return False
|
159
|
+
|
160
|
+
|
161
|
+
def enrichment_distance(
|
162
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
163
|
+
) -> bool:
|
164
|
+
changed = False
|
165
|
+
|
166
|
+
distances = get_distance(
|
167
|
+
time_series["latitude"].shift(1),
|
168
|
+
time_series["longitude"].shift(1),
|
169
|
+
time_series["latitude"],
|
170
|
+
time_series["longitude"],
|
171
|
+
).fillna(0.0)
|
172
|
+
|
173
|
+
if config.time_diff_threshold_seconds:
|
174
|
+
time_diff = (
|
175
|
+
time_series["time"] - time_series["time"].shift(1)
|
176
|
+
).dt.total_seconds()
|
177
|
+
jump_indices = time_diff >= config.time_diff_threshold_seconds
|
178
|
+
distances.loc[jump_indices] = 0.0
|
179
|
+
|
180
|
+
if "distance_km" not in time_series.columns:
|
181
|
+
time_series["distance_km"] = pd.Series(np.cumsum(distances)) / 1000
|
182
|
+
changed = True
|
183
|
+
|
184
|
+
if "speed" not in time_series.columns:
|
185
|
+
time_series["speed"] = (
|
186
|
+
time_series["distance_km"].diff()
|
187
|
+
/ (time_series["time"].diff().dt.total_seconds() + 1e-3)
|
188
|
+
* 3600
|
189
|
+
)
|
190
|
+
changed = True
|
191
|
+
|
192
|
+
potential_jumps = (time_series["speed"] > 40) & (time_series["speed"].diff() > 10)
|
193
|
+
if np.any(potential_jumps):
|
194
|
+
time_series.replace(time_series.loc[~potential_jumps])
|
195
|
+
changed = True
|
196
|
+
|
197
|
+
if "segment_id" not in time_series.columns:
|
198
|
+
if config.time_diff_threshold_seconds:
|
199
|
+
time_series["segment_id"] = np.cumsum(jump_indices)
|
200
|
+
else:
|
201
|
+
time_series["segment_id"] = 0
|
202
|
+
changed = True
|
203
|
+
|
204
|
+
new_distance_km = (
|
205
|
+
time_series["distance_km"].iloc[-1] - time_series["distance_km"].iloc[0]
|
206
|
+
)
|
207
|
+
if new_distance_km != activity.distance_km:
|
208
|
+
activity.distance_km = new_distance_km
|
209
|
+
changed = True
|
210
|
+
|
211
|
+
return changed
|
212
|
+
|
213
|
+
|
214
|
+
def enrichment_moving_time(
|
215
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
216
|
+
) -> bool:
|
217
|
+
def moving_time(group) -> datetime.timedelta:
|
218
|
+
selection = group["speed"] > 1.0
|
219
|
+
time_diff = group["time"].diff().loc[selection]
|
220
|
+
return time_diff.sum()
|
221
|
+
|
222
|
+
new_moving_time = (
|
223
|
+
time_series.groupby("segment_id").apply(moving_time, include_groups=False).sum()
|
224
|
+
)
|
225
|
+
if new_moving_time != activity.moving_time:
|
226
|
+
activity.moving_time = new_moving_time
|
227
|
+
return True
|
228
|
+
else:
|
229
|
+
return False
|
230
|
+
|
231
|
+
|
232
|
+
def enrichment_copy_latlon(
|
233
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
234
|
+
) -> bool:
|
235
|
+
if activity.start_latitude is None:
|
236
|
+
activity.start_latitude = time_series["latitude"].iloc[0]
|
237
|
+
activity.end_latitude = time_series["latitude"].iloc[-1]
|
238
|
+
activity.start_longitude = time_series["longitude"].iloc[0]
|
239
|
+
activity.end_longitude = time_series["longitude"].iloc[-1]
|
240
|
+
return True
|
241
|
+
else:
|
242
|
+
return False
|
243
|
+
|
244
|
+
|
245
|
+
enrichments: list[Callable[[Activity, pd.DataFrame, Config], bool]] = [
|
246
|
+
enrichment_set_timezone,
|
247
|
+
enrichment_normalize_time,
|
248
|
+
enrichment_rename_altitude,
|
249
|
+
enrichment_compute_tile_xy,
|
250
|
+
enrichment_copernicus_elevation,
|
251
|
+
enrichment_elevation_gain,
|
252
|
+
enrichment_add_calories,
|
253
|
+
enrichment_distance,
|
254
|
+
enrichment_moving_time,
|
255
|
+
enrichment_copy_latlon,
|
256
|
+
]
|
257
|
+
|
258
|
+
|
259
|
+
def apply_enrichments(
|
260
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
261
|
+
) -> bool:
|
262
|
+
was_changed = False
|
263
|
+
for enrichment in enrichments:
|
264
|
+
was_changed |= enrichment(activity, time_series, config)
|
265
|
+
return was_changed
|
266
|
+
|
267
|
+
|
268
|
+
def update_and_commit(
|
269
|
+
activity: Activity, time_series: pd.DataFrame, config: Config
|
270
|
+
) -> None:
|
271
|
+
changed = apply_enrichments(activity, time_series, config)
|
272
|
+
if not activity.time_series_uuid:
|
273
|
+
activity.time_series_uuid = str(uuid.uuid4())
|
274
|
+
if changed:
|
275
|
+
activity.replace_time_series(time_series)
|
276
|
+
DB.session.add(activity)
|
277
|
+
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:
|