geo-activity-playground 0.35.0__py3-none-any.whl → 0.36.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- geo_activity_playground/__main__.py +12 -0
- geo_activity_playground/core/activities.py +21 -13
- geo_activity_playground/core/raster_map.py +246 -0
- geo_activity_playground/core/tiles.py +6 -50
- geo_activity_playground/explorer/video.py +1 -1
- geo_activity_playground/heatmap_video.py +93 -0
- geo_activity_playground/importers/activity_parsers.py +1 -1
- geo_activity_playground/importers/directory.py +6 -15
- geo_activity_playground/webui/activity/blueprint.py +3 -10
- geo_activity_playground/webui/activity/controller.py +10 -71
- geo_activity_playground/webui/app.py +32 -22
- geo_activity_playground/webui/{auth/blueprint.py → auth_blueprint.py} +1 -1
- geo_activity_playground/webui/calendar/blueprint.py +2 -5
- geo_activity_playground/webui/{eddington/controller.py → eddington_blueprint.py} +17 -13
- geo_activity_playground/webui/equipment/blueprint.py +2 -8
- geo_activity_playground/webui/explorer/blueprint.py +2 -10
- geo_activity_playground/webui/heatmap/blueprint.py +36 -10
- geo_activity_playground/webui/heatmap/heatmap_controller.py +151 -71
- geo_activity_playground/webui/heatmap/templates/heatmap/index.html.j2 +30 -12
- geo_activity_playground/webui/{search/blueprint.py → search_blueprint.py} +1 -1
- geo_activity_playground/webui/settings/blueprint.py +1 -2
- geo_activity_playground/webui/square_planner_blueprint.py +118 -0
- geo_activity_playground/webui/{summary/controller.py → summary_blueprint.py} +23 -24
- geo_activity_playground/webui/templates/page.html.j2 +11 -0
- geo_activity_playground/webui/tile_blueprint.py +42 -0
- geo_activity_playground/webui/upload_blueprint.py +1 -3
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/METADATA +1 -1
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/RECORD +36 -43
- geo_activity_playground/core/heatmap.py +0 -194
- geo_activity_playground/webui/eddington/__init__.py +0 -0
- geo_activity_playground/webui/eddington/blueprint.py +0 -19
- geo_activity_playground/webui/square_planner/__init__.py +0 -0
- geo_activity_playground/webui/square_planner/blueprint.py +0 -38
- geo_activity_playground/webui/square_planner/controller.py +0 -101
- geo_activity_playground/webui/summary/__init__.py +0 -0
- geo_activity_playground/webui/summary/blueprint.py +0 -17
- geo_activity_playground/webui/tile/__init__.py +0 -0
- geo_activity_playground/webui/tile/blueprint.py +0 -32
- geo_activity_playground/webui/tile/controller.py +0 -36
- /geo_activity_playground/webui/{auth/templates → templates}/auth/index.html.j2 +0 -0
- /geo_activity_playground/webui/{eddington/templates → templates}/eddington/index.html.j2 +0 -0
- /geo_activity_playground/webui/{search/templates → templates}/search/index.html.j2 +0 -0
- /geo_activity_playground/webui/{square_planner/templates → templates}/square_planner/index.html.j2 +0 -0
- /geo_activity_playground/webui/{summary/templates → templates}/summary/index.html.j2 +0 -0
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/LICENSE +0 -0
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/WHEEL +0 -0
- {geo_activity_playground-0.35.0.dist-info → geo_activity_playground-0.36.0.dist-info}/entry_points.txt +0 -0
| @@ -1,6 +1,8 @@ | |
| 1 | 
            +
            import datetime
         | 
| 1 2 | 
             
            import io
         | 
| 2 3 | 
             
            import logging
         | 
| 3 4 | 
             
            import pathlib
         | 
| 5 | 
            +
            from typing import Optional
         | 
| 4 6 |  | 
| 5 7 | 
             
            import matplotlib.pylab as pl
         | 
| 6 8 | 
             
            import numpy as np
         | 
| @@ -9,12 +11,13 @@ from PIL import ImageDraw | |
| 9 11 |  | 
| 10 12 | 
             
            from geo_activity_playground.core.activities import ActivityRepository
         | 
| 11 13 | 
             
            from geo_activity_playground.core.config import Config
         | 
| 12 | 
            -
            from geo_activity_playground.core. | 
| 13 | 
            -
            from geo_activity_playground.core. | 
| 14 | 
            -
            from geo_activity_playground.core. | 
| 15 | 
            -
            from geo_activity_playground.core. | 
| 14 | 
            +
            from geo_activity_playground.core.raster_map import convert_to_grayscale
         | 
| 15 | 
            +
            from geo_activity_playground.core.raster_map import GeoBounds
         | 
| 16 | 
            +
            from geo_activity_playground.core.raster_map import get_sensible_zoom_level
         | 
| 17 | 
            +
            from geo_activity_playground.core.raster_map import get_tile
         | 
| 18 | 
            +
            from geo_activity_playground.core.raster_map import OSM_TILE_SIZE
         | 
| 19 | 
            +
            from geo_activity_playground.core.raster_map import PixelBounds
         | 
| 16 20 | 
             
            from geo_activity_playground.core.tasks import work_tracker
         | 
| 17 | 
            -
            from geo_activity_playground.core.tiles import get_tile
         | 
| 18 21 | 
             
            from geo_activity_playground.core.tiles import get_tile_upper_left_lat_lon
         | 
| 19 22 | 
             
            from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
         | 
| 20 23 | 
             
            from geo_activity_playground.webui.explorer.controller import (
         | 
| @@ -25,9 +28,6 @@ from geo_activity_playground.webui.explorer.controller import ( | |
| 25 28 | 
             
            logger = logging.getLogger(__name__)
         | 
| 26 29 |  | 
| 27 30 |  | 
| 28 | 
            -
            OSM_TILE_SIZE = 256  # OSM tile size in pixel
         | 
| 29 | 
            -
             | 
| 30 | 
            -
             | 
| 31 31 | 
             
            class HeatmapController:
         | 
| 32 32 | 
             
                def __init__(
         | 
| 33 33 | 
             
                    self,
         | 
| @@ -48,7 +48,12 @@ class HeatmapController: | |
| 48 48 | 
             
                        "activities_per_tile"
         | 
| 49 49 | 
             
                    ]
         | 
| 50 50 |  | 
| 51 | 
            -
                def render( | 
| 51 | 
            +
                def render(
         | 
| 52 | 
            +
                    self,
         | 
| 53 | 
            +
                    kinds: list[int],
         | 
| 54 | 
            +
                    date_start: Optional[datetime.date],
         | 
| 55 | 
            +
                    date_end: Optional[datetime.date],
         | 
| 56 | 
            +
                ) -> dict:
         | 
| 52 57 | 
             
                    zoom = 14
         | 
| 53 58 | 
             
                    tiles = self.tile_histories[zoom]
         | 
| 54 59 | 
             
                    medians = tiles.median(skipna=True)
         | 
| @@ -60,9 +65,17 @@ class HeatmapController: | |
| 60 65 | 
             
                    available_kinds = sorted(self._repository.meta["kind"].unique())
         | 
| 61 66 |  | 
| 62 67 | 
             
                    if not kinds:
         | 
| 63 | 
            -
                        kinds = available_kinds
         | 
| 68 | 
            +
                        kinds = list(range(len(available_kinds)))
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    extra_args = []
         | 
| 71 | 
            +
                    if date_start is not None:
         | 
| 72 | 
            +
                        extra_args.append(f"date-start={date_start.isoformat()}")
         | 
| 73 | 
            +
                    if date_end is not None:
         | 
| 74 | 
            +
                        extra_args.append(f"date-end={date_end.isoformat()}")
         | 
| 75 | 
            +
                    for kind in kinds:
         | 
| 76 | 
            +
                        extra_args.append(f"kind={kind}")
         | 
| 64 77 |  | 
| 65 | 
            -
                     | 
| 78 | 
            +
                    values = {
         | 
| 66 79 | 
             
                        "center": {
         | 
| 67 80 | 
             
                            "latitude": median_lat,
         | 
| 68 81 | 
             
                            "longitude": median_lon,
         | 
| @@ -76,71 +89,117 @@ class HeatmapController: | |
| 76 89 | 
             
                        },
         | 
| 77 90 | 
             
                        "kinds": kinds,
         | 
| 78 91 | 
             
                        "available_kinds": available_kinds,
         | 
| 79 | 
            -
                        " | 
| 92 | 
            +
                        "extra_args": "&".join(extra_args),
         | 
| 80 93 | 
             
                    }
         | 
| 94 | 
            +
                    if date_start is not None:
         | 
| 95 | 
            +
                        values["date_start"] = date_start.date().isoformat()
         | 
| 96 | 
            +
                    if date_end is not None:
         | 
| 97 | 
            +
                        values["date_end"] = date_end.date().isoformat()
         | 
| 81 98 |  | 
| 82 | 
            -
             | 
| 99 | 
            +
                    return values
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def _get_counts(
         | 
| 102 | 
            +
                    self,
         | 
| 103 | 
            +
                    x: int,
         | 
| 104 | 
            +
                    y: int,
         | 
| 105 | 
            +
                    z: int,
         | 
| 106 | 
            +
                    kind: str,
         | 
| 107 | 
            +
                    date_start: Optional[datetime.date],
         | 
| 108 | 
            +
                    date_end: Optional[datetime.date],
         | 
| 109 | 
            +
                ) -> np.ndarray:
         | 
| 83 110 | 
             
                    tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
         | 
| 84 | 
            -
                     | 
| 85 | 
            -
                    if  | 
| 86 | 
            -
                         | 
| 87 | 
            -
                             | 
| 88 | 
            -
                         | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
             | 
| 92 | 
            -
                             | 
| 93 | 
            -
                            tile_counts = np.zeros(tile_pixels, dtype=np.int32)
         | 
| 94 | 
            -
                    else:
         | 
| 95 | 
            -
                        tile_counts = np.zeros(tile_pixels, dtype=np.int32)
         | 
| 96 | 
            -
                    tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 97 | 
            -
                    activity_ids = self.activities_per_tile[z].get((x, y), set())
         | 
| 98 | 
            -
                    activity_ids_kind = set()
         | 
| 99 | 
            -
                    for activity_id in activity_ids:
         | 
| 100 | 
            -
                        activity = self._repository.get_activity_by_id(activity_id)
         | 
| 101 | 
            -
                        if activity["kind"] == kind:
         | 
| 102 | 
            -
                            activity_ids_kind.add(activity_id)
         | 
| 103 | 
            -
                    if activity_ids_kind:
         | 
| 104 | 
            -
                        with work_tracker(
         | 
| 105 | 
            -
                            tile_count_cache_path.with_suffix(".json")
         | 
| 106 | 
            -
                        ) as parsed_activities:
         | 
| 107 | 
            -
                            if parsed_activities - activity_ids_kind:
         | 
| 111 | 
            +
                    tile_counts = np.zeros(tile_pixels, dtype=np.int32)
         | 
| 112 | 
            +
                    if date_start is None and date_end is None:
         | 
| 113 | 
            +
                        tile_count_cache_path = pathlib.Path(
         | 
| 114 | 
            +
                            f"Cache/Heatmap/{kind}/{z}/{x}/{y}.npy"
         | 
| 115 | 
            +
                        )
         | 
| 116 | 
            +
                        if tile_count_cache_path.exists():
         | 
| 117 | 
            +
                            try:
         | 
| 118 | 
            +
                                tile_counts = np.load(tile_count_cache_path)
         | 
| 119 | 
            +
                            except ValueError:
         | 
| 108 120 | 
             
                                logger.warning(
         | 
| 109 | 
            -
                                    f" | 
| 121 | 
            +
                                    f"Heatmap count file {tile_count_cache_path} is corrupted, deleting."
         | 
| 110 122 | 
             
                                )
         | 
| 123 | 
            +
                                tile_count_cache_path.unlink()
         | 
| 111 124 | 
             
                                tile_counts = np.zeros(tile_pixels, dtype=np.int32)
         | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 114 | 
            -
             | 
| 115 | 
            -
             | 
| 116 | 
            -
             | 
| 117 | 
            -
             | 
| 118 | 
            -
                                 | 
| 119 | 
            -
             | 
| 120 | 
            -
             | 
| 121 | 
            -
             | 
| 122 | 
            -
             | 
| 123 | 
            -
             | 
| 125 | 
            +
                        tile_count_cache_path.parent.mkdir(parents=True, exist_ok=True)
         | 
| 126 | 
            +
                        activity_ids = self.activities_per_tile[z].get((x, y), set())
         | 
| 127 | 
            +
                        activity_ids_kind = set()
         | 
| 128 | 
            +
                        for activity_id in activity_ids:
         | 
| 129 | 
            +
                            activity = self._repository.get_activity_by_id(activity_id)
         | 
| 130 | 
            +
                            if activity["kind"] == kind:
         | 
| 131 | 
            +
                                activity_ids_kind.add(activity_id)
         | 
| 132 | 
            +
                        if activity_ids_kind:
         | 
| 133 | 
            +
                            with work_tracker(
         | 
| 134 | 
            +
                                tile_count_cache_path.with_suffix(".json")
         | 
| 135 | 
            +
                            ) as parsed_activities:
         | 
| 136 | 
            +
                                if parsed_activities - activity_ids_kind:
         | 
| 137 | 
            +
                                    logger.warning(
         | 
| 138 | 
            +
                                        f"Resetting heatmap cache for {kind=}/{x=}/{y=}/{z=} because activities have been removed."
         | 
| 124 139 | 
             
                                    )
         | 
| 125 | 
            -
                                     | 
| 126 | 
            -
                                     | 
| 127 | 
            -
             | 
| 128 | 
            -
                                     | 
| 129 | 
            -
             | 
| 130 | 
            -
                                     | 
| 131 | 
            -
             | 
| 132 | 
            -
             | 
| 133 | 
            -
             | 
| 134 | 
            -
             | 
| 140 | 
            +
                                    tile_counts = np.zeros(tile_pixels, dtype=np.int32)
         | 
| 141 | 
            +
                                    parsed_activities.clear()
         | 
| 142 | 
            +
                                for activity_id in activity_ids_kind:
         | 
| 143 | 
            +
                                    if activity_id in parsed_activities:
         | 
| 144 | 
            +
                                        continue
         | 
| 145 | 
            +
                                    parsed_activities.add(activity_id)
         | 
| 146 | 
            +
                                    time_series = self._repository.get_time_series(activity_id)
         | 
| 147 | 
            +
                                    for _, group in time_series.groupby("segment_id"):
         | 
| 148 | 
            +
                                        xy_pixels = (
         | 
| 149 | 
            +
                                            np.array(
         | 
| 150 | 
            +
                                                [group["x"] * 2**z - x, group["y"] * 2**z - y]
         | 
| 151 | 
            +
                                            ).T
         | 
| 152 | 
            +
                                            * OSM_TILE_SIZE
         | 
| 153 | 
            +
                                        )
         | 
| 154 | 
            +
                                        im = Image.new("L", tile_pixels)
         | 
| 155 | 
            +
                                        draw = ImageDraw.Draw(im)
         | 
| 156 | 
            +
                                        pixels = list(map(int, xy_pixels.flatten()))
         | 
| 157 | 
            +
                                        draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
         | 
| 158 | 
            +
                                        aim = np.array(im)
         | 
| 159 | 
            +
                                        tile_counts += aim
         | 
| 160 | 
            +
                            tmp_path = tile_count_cache_path.with_suffix(".tmp.npy")
         | 
| 161 | 
            +
                            np.save(tmp_path, tile_counts)
         | 
| 162 | 
            +
                            tile_count_cache_path.unlink(missing_ok=True)
         | 
| 163 | 
            +
                            tmp_path.rename(tile_count_cache_path)
         | 
| 164 | 
            +
                    else:
         | 
| 165 | 
            +
                        activity_ids = self.activities_per_tile[z].get((x, y), set())
         | 
| 166 | 
            +
                        for activity_id in activity_ids:
         | 
| 167 | 
            +
                            activity = self._repository.get_activity_by_id(activity_id)
         | 
| 168 | 
            +
                            if not activity["kind"] == kind:
         | 
| 169 | 
            +
                                continue
         | 
| 170 | 
            +
                            if date_start is not None and activity["start"] < date_start:
         | 
| 171 | 
            +
                                continue
         | 
| 172 | 
            +
                            if date_end is not None and date_end < activity["start"]:
         | 
| 173 | 
            +
                                continue
         | 
| 174 | 
            +
                            time_series = self._repository.get_time_series(activity_id)
         | 
| 175 | 
            +
                            for _, group in time_series.groupby("segment_id"):
         | 
| 176 | 
            +
                                xy_pixels = (
         | 
| 177 | 
            +
                                    np.array([group["x"] * 2**z - x, group["y"] * 2**z - y]).T
         | 
| 178 | 
            +
                                    * OSM_TILE_SIZE
         | 
| 179 | 
            +
                                )
         | 
| 180 | 
            +
                                im = Image.new("L", tile_pixels)
         | 
| 181 | 
            +
                                draw = ImageDraw.Draw(im)
         | 
| 182 | 
            +
                                pixels = list(map(int, xy_pixels.flatten()))
         | 
| 183 | 
            +
                                draw.line(pixels, fill=1, width=max(3, 6 * (z - 17)))
         | 
| 184 | 
            +
                                aim = np.array(im)
         | 
| 185 | 
            +
                                tile_counts += aim
         | 
| 135 186 | 
             
                    return tile_counts
         | 
| 136 187 |  | 
| 137 188 | 
             
                def _render_tile_image(
         | 
| 138 | 
            -
                    self, | 
| 189 | 
            +
                    self,
         | 
| 190 | 
            +
                    x: int,
         | 
| 191 | 
            +
                    y: int,
         | 
| 192 | 
            +
                    z: int,
         | 
| 193 | 
            +
                    kinds_ids: list[int],
         | 
| 194 | 
            +
                    date_start: Optional[datetime.date],
         | 
| 195 | 
            +
                    date_end: Optional[datetime.date],
         | 
| 139 196 | 
             
                ) -> np.ndarray:
         | 
| 140 197 | 
             
                    tile_pixels = (OSM_TILE_SIZE, OSM_TILE_SIZE)
         | 
| 141 198 | 
             
                    tile_counts = np.zeros(tile_pixels)
         | 
| 142 | 
            -
                     | 
| 143 | 
            -
             | 
| 199 | 
            +
                    available_kinds = sorted(self._repository.meta["kind"].unique())
         | 
| 200 | 
            +
                    for kind_id in kinds_ids:
         | 
| 201 | 
            +
                        kind = available_kinds[kind_id]
         | 
| 202 | 
            +
                        tile_counts += self._get_counts(x, y, z, kind, date_start, date_end)
         | 
| 144 203 |  | 
| 145 204 | 
             
                    tile_counts = np.sqrt(tile_counts) / 5
         | 
| 146 205 | 
             
                    tile_counts[tile_counts > 1.0] = 1.0
         | 
| @@ -158,21 +217,40 @@ class HeatmapController: | |
| 158 217 | 
             
                        ] + data_color[:, :, c]
         | 
| 159 218 | 
             
                    return map_tile
         | 
| 160 219 |  | 
| 161 | 
            -
                def render_tile( | 
| 220 | 
            +
                def render_tile(
         | 
| 221 | 
            +
                    self,
         | 
| 222 | 
            +
                    x: int,
         | 
| 223 | 
            +
                    y: int,
         | 
| 224 | 
            +
                    z: int,
         | 
| 225 | 
            +
                    kind_ids: list[int],
         | 
| 226 | 
            +
                    date_start: Optional[datetime.date],
         | 
| 227 | 
            +
                    date_end: Optional[datetime.date],
         | 
| 228 | 
            +
                ) -> bytes:
         | 
| 162 229 | 
             
                    f = io.BytesIO()
         | 
| 163 | 
            -
                    pl.imsave( | 
| 230 | 
            +
                    pl.imsave(
         | 
| 231 | 
            +
                        f,
         | 
| 232 | 
            +
                        self._render_tile_image(x, y, z, kind_ids, date_start, date_end),
         | 
| 233 | 
            +
                        format="png",
         | 
| 234 | 
            +
                    )
         | 
| 164 235 | 
             
                    return bytes(f.getbuffer())
         | 
| 165 236 |  | 
| 166 237 | 
             
                def download_heatmap(
         | 
| 167 | 
            -
                    self, | 
| 238 | 
            +
                    self,
         | 
| 239 | 
            +
                    north: float,
         | 
| 240 | 
            +
                    east: float,
         | 
| 241 | 
            +
                    south: float,
         | 
| 242 | 
            +
                    west: float,
         | 
| 243 | 
            +
                    kind_ids: list[int],
         | 
| 244 | 
            +
                    date_start: Optional[datetime.date],
         | 
| 245 | 
            +
                    date_end: Optional[datetime.date],
         | 
| 168 246 | 
             
                ) -> bytes:
         | 
| 169 247 | 
             
                    geo_bounds = GeoBounds(south, west, north, east)
         | 
| 170 248 | 
             
                    tile_bounds = get_sensible_zoom_level(geo_bounds, (4000, 4000))
         | 
| 171 249 | 
             
                    pixel_bounds = PixelBounds.from_tile_bounds(tile_bounds)
         | 
| 172 250 |  | 
| 173 251 | 
             
                    background = np.zeros((*pixel_bounds.shape, 3))
         | 
| 174 | 
            -
                    for x in range(tile_bounds. | 
| 175 | 
            -
                        for y in range(tile_bounds. | 
| 252 | 
            +
                    for x in range(tile_bounds.x1, tile_bounds.x2):
         | 
| 253 | 
            +
                        for y in range(tile_bounds.y1, tile_bounds.y2):
         | 
| 176 254 | 
             
                            tile = (
         | 
| 177 255 | 
             
                                np.array(
         | 
| 178 256 | 
             
                                    get_tile(tile_bounds.zoom, x, y, self._config.map_tile_url)
         | 
| @@ -180,14 +258,16 @@ class HeatmapController: | |
| 180 258 | 
             
                                / 255
         | 
| 181 259 | 
             
                            )
         | 
| 182 260 |  | 
| 183 | 
            -
                            i = y - tile_bounds. | 
| 184 | 
            -
                            j = x - tile_bounds. | 
| 261 | 
            +
                            i = y - tile_bounds.y1
         | 
| 262 | 
            +
                            j = x - tile_bounds.x1
         | 
| 185 263 |  | 
| 186 264 | 
             
                            background[
         | 
| 187 265 | 
             
                                i * OSM_TILE_SIZE : (i + 1) * OSM_TILE_SIZE,
         | 
| 188 266 | 
             
                                j * OSM_TILE_SIZE : (j + 1) * OSM_TILE_SIZE,
         | 
| 189 267 | 
             
                                :,
         | 
| 190 | 
            -
                            ] = self._render_tile_image( | 
| 268 | 
            +
                            ] = self._render_tile_image(
         | 
| 269 | 
            +
                                x, y, tile_bounds.zoom, kind_ids, date_start, date_end
         | 
| 270 | 
            +
                            )
         | 
| 191 271 |  | 
| 192 272 | 
             
                    f = io.BytesIO()
         | 
| 193 273 | 
             
                    pl.imsave(f, background, format="png")
         | 
| @@ -7,20 +7,38 @@ | |
| 7 7 | 
             
                </div>
         | 
| 8 8 | 
             
            </div>
         | 
| 9 9 |  | 
| 10 | 
            -
            < | 
| 11 | 
            -
             | 
| 12 | 
            -
             | 
| 10 | 
            +
            <form action="" method="GET">
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                <div class="row mb-3">
         | 
| 13 | 
            +
                    <label class="col-sm-2 col-form-label">Kinds</label>
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    <div class="col-sm-10">
         | 
| 13 16 | 
             
                        {% for kind in available_kinds %}
         | 
| 14 | 
            -
                        <div class="form-check form-check-inline form-switch">
         | 
| 15 | 
            -
                            <input class="form-check-input" type="checkbox" role="switch" id="{{  | 
| 16 | 
            -
                                value="{{  | 
| 17 | 
            -
                            <label class="form-check-label" for="{{  | 
| 17 | 
            +
                        <div class="form-check form-check-inline form-switch ">
         | 
| 18 | 
            +
                            <input class="form-check-input" type="checkbox" role="switch" id="kind{{ loop.index0 }}" name="kind"
         | 
| 19 | 
            +
                                value="{{ loop.index0 }}" {{ 'checked' if loop.index0 in kinds else '' }} />
         | 
| 20 | 
            +
                            <label class="form-check-label" for="kind{{ loop.index0 }}">{{ kind }}</label>
         | 
| 18 21 | 
             
                        </div>
         | 
| 19 22 | 
             
                        {% endfor %}
         | 
| 20 | 
            -
             | 
| 21 | 
            -
                    </form>
         | 
| 23 | 
            +
                    </div>
         | 
| 22 24 | 
             
                </div>
         | 
| 23 | 
            -
             | 
| 25 | 
            +
             | 
| 26 | 
            +
                <div class="row mb-3">
         | 
| 27 | 
            +
                    <label class="col-sm-2 col-form-label">Date range</label>
         | 
| 28 | 
            +
                    <div class="col-sm-5">
         | 
| 29 | 
            +
                        <input type="date" id="date-start" name="date-start" value="{{ date_start }}" />
         | 
| 30 | 
            +
                        <label for="date-start" class="form-label">Start date</label>
         | 
| 31 | 
            +
                    </div>
         | 
| 32 | 
            +
                    <div class="col-sm-5">
         | 
| 33 | 
            +
                        <input type="date" id="date-end" name="date-end" value="{{ date_end }}" />
         | 
| 34 | 
            +
                        <label for="date-start" class="form-label">End date</label>
         | 
| 35 | 
            +
                    </div>
         | 
| 36 | 
            +
                </div>
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                <div class="mb-3">
         | 
| 39 | 
            +
                    <button type="submit" class="btn btn-primary">Filter heatmap</button>
         | 
| 40 | 
            +
                </div>
         | 
| 41 | 
            +
            </form>
         | 
| 24 42 |  | 
| 25 43 | 
             
            <div class="row mb-3">
         | 
| 26 44 | 
             
                <div class="col">
         | 
| @@ -33,7 +51,7 @@ | |
| 33 51 | 
             
                            center: [{{ center.latitude }}, {{ center.longitude }}],
         | 
| 34 52 | 
             
                        zoom: 12
         | 
| 35 53 | 
             
                        });
         | 
| 36 | 
            -
                        L.tileLayer('/heatmap/tile/{z}/{x}/{y} | 
| 54 | 
            +
                        L.tileLayer('/heatmap/tile/{z}/{x}/{y}.png?{{ extra_args|safe }}', {
         | 
| 37 55 | 
             
                            maxZoom: 19,
         | 
| 38 56 | 
             
                            attribution: '{{ map_tile_attribution|safe }}'
         | 
| 39 57 | 
             
                        }).addTo(map)
         | 
| @@ -47,7 +65,7 @@ | |
| 47 65 | 
             
                        function downloadAs() {
         | 
| 48 66 | 
             
                            bounds = map.getBounds()
         | 
| 49 67 | 
             
                            window.location.href =
         | 
| 50 | 
            -
                                `/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/{{  | 
| 68 | 
            +
                                `/heatmap/download/${bounds.getNorth()}/${bounds.getEast()}/${bounds.getSouth()}/${bounds.getWest()}/heatmap.png?{{ extra_args|safe }}`
         | 
| 51 69 | 
             
                        }
         | 
| 52 70 | 
             
                    </script>
         | 
| 53 71 | 
             
                </div>
         | 
| @@ -0,0 +1,118 @@ | |
| 1 | 
            +
            import geojson
         | 
| 2 | 
            +
            from flask import Blueprint
         | 
| 3 | 
            +
            from flask import redirect
         | 
| 4 | 
            +
            from flask import render_template
         | 
| 5 | 
            +
            from flask import Response
         | 
| 6 | 
            +
            from flask import url_for
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            from geo_activity_playground.explorer.grid_file import make_explorer_rectangle
         | 
| 9 | 
            +
            from geo_activity_playground.explorer.grid_file import make_explorer_tile
         | 
| 10 | 
            +
            from geo_activity_playground.explorer.grid_file import make_grid_file_geojson
         | 
| 11 | 
            +
            from geo_activity_playground.explorer.grid_file import make_grid_file_gpx
         | 
| 12 | 
            +
            from geo_activity_playground.explorer.grid_file import make_grid_points
         | 
| 13 | 
            +
            from geo_activity_playground.explorer.tile_visits import TileVisitAccessor
         | 
| 14 | 
            +
             | 
| 15 | 
            +
             | 
| 16 | 
            +
            def make_square_planner_blueprint(tile_visit_accessor: TileVisitAccessor) -> Blueprint:
         | 
| 17 | 
            +
                tile_visits = tile_visit_accessor.tile_state["tile_visits"]
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                blueprint = Blueprint("square_planner", __name__, template_folder="templates")
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                @blueprint.route("/<int:zoom>")
         | 
| 22 | 
            +
                def landing(zoom: int):
         | 
| 23 | 
            +
                    explored = tile_visit_accessor.tile_state["evolution_state"][zoom]
         | 
| 24 | 
            +
                    return redirect(
         | 
| 25 | 
            +
                        url_for(
         | 
| 26 | 
            +
                            "square_planner.index",
         | 
| 27 | 
            +
                            zoom=zoom,
         | 
| 28 | 
            +
                            x=explored.square_x,
         | 
| 29 | 
            +
                            y=explored.square_y,
         | 
| 30 | 
            +
                            size=explored.max_square_size,
         | 
| 31 | 
            +
                        )
         | 
| 32 | 
            +
                    )
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                @blueprint.route("/<int:zoom>/<int:x>/<int:y>/<int:size>")
         | 
| 35 | 
            +
                def index(zoom: int, x: int, y: int, size: int):
         | 
| 36 | 
            +
                    square_geojson = geojson.dumps(
         | 
| 37 | 
            +
                        geojson.FeatureCollection(
         | 
| 38 | 
            +
                            features=[
         | 
| 39 | 
            +
                                make_explorer_rectangle(
         | 
| 40 | 
            +
                                    x,
         | 
| 41 | 
            +
                                    y,
         | 
| 42 | 
            +
                                    x + size,
         | 
| 43 | 
            +
                                    y + size,
         | 
| 44 | 
            +
                                    zoom,
         | 
| 45 | 
            +
                                )
         | 
| 46 | 
            +
                            ]
         | 
| 47 | 
            +
                        )
         | 
| 48 | 
            +
                    )
         | 
| 49 | 
            +
             | 
| 50 | 
            +
                    missing_geojson = geojson.dumps(
         | 
| 51 | 
            +
                        geojson.FeatureCollection(
         | 
| 52 | 
            +
                            features=[
         | 
| 53 | 
            +
                                make_explorer_tile(
         | 
| 54 | 
            +
                                    tile_x,
         | 
| 55 | 
            +
                                    tile_y,
         | 
| 56 | 
            +
                                    {},
         | 
| 57 | 
            +
                                    zoom,
         | 
| 58 | 
            +
                                )
         | 
| 59 | 
            +
                                for tile_x in range(x, x + size)
         | 
| 60 | 
            +
                                for tile_y in range(y, y + size)
         | 
| 61 | 
            +
                                if (tile_x, tile_y) not in set(tile_visits[zoom].keys())
         | 
| 62 | 
            +
                            ]
         | 
| 63 | 
            +
                        )
         | 
| 64 | 
            +
                    )
         | 
| 65 | 
            +
             | 
| 66 | 
            +
                    return render_template(
         | 
| 67 | 
            +
                        "square_planner/index.html.j2",
         | 
| 68 | 
            +
                        explored_geojson=_get_explored_geojson(tile_visits[zoom].keys(), zoom),
         | 
| 69 | 
            +
                        missing_geojson=missing_geojson,
         | 
| 70 | 
            +
                        square_geojson=square_geojson,
         | 
| 71 | 
            +
                        zoom=zoom,
         | 
| 72 | 
            +
                        square_x=x,
         | 
| 73 | 
            +
                        square_y=y,
         | 
| 74 | 
            +
                        square_size=size,
         | 
| 75 | 
            +
                    )
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                @blueprint.route("/<int:zoom>/<int:x>/<int:y>/<int:size>/missing.<suffix>")
         | 
| 78 | 
            +
                def square_planner_missing(zoom: int, x: int, y: int, size: int, suffix: str):
         | 
| 79 | 
            +
                    points = make_grid_points(
         | 
| 80 | 
            +
                        (
         | 
| 81 | 
            +
                            (tile_x, tile_y)
         | 
| 82 | 
            +
                            for tile_x in range(x, x + size)
         | 
| 83 | 
            +
                            for tile_y in range(y, y + size)
         | 
| 84 | 
            +
                            if (tile_x, tile_y) not in set(tile_visits[zoom].keys())
         | 
| 85 | 
            +
                        ),
         | 
| 86 | 
            +
                        zoom,
         | 
| 87 | 
            +
                    )
         | 
| 88 | 
            +
                    if suffix == "geojson":
         | 
| 89 | 
            +
                        response = make_grid_file_geojson(points)
         | 
| 90 | 
            +
                    elif suffix == "gpx":
         | 
| 91 | 
            +
                        response = make_grid_file_gpx(points)
         | 
| 92 | 
            +
                    else:
         | 
| 93 | 
            +
                        raise RuntimeError(f"Unsupported suffix {suffix}.")
         | 
| 94 | 
            +
             | 
| 95 | 
            +
                    mimetypes = {"geojson": "application/json", "gpx": "application/xml"}
         | 
| 96 | 
            +
                    return Response(
         | 
| 97 | 
            +
                        response,
         | 
| 98 | 
            +
                        mimetype=mimetypes[suffix],
         | 
| 99 | 
            +
                        headers={"Content-disposition": "attachment"},
         | 
| 100 | 
            +
                    )
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                return blueprint
         | 
| 103 | 
            +
             | 
| 104 | 
            +
             | 
| 105 | 
            +
            def _get_explored_geojson(tile_visits: list[tuple[int, int]], zoom: int) -> str:
         | 
| 106 | 
            +
                return geojson.dumps(
         | 
| 107 | 
            +
                    geojson.FeatureCollection(
         | 
| 108 | 
            +
                        features=[
         | 
| 109 | 
            +
                            make_explorer_tile(
         | 
| 110 | 
            +
                                tile_x,
         | 
| 111 | 
            +
                                tile_y,
         | 
| 112 | 
            +
                                {},
         | 
| 113 | 
            +
                                zoom,
         | 
| 114 | 
            +
                            )
         | 
| 115 | 
            +
                            for tile_x, tile_y in tile_visits
         | 
| 116 | 
            +
                        ]
         | 
| 117 | 
            +
                    )
         | 
| 118 | 
            +
                )
         | 
| @@ -1,10 +1,10 @@ | |
| 1 1 | 
             
            import collections
         | 
| 2 2 | 
             
            import datetime
         | 
| 3 | 
            -
            import functools
         | 
| 4 | 
            -
            from typing import Optional
         | 
| 5 3 |  | 
| 6 4 | 
             
            import altair as alt
         | 
| 7 5 | 
             
            import pandas as pd
         | 
| 6 | 
            +
            from flask import Blueprint
         | 
| 7 | 
            +
            from flask import render_template
         | 
| 8 8 |  | 
| 9 9 | 
             
            from geo_activity_playground.core.activities import ActivityRepository
         | 
| 10 10 | 
             
            from geo_activity_playground.core.activities import make_geojson_from_time_series
         | 
| @@ -12,15 +12,13 @@ from geo_activity_playground.core.config import Config | |
| 12 12 | 
             
            from geo_activity_playground.webui.plot_util import make_kind_scale
         | 
| 13 13 |  | 
| 14 14 |  | 
| 15 | 
            -
             | 
| 16 | 
            -
                 | 
| 17 | 
            -
                    self._repository = repository
         | 
| 18 | 
            -
                    self._config = config
         | 
| 15 | 
            +
            def make_summary_blueprint(repository: ActivityRepository, config: Config) -> Blueprint:
         | 
| 16 | 
            +
                blueprint = Blueprint("summary", __name__, template_folder="templates")
         | 
| 19 17 |  | 
| 20 | 
            -
                @ | 
| 21 | 
            -
                def  | 
| 22 | 
            -
                    kind_scale = make_kind_scale( | 
| 23 | 
            -
                    df = embellished_activities( | 
| 18 | 
            +
                @blueprint.route("/")
         | 
| 19 | 
            +
                def index():
         | 
| 20 | 
            +
                    kind_scale = make_kind_scale(repository.meta, config)
         | 
| 21 | 
            +
                    df = embellished_activities(repository.meta)
         | 
| 24 22 | 
             
                    # df = df.loc[df["consider_for_achievements"]]
         | 
| 25 23 |  | 
| 26 24 | 
             
                    year_kind_total = (
         | 
| @@ -30,28 +28,29 @@ class SummaryController: | |
| 30 28 | 
             
                        .reset_index()
         | 
| 31 29 | 
             
                    )
         | 
| 32 30 |  | 
| 33 | 
            -
                    return  | 
| 34 | 
            -
                        " | 
| 35 | 
            -
                         | 
| 36 | 
            -
                         | 
| 37 | 
            -
                         | 
| 38 | 
            -
                         | 
| 31 | 
            +
                    return render_template(
         | 
| 32 | 
            +
                        "summary/index.html.j2",
         | 
| 33 | 
            +
                        plot_distance_heatmaps=plot_distance_heatmaps(df, config),
         | 
| 34 | 
            +
                        plot_monthly_distance=plot_monthly_distance(df, kind_scale),
         | 
| 35 | 
            +
                        plot_yearly_distance=plot_yearly_distance(year_kind_total, kind_scale),
         | 
| 36 | 
            +
                        plot_year_cumulative=plot_year_cumulative(df),
         | 
| 37 | 
            +
                        tabulate_year_kind_mean=tabulate_year_kind_mean(df)
         | 
| 39 38 | 
             
                        .reset_index()
         | 
| 40 39 | 
             
                        .to_dict(orient="split"),
         | 
| 41 | 
            -
                         | 
| 42 | 
            -
                         | 
| 40 | 
            +
                        plot_weekly_distance=plot_weekly_distance(df, kind_scale),
         | 
| 41 | 
            +
                        nominations=[
         | 
| 43 42 | 
             
                            (
         | 
| 44 | 
            -
                                 | 
| 43 | 
            +
                                repository.get_activity_by_id(activity_id),
         | 
| 45 44 | 
             
                                reasons,
         | 
| 46 45 | 
             
                                make_geojson_from_time_series(
         | 
| 47 | 
            -
                                     | 
| 46 | 
            +
                                    repository.get_time_series(activity_id)
         | 
| 48 47 | 
             
                                ),
         | 
| 49 48 | 
             
                            )
         | 
| 50 | 
            -
                            for activity_id, reasons in nominate_activities(
         | 
| 51 | 
            -
                                self._repository.meta
         | 
| 52 | 
            -
                            ).items()
         | 
| 49 | 
            +
                            for activity_id, reasons in nominate_activities(repository.meta).items()
         | 
| 53 50 | 
             
                        ],
         | 
| 54 | 
            -
                     | 
| 51 | 
            +
                    )
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                return blueprint
         | 
| 55 54 |  | 
| 56 55 |  | 
| 57 56 | 
             
            def nominate_activities(meta: pd.DataFrame) -> dict[int, list[str]]:
         | 
| @@ -97,6 +97,17 @@ | |
| 97 97 | 
             
                                                    Tiles (Zoom 14)</a></li>
         | 
| 98 98 | 
             
                                            <li><a class="dropdown-item" href="{{ url_for('explorer.map', zoom=17) }}">Squadratinhos
         | 
| 99 99 | 
             
                                                    (Zoom 17)</a></li>
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                                            <li>
         | 
| 102 | 
            +
                                                <hr class="dropdown-divider">
         | 
| 103 | 
            +
                                            </li>
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                                            <li><a class="dropdown-item"
         | 
| 106 | 
            +
                                                    href="{{ url_for('square_planner.landing', zoom=14) }}">Square Planner
         | 
| 107 | 
            +
                                                    (Zoom 14)</a></li>
         | 
| 108 | 
            +
                                            <li><a class="dropdown-item"
         | 
| 109 | 
            +
                                                    href="{{ url_for('square_planner.landing', zoom=17) }}">Square Planner
         | 
| 110 | 
            +
                                                    (Zoom 17)</a></li>
         | 
| 100 111 | 
             
                                        </ul>
         | 
| 101 112 | 
             
                                    </li>
         | 
| 102 113 |  | 
| @@ -0,0 +1,42 @@ | |
| 1 | 
            +
            import io
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            import matplotlib.pyplot as pl
         | 
| 4 | 
            +
            import numpy as np
         | 
| 5 | 
            +
            from flask import Blueprint
         | 
| 6 | 
            +
            from flask import Response
         | 
| 7 | 
            +
             | 
| 8 | 
            +
            from geo_activity_playground.core.config import Config
         | 
| 9 | 
            +
            from geo_activity_playground.core.raster_map import get_tile
         | 
| 10 | 
            +
             | 
| 11 | 
            +
             | 
| 12 | 
            +
            def make_tile_blueprint(config: Config) -> Blueprint:
         | 
| 13 | 
            +
                blueprint = Blueprint("tiles", __name__, template_folder="templates")
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                @blueprint.route("/color/<int:z>/<int:x>/<int:y>.png")
         | 
| 16 | 
            +
                def tile_color(x: int, y: int, z: int):
         | 
| 17 | 
            +
                    map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
         | 
| 18 | 
            +
                    f = io.BytesIO()
         | 
| 19 | 
            +
                    pl.imsave(f, map_tile, format="png")
         | 
| 20 | 
            +
                    return Response(bytes(f.getbuffer()), mimetype="image/png")
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                @blueprint.route("/grayscale/<int:z>/<int:x>/<int:y>.png")
         | 
| 23 | 
            +
                def tile_grayscale(x: int, y: int, z: int):
         | 
| 24 | 
            +
                    map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
         | 
| 25 | 
            +
                    map_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2)  # to grayscale
         | 
| 26 | 
            +
                    map_tile = np.dstack((map_tile, map_tile, map_tile))  # to rgb
         | 
| 27 | 
            +
                    f = io.BytesIO()
         | 
| 28 | 
            +
                    pl.imsave(f, map_tile, format="png")
         | 
| 29 | 
            +
                    return Response(bytes(f.getbuffer()), mimetype="image/png")
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                @blueprint.route("/pastel/<int:z>/<int:x>/<int:y>.png")
         | 
| 32 | 
            +
                def tile_pastel(x: int, y: int, z: int):
         | 
| 33 | 
            +
                    map_tile = np.array(get_tile(z, x, y, config.map_tile_url)) / 255
         | 
| 34 | 
            +
                    averaged_tile = np.sum(map_tile * [0.2126, 0.7152, 0.0722], axis=2)
         | 
| 35 | 
            +
                    grayscale_tile = np.dstack((averaged_tile, averaged_tile, averaged_tile))
         | 
| 36 | 
            +
                    factor = 0.7
         | 
| 37 | 
            +
                    pastel_tile = factor * grayscale_tile + (1 - factor) * map_tile
         | 
| 38 | 
            +
                    f = io.BytesIO()
         | 
| 39 | 
            +
                    pl.imsave(f, pastel_tile, format="png")
         | 
| 40 | 
            +
                    return Response(bytes(f.getbuffer()), mimetype="image/png")
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                return blueprint
         | 
| @@ -108,9 +108,7 @@ def scan_for_activities( | |
| 108 108 | 
             
                skip_strava: bool = False,
         | 
| 109 109 | 
             
            ) -> None:
         | 
| 110 110 | 
             
                if pathlib.Path("Activities").exists():
         | 
| 111 | 
            -
                    import_from_directory(
         | 
| 112 | 
            -
                        config.metadata_extraction_regexes, config.num_processes, config
         | 
| 113 | 
            -
                    )
         | 
| 111 | 
            +
                    import_from_directory(config.metadata_extraction_regexes, config)
         | 
| 114 112 | 
             
                if pathlib.Path("Strava Export").exists():
         | 
| 115 113 | 
             
                    import_from_strava_checkout()
         | 
| 116 114 | 
             
                if config.strava_client_code and not skip_strava:
         |