ssb-sgis 1.0.0__py3-none-any.whl → 1.0.2__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.
- sgis/__init__.py +97 -115
- sgis/exceptions.py +3 -1
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +75 -38
- sgis/geopandas_tools/buffer_dissolve_explode.py +38 -34
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +149 -101
- sgis/geopandas_tools/duplicates.py +31 -17
- sgis/geopandas_tools/general.py +76 -48
- sgis/geopandas_tools/geometry_types.py +21 -7
- sgis/geopandas_tools/neighbors.py +20 -8
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +9 -8
- sgis/geopandas_tools/polygon_operations.py +48 -56
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +14 -14
- sgis/helpers.py +114 -56
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +39 -26
- sgis/maps/explore.py +112 -66
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +124 -65
- sgis/maps/map.py +66 -41
- sgis/maps/maps.py +31 -29
- sgis/maps/thematicmap.py +46 -33
- sgis/maps/tilesources.py +3 -8
- sgis/networkanalysis/_get_route.py +5 -4
- sgis/networkanalysis/_od_cost_matrix.py +44 -1
- sgis/networkanalysis/_points.py +10 -4
- sgis/networkanalysis/_service_area.py +5 -2
- sgis/networkanalysis/closing_network_holes.py +20 -62
- sgis/networkanalysis/cutting_lines.py +55 -43
- sgis/networkanalysis/directednetwork.py +15 -7
- sgis/networkanalysis/finding_isolated_networks.py +4 -3
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +72 -54
- sgis/networkanalysis/networkanalysisrules.py +20 -16
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +5 -2
- sgis/parallel/parallel.py +337 -127
- sgis/raster/__init__.py +6 -0
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +280 -208
- sgis/raster/cubebase.py +15 -29
- sgis/raster/indices.py +3 -7
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/raster.py +313 -127
- sgis/raster/torchgeo.py +58 -37
- sgis/raster/zonal.py +38 -13
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/METADATA +89 -18
- ssb_sgis-1.0.2.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.0.dist-info → ssb_sgis-1.0.2.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- ssb_sgis-1.0.0.dist-info/RECORD +0 -63
sgis/io/dapla_functions.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
"""Functions for reading and writing GeoDataFrames in Statistics Norway's GCS Dapla.
|
|
2
|
-
"""
|
|
1
|
+
"""Functions for reading and writing GeoDataFrames in Statistics Norway's GCS Dapla."""
|
|
3
2
|
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
from typing import Optional
|
|
6
4
|
|
|
7
5
|
import dapla as dp
|
|
8
6
|
import geopandas as gpd
|
|
7
|
+
import joblib
|
|
9
8
|
import pandas as pd
|
|
10
9
|
from geopandas import GeoDataFrame
|
|
11
10
|
from geopandas.io.arrow import _geopandas_to_arrow
|
|
@@ -14,9 +13,9 @@ from pyarrow import parquet
|
|
|
14
13
|
|
|
15
14
|
|
|
16
15
|
def read_geopandas(
|
|
17
|
-
gcs_path: str | Path,
|
|
16
|
+
gcs_path: str | Path | list[str | Path],
|
|
18
17
|
pandas_fallback: bool = False,
|
|
19
|
-
file_system:
|
|
18
|
+
file_system: dp.gcs.GCSFileSystem | None = None,
|
|
20
19
|
**kwargs,
|
|
21
20
|
) -> GeoDataFrame | DataFrame:
|
|
22
21
|
"""Reads geoparquet or other geodata from a file on GCS.
|
|
@@ -28,25 +27,35 @@ def read_geopandas(
|
|
|
28
27
|
Does not currently read shapefiles or filegeodatabases.
|
|
29
28
|
|
|
30
29
|
Args:
|
|
31
|
-
gcs_path: path to
|
|
30
|
+
gcs_path: path to one or more files on Google Cloud Storage.
|
|
31
|
+
Multiple paths are read with threading.
|
|
32
32
|
pandas_fallback: If False (default), an exception is raised if the file can
|
|
33
33
|
not be read with geopandas and the number of rows is more than 0. If True,
|
|
34
|
-
the file will be read
|
|
34
|
+
the file will be read with pandas if geopandas fails.
|
|
35
|
+
file_system: Optional file system.
|
|
35
36
|
**kwargs: Additional keyword arguments passed to geopandas' read_parquet
|
|
36
37
|
or read_file, depending on the file type.
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Returns:
|
|
39
40
|
A GeoDataFrame if it has rows. If zero rows, a pandas DataFrame is returned.
|
|
40
41
|
"""
|
|
42
|
+
if file_system is None:
|
|
43
|
+
file_system = dp.FileClient.get_gcs_file_system()
|
|
44
|
+
|
|
45
|
+
if isinstance(gcs_path, (list, tuple)):
|
|
46
|
+
kwargs |= {"file_system": file_system, "pandas_fallback": pandas_fallback}
|
|
47
|
+
# recursive read with threads
|
|
48
|
+
with joblib.Parallel(n_jobs=len(gcs_path), backend="threading") as parallel:
|
|
49
|
+
dfs: list[GeoDataFrame] = parallel(
|
|
50
|
+
joblib.delayed(read_geopandas)(x, **kwargs) for x in gcs_path
|
|
51
|
+
)
|
|
52
|
+
return pd.concat(dfs)
|
|
41
53
|
|
|
42
54
|
if not isinstance(gcs_path, str):
|
|
43
55
|
try:
|
|
44
56
|
gcs_path = str(gcs_path)
|
|
45
|
-
except TypeError:
|
|
46
|
-
raise TypeError(f"Unexpected type {type(gcs_path)}.")
|
|
47
|
-
|
|
48
|
-
if file_system is None:
|
|
49
|
-
file_system = dp.FileClient.get_gcs_file_system()
|
|
57
|
+
except TypeError as e:
|
|
58
|
+
raise TypeError(f"Unexpected type {type(gcs_path)}.") from e
|
|
50
59
|
|
|
51
60
|
if "parquet" in gcs_path or "prqt" in gcs_path:
|
|
52
61
|
with file_system.open(gcs_path, mode="rb") as file:
|
|
@@ -77,11 +86,11 @@ def read_geopandas(
|
|
|
77
86
|
|
|
78
87
|
|
|
79
88
|
def write_geopandas(
|
|
80
|
-
df:
|
|
89
|
+
df: GeoDataFrame,
|
|
81
90
|
gcs_path: str | Path,
|
|
82
91
|
overwrite: bool = True,
|
|
83
92
|
pandas_fallback: bool = False,
|
|
84
|
-
file_system:
|
|
93
|
+
file_system: dp.gcs.GCSFileSystem | None = None,
|
|
85
94
|
**kwargs,
|
|
86
95
|
) -> None:
|
|
87
96
|
"""Writes a GeoDataFrame to the speficied format.
|
|
@@ -93,10 +102,13 @@ def write_geopandas(
|
|
|
93
102
|
df: The GeoDataFrame to write.
|
|
94
103
|
gcs_path: The path to the file you want to write to.
|
|
95
104
|
overwrite: Whether to overwrite the file if it exists. Defaults to True.
|
|
105
|
+
pandas_fallback: If False (default), an exception is raised if the file can
|
|
106
|
+
not be written with geopandas and the number of rows is more than 0. If True,
|
|
107
|
+
the file will be written without geo-metadata if >0 rows.
|
|
108
|
+
file_system: Optional file sustem.
|
|
96
109
|
**kwargs: Additional keyword arguments passed to parquet.write_table
|
|
97
110
|
(for parquet) or geopandas' to_file method (if not parquet).
|
|
98
111
|
"""
|
|
99
|
-
|
|
100
112
|
if not isinstance(gcs_path, str):
|
|
101
113
|
try:
|
|
102
114
|
gcs_path = str(gcs_path)
|
|
@@ -109,7 +121,8 @@ def write_geopandas(
|
|
|
109
121
|
if file_system is None:
|
|
110
122
|
file_system = dp.FileClient.get_gcs_file_system()
|
|
111
123
|
|
|
112
|
-
|
|
124
|
+
if not isinstance(df, GeoDataFrame):
|
|
125
|
+
raise ValueError("DataFrame must be GeoDataFrame.")
|
|
113
126
|
|
|
114
127
|
if not len(df):
|
|
115
128
|
if pandas_fallback:
|
|
@@ -152,7 +165,6 @@ def exists(path: str | Path) -> bool:
|
|
|
152
165
|
Returns:
|
|
153
166
|
True if the path exists, False if not.
|
|
154
167
|
"""
|
|
155
|
-
|
|
156
168
|
file_system = dp.FileClient.get_gcs_file_system()
|
|
157
169
|
return file_system.exists(path)
|
|
158
170
|
|
|
@@ -185,7 +197,7 @@ def check_files(
|
|
|
185
197
|
]
|
|
186
198
|
folderinfo = [x["name"] for x in info if x["storageClass"] == "DIRECTORY"]
|
|
187
199
|
|
|
188
|
-
fileinfo +=
|
|
200
|
+
fileinfo += _get_files_in_subfolders(folderinfo)
|
|
189
201
|
|
|
190
202
|
df = pd.DataFrame(fileinfo, columns=["path", "kb", "updated"])
|
|
191
203
|
|
|
@@ -224,12 +236,9 @@ def check_files(
|
|
|
224
236
|
return df.loc[lambda x: x.index > the_time, ["kb", "mb", "name", "child", "path"]]
|
|
225
237
|
|
|
226
238
|
|
|
227
|
-
def
|
|
239
|
+
def _get_files_in_subfolders(folderinfo: list[dict]) -> list[tuple]:
|
|
228
240
|
file_system = dp.FileClient.get_gcs_file_system()
|
|
229
241
|
|
|
230
|
-
if isinstance(folderinfo, (str, Path)):
|
|
231
|
-
folderinfo = [folderinfo]
|
|
232
|
-
|
|
233
242
|
fileinfo = []
|
|
234
243
|
|
|
235
244
|
while folderinfo:
|
sgis/io/opener.py
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
+
from collections.abc import Generator
|
|
1
2
|
from contextlib import contextmanager
|
|
2
|
-
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
try:
|
|
5
|
-
|
|
6
|
+
from dapla import FileClient
|
|
7
|
+
from dapla.gcs import GCSFileSystem
|
|
6
8
|
except ImportError:
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
class GCSFileSystem: # type: ignore[no-redef]
|
|
11
|
+
"""Placeholder."""
|
|
12
|
+
|
|
8
13
|
|
|
9
14
|
from ._is_dapla import is_dapla
|
|
10
15
|
|
|
11
16
|
|
|
12
17
|
@contextmanager
|
|
13
|
-
def opener(
|
|
18
|
+
def opener(
|
|
19
|
+
path, mode: str = "rb", file_system: GCSFileSystem | None = None
|
|
20
|
+
) -> Generator[str | Any, None, None]:
|
|
14
21
|
"""Yields a gcs buffer if in Dapla, otherwise yields the path.
|
|
15
22
|
|
|
16
|
-
Example
|
|
23
|
+
Example:
|
|
17
24
|
-------
|
|
18
25
|
>>> with opener(path) as file:
|
|
19
26
|
>>> with rasterio.open(file) as src:
|
|
@@ -21,7 +28,7 @@ def opener(path, mode="rb", file_system=None):
|
|
|
21
28
|
"""
|
|
22
29
|
if is_dapla():
|
|
23
30
|
if file_system is None:
|
|
24
|
-
file_system =
|
|
31
|
+
file_system = FileClient.get_gcs_file_system()
|
|
25
32
|
yield file_system.open(str(path), mode=mode)
|
|
26
33
|
else:
|
|
27
34
|
yield str(path)
|
sgis/io/read_parquet.py
CHANGED
|
@@ -14,7 +14,7 @@ def read_parquet_url(url: str) -> GeoDataFrame:
|
|
|
14
14
|
Returns:
|
|
15
15
|
A GeoDataFrame.
|
|
16
16
|
|
|
17
|
-
Examples
|
|
17
|
+
Examples:
|
|
18
18
|
--------
|
|
19
19
|
>>> from sgis import read_parquet_url
|
|
20
20
|
>>> url = "https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/points_oslo.parquet"
|
sgis/maps/examine.py
CHANGED
|
@@ -4,7 +4,9 @@ import numpy as np
|
|
|
4
4
|
from ..geopandas_tools.bounds import get_total_bounds
|
|
5
5
|
from ..helpers import unit_is_degrees
|
|
6
6
|
from .map import Map
|
|
7
|
-
from .maps import clipmap
|
|
7
|
+
from .maps import clipmap
|
|
8
|
+
from .maps import explore
|
|
9
|
+
from .maps import samplemap
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class Examine:
|
|
@@ -19,19 +21,7 @@ class Examine:
|
|
|
19
21
|
first geometry in 'mask_gdf' (or the first speficied gdf). The 'next' method
|
|
20
22
|
can then be repeated.
|
|
21
23
|
|
|
22
|
-
|
|
23
|
-
*gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
|
|
24
|
-
will be used as masks, unless 'mask_gdf' is specified.
|
|
25
|
-
column: Column to use as colors.
|
|
26
|
-
mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
|
|
27
|
-
of mask_gdf will not be shown.
|
|
28
|
-
size: Number of meters (or other crs unit) to buffer the mask geometry
|
|
29
|
-
before clipping.
|
|
30
|
-
sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
|
|
31
|
-
will be iterated through from the top.
|
|
32
|
-
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
33
|
-
|
|
34
|
-
Examples
|
|
24
|
+
Examples:
|
|
35
25
|
--------
|
|
36
26
|
Create the examiner.
|
|
37
27
|
|
|
@@ -78,7 +68,24 @@ class Examine:
|
|
|
78
68
|
size: int | float = 1000,
|
|
79
69
|
only_show_mask: bool = True,
|
|
80
70
|
**kwargs,
|
|
81
|
-
):
|
|
71
|
+
) -> None:
|
|
72
|
+
"""Initialiser.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
*gdfs: One or more GeoDataFrames. The rows of the first GeoDataFrame
|
|
76
|
+
will be used as masks, unless 'mask_gdf' is specified.
|
|
77
|
+
column: Column to use as colors.
|
|
78
|
+
mask_gdf: Optional GeoDataFrame to use as mask iterator. The geometries
|
|
79
|
+
of mask_gdf will not be shown.
|
|
80
|
+
size: Number of meters (or other crs unit) to buffer the mask geometry
|
|
81
|
+
before clipping.
|
|
82
|
+
sort_values: Optional sorting column(s) of the mask GeoDataFrame. Rows
|
|
83
|
+
will be iterated through from the top.
|
|
84
|
+
only_show_mask: If True (default), show only the mask GeoDataFrame by default.
|
|
85
|
+
The other layers can be toggled on.
|
|
86
|
+
**kwargs: Additional keyword arguments passed to sgis.clipmap.
|
|
87
|
+
|
|
88
|
+
"""
|
|
82
89
|
gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
|
|
83
90
|
|
|
84
91
|
if mask_gdf is None:
|
|
@@ -87,7 +94,9 @@ class Examine:
|
|
|
87
94
|
self.mask_gdf = mask_gdf
|
|
88
95
|
|
|
89
96
|
m = Map(*gdfs, column=column, **kwargs)
|
|
90
|
-
self._gdfs: dict[str, gpd.GeoDataFrame] = dict(
|
|
97
|
+
self._gdfs: dict[str, gpd.GeoDataFrame] = dict(
|
|
98
|
+
zip(m.labels, m.gdfs, strict=False)
|
|
99
|
+
)
|
|
91
100
|
|
|
92
101
|
self.indices = list(range(len(self.mask_gdf)))
|
|
93
102
|
self.i = 0
|
|
@@ -121,7 +130,7 @@ class Examine:
|
|
|
121
130
|
elif not kwargs.get("show", True):
|
|
122
131
|
self.kwargs["show"] = [False] * len(self._gdfs)
|
|
123
132
|
|
|
124
|
-
def next(self, i: int | None = None, **kwargs):
|
|
133
|
+
def next(self, i: int | None = None, **kwargs) -> None:
|
|
125
134
|
"""Displays a map of geometries within the next row of the mask gdf.
|
|
126
135
|
|
|
127
136
|
Args:
|
|
@@ -151,7 +160,7 @@ class Examine:
|
|
|
151
160
|
)
|
|
152
161
|
self.i += 1
|
|
153
162
|
|
|
154
|
-
def sample(self, **kwargs):
|
|
163
|
+
def sample(self, **kwargs) -> None:
|
|
155
164
|
"""Takes a sample index of the mask and displays a map of this area.
|
|
156
165
|
|
|
157
166
|
Args:
|
|
@@ -171,7 +180,7 @@ class Examine:
|
|
|
171
180
|
**self.kwargs,
|
|
172
181
|
)
|
|
173
182
|
|
|
174
|
-
def current(self, i: int | None = None, **kwargs):
|
|
183
|
+
def current(self, i: int | None = None, **kwargs) -> None:
|
|
175
184
|
"""Repeat the last shown map."""
|
|
176
185
|
if kwargs:
|
|
177
186
|
kwargs = self._fix_kwargs(kwargs)
|
|
@@ -190,7 +199,7 @@ class Examine:
|
|
|
190
199
|
**self.kwargs,
|
|
191
200
|
)
|
|
192
201
|
|
|
193
|
-
def explore(self, **kwargs):
|
|
202
|
+
def explore(self, **kwargs) -> None:
|
|
194
203
|
"""Show all rows like the function explore."""
|
|
195
204
|
if kwargs:
|
|
196
205
|
kwargs = self._fix_kwargs(kwargs)
|
|
@@ -202,7 +211,7 @@ class Examine:
|
|
|
202
211
|
**self.kwargs,
|
|
203
212
|
)
|
|
204
213
|
|
|
205
|
-
def clipmap(self, **kwargs):
|
|
214
|
+
def clipmap(self, **kwargs) -> None:
|
|
206
215
|
"""Show all rows like the function clipmap."""
|
|
207
216
|
if kwargs:
|
|
208
217
|
kwargs = self._fix_kwargs(kwargs)
|
|
@@ -214,7 +223,7 @@ class Examine:
|
|
|
214
223
|
**self.kwargs,
|
|
215
224
|
)
|
|
216
225
|
|
|
217
|
-
def samplemap(self, **kwargs):
|
|
226
|
+
def samplemap(self, **kwargs) -> None:
|
|
218
227
|
"""Show all rows like the function samplemap."""
|
|
219
228
|
if kwargs:
|
|
220
229
|
kwargs = self._fix_kwargs(kwargs)
|
|
@@ -241,21 +250,25 @@ class Examine:
|
|
|
241
250
|
return gdfs
|
|
242
251
|
|
|
243
252
|
@property
|
|
244
|
-
def bounds(self):
|
|
253
|
+
def bounds(self) -> tuple[float, float, float, float]:
|
|
254
|
+
"""Total bounds of all GeoDataFrames."""
|
|
245
255
|
return get_total_bounds(*list(self.gdfs.values()))
|
|
246
256
|
|
|
247
|
-
def _fix_kwargs(self, kwargs) -> dict:
|
|
257
|
+
def _fix_kwargs(self, kwargs: dict) -> dict:
|
|
248
258
|
self.size = kwargs.pop("size", self.size)
|
|
249
259
|
self.column = kwargs.pop("column", self.column)
|
|
250
260
|
return kwargs
|
|
251
261
|
|
|
252
262
|
def __repr__(self) -> str:
|
|
263
|
+
"""Representation."""
|
|
253
264
|
return f"{self.__class__.__name__}(indices={len(self.indices)}, current={self.i}, n_gdfs={len(self._gdfs)})"
|
|
254
265
|
|
|
255
|
-
def __add__(self, scalar):
|
|
266
|
+
def __add__(self, scalar: int) -> "Examine":
|
|
267
|
+
"""Add a number to the index."""
|
|
256
268
|
self.i += scalar
|
|
257
269
|
return self
|
|
258
270
|
|
|
259
|
-
def __sub__(self, scalar):
|
|
271
|
+
def __sub__(self, scalar: int) -> "Examine":
|
|
272
|
+
"""Subtract a number from the index."""
|
|
260
273
|
self.i -= scalar
|
|
261
274
|
return self
|
sgis/maps/explore.py
CHANGED
|
@@ -9,6 +9,8 @@ import warnings
|
|
|
9
9
|
from collections.abc import Iterable
|
|
10
10
|
from numbers import Number
|
|
11
11
|
from statistics import mean
|
|
12
|
+
from typing import Any
|
|
13
|
+
from typing import ClassVar
|
|
12
14
|
|
|
13
15
|
import branca as bc
|
|
14
16
|
import folium
|
|
@@ -17,27 +19,30 @@ import numpy as np
|
|
|
17
19
|
import pandas as pd
|
|
18
20
|
import xyzservices
|
|
19
21
|
from folium import plugins
|
|
20
|
-
from geopandas import GeoDataFrame
|
|
22
|
+
from geopandas import GeoDataFrame
|
|
23
|
+
from geopandas import GeoSeries
|
|
21
24
|
from IPython.display import display
|
|
22
25
|
from jinja2 import Template
|
|
23
26
|
from pandas.api.types import is_datetime64_any_dtype
|
|
24
27
|
from shapely import Geometry
|
|
25
28
|
from shapely.geometry import LineString
|
|
26
29
|
|
|
27
|
-
from ..geopandas_tools.conversion import
|
|
28
|
-
from ..geopandas_tools.general import clean_geoms
|
|
29
|
-
from ..geopandas_tools.
|
|
30
|
+
from ..geopandas_tools.conversion import to_gdf
|
|
31
|
+
from ..geopandas_tools.general import clean_geoms
|
|
32
|
+
from ..geopandas_tools.general import make_all_singlepart
|
|
33
|
+
from ..geopandas_tools.geometry_types import get_geom_type
|
|
34
|
+
from ..geopandas_tools.geometry_types import to_single_geom_type
|
|
30
35
|
from .httpserver import run_html_server
|
|
31
36
|
from .map import Map
|
|
32
|
-
from .tilesources import kartverket
|
|
33
|
-
|
|
37
|
+
from .tilesources import kartverket
|
|
38
|
+
from .tilesources import xyz
|
|
34
39
|
|
|
35
40
|
try:
|
|
36
41
|
from torchgeo.datasets.geo import RasterDataset
|
|
37
42
|
except ImportError:
|
|
38
43
|
|
|
39
44
|
class RasterDataset:
|
|
40
|
-
"""Placeholder"""
|
|
45
|
+
"""Placeholder."""
|
|
41
46
|
|
|
42
47
|
|
|
43
48
|
# the geopandas._explore raises a deprication warning. Ignoring for now.
|
|
@@ -96,7 +101,10 @@ class MeasureControlFix(plugins.MeasureControl):
|
|
|
96
101
|
"""
|
|
97
102
|
)
|
|
98
103
|
|
|
99
|
-
def __init__(
|
|
104
|
+
def __init__(
|
|
105
|
+
self, active_color: str = "red", completed_color: str = "red", **kwargs
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Run super __init__ after the new _template class attribute is made."""
|
|
100
108
|
super().__init__(
|
|
101
109
|
active_color=active_color, completed_color=completed_color, **kwargs
|
|
102
110
|
)
|
|
@@ -146,8 +154,10 @@ def to_tile(tile: str | xyzservices.TileProvider, max_zoom: int) -> folium.TileL
|
|
|
146
154
|
|
|
147
155
|
|
|
148
156
|
class Explore(Map):
|
|
157
|
+
"""Class for displaying and saving html maps of multiple GeoDataFrames."""
|
|
158
|
+
|
|
149
159
|
# class attribute that can be overridden locally
|
|
150
|
-
tiles = (
|
|
160
|
+
tiles: ClassVar[tuple[str]] = (
|
|
151
161
|
"grunnkart",
|
|
152
162
|
"norge_i_bilder",
|
|
153
163
|
"dark",
|
|
@@ -161,17 +171,41 @@ class Explore(Map):
|
|
|
161
171
|
column: str | None = None,
|
|
162
172
|
popup: bool = True,
|
|
163
173
|
max_zoom: int = 40,
|
|
164
|
-
smooth_factor: float = 1.
|
|
174
|
+
smooth_factor: float = 1.1,
|
|
165
175
|
browser: bool = False,
|
|
166
176
|
prefer_canvas: bool = True,
|
|
167
177
|
measure_control: bool = True,
|
|
168
178
|
geocoder: bool = False,
|
|
169
|
-
save=None,
|
|
179
|
+
save: str | None = None,
|
|
170
180
|
show: bool | Iterable[bool] | None = None,
|
|
171
181
|
text: str | None = None,
|
|
172
182
|
decimals: int = 6,
|
|
173
183
|
**kwargs,
|
|
174
|
-
):
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Initialiser.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
*gdfs: One or more GeoDataFrames.
|
|
189
|
+
mask: Optional mask to clip to.
|
|
190
|
+
column: Optional column to color the data by.
|
|
191
|
+
popup: Whether to make the data popups clickable.
|
|
192
|
+
max_zoom: Max levels of zoom.
|
|
193
|
+
smooth_factor: Float of 1 or higher, 1 meaning no smoothing
|
|
194
|
+
of lines.
|
|
195
|
+
browser: Whether to open the map in a browser tab.
|
|
196
|
+
prefer_canvas: Option.
|
|
197
|
+
measure_control: Whether to include measurement box.
|
|
198
|
+
geocoder: Whether to include search bar for addresses.
|
|
199
|
+
save: Optional file path to an html file. The map will then
|
|
200
|
+
be saved instead of displayed.
|
|
201
|
+
show: Whether to show or hide the data upon creating the map.
|
|
202
|
+
If False, the data can be toggled on later. 'show' can also be
|
|
203
|
+
a sequence of boolean values the same length as the number of
|
|
204
|
+
GeoDataFrames.
|
|
205
|
+
text: Optional text for a text box in the map.
|
|
206
|
+
decimals: Number of decimals in the coordinates.
|
|
207
|
+
**kwargs: Additional keyword arguments passed to
|
|
208
|
+
"""
|
|
175
209
|
self.popup = popup
|
|
176
210
|
self.max_zoom = max_zoom
|
|
177
211
|
self.smooth_factor = smooth_factor
|
|
@@ -195,12 +229,12 @@ class Explore(Map):
|
|
|
195
229
|
else:
|
|
196
230
|
show_was_none = False
|
|
197
231
|
|
|
198
|
-
self.raster_datasets = tuple(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
self.tiles # += self.raster_datasets
|
|
232
|
+
self.raster_datasets = [] # tuple(
|
|
233
|
+
# raster_dataset_to_background_map(x)
|
|
234
|
+
# for x in gdfs
|
|
235
|
+
# if isinstance(x, RasterDataset)
|
|
236
|
+
# )
|
|
237
|
+
# self.tiles # += self.raster_datasets
|
|
204
238
|
|
|
205
239
|
super().__init__(*gdfs, column=column, show=show, **kwargs)
|
|
206
240
|
|
|
@@ -262,14 +296,20 @@ class Explore(Map):
|
|
|
262
296
|
|
|
263
297
|
self.original_crs = self.gdf.crs
|
|
264
298
|
|
|
265
|
-
def __repr__(self):
|
|
299
|
+
def __repr__(self) -> str:
|
|
300
|
+
"""Representation."""
|
|
266
301
|
return f"{self.__class__.__name__}()"
|
|
267
302
|
|
|
268
303
|
def explore(
|
|
269
|
-
self,
|
|
304
|
+
self,
|
|
305
|
+
column: str | None = None,
|
|
306
|
+
center: Any | None = None,
|
|
307
|
+
size: int | None = None,
|
|
308
|
+
**kwargs,
|
|
270
309
|
) -> None:
|
|
310
|
+
"""Explore all the data."""
|
|
271
311
|
if not any(len(gdf) for gdf in self._gdfs) and not len(self.raster_datasets):
|
|
272
|
-
warnings.warn("None of the GeoDataFrames have rows.")
|
|
312
|
+
warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
|
|
273
313
|
return
|
|
274
314
|
if column:
|
|
275
315
|
self._column = column
|
|
@@ -311,6 +351,7 @@ class Explore(Map):
|
|
|
311
351
|
sample_from_first: bool = True,
|
|
312
352
|
**kwargs,
|
|
313
353
|
) -> None:
|
|
354
|
+
"""Explore a sample of the data."""
|
|
314
355
|
if column:
|
|
315
356
|
self._column = column
|
|
316
357
|
self._update_column()
|
|
@@ -347,10 +388,11 @@ class Explore(Map):
|
|
|
347
388
|
|
|
348
389
|
def clipmap(
|
|
349
390
|
self,
|
|
350
|
-
mask,
|
|
391
|
+
mask: Any,
|
|
351
392
|
column: str | None = None,
|
|
352
393
|
**kwargs,
|
|
353
394
|
) -> None:
|
|
395
|
+
"""Explore the data within a mask extent."""
|
|
354
396
|
if column:
|
|
355
397
|
self._column = column
|
|
356
398
|
self._update_column()
|
|
@@ -368,7 +410,7 @@ class Explore(Map):
|
|
|
368
410
|
self._gdf = pd.concat(gdfs, ignore_index=True)
|
|
369
411
|
self._explore(**kwargs)
|
|
370
412
|
|
|
371
|
-
def _explore(self, **kwargs):
|
|
413
|
+
def _explore(self, **kwargs) -> None:
|
|
372
414
|
self.kwargs = self.kwargs | kwargs
|
|
373
415
|
|
|
374
416
|
if self._is_categorical:
|
|
@@ -384,7 +426,7 @@ class Explore(Map):
|
|
|
384
426
|
else:
|
|
385
427
|
display(self.map)
|
|
386
428
|
|
|
387
|
-
def _split_categories(self):
|
|
429
|
+
def _split_categories(self) -> None:
|
|
388
430
|
new_gdfs, new_labels, new_shows = [], [], []
|
|
389
431
|
for cat in self._unique_values:
|
|
390
432
|
gdf = self.gdf.loc[self.gdf[self.column] == cat]
|
|
@@ -396,7 +438,7 @@ class Explore(Map):
|
|
|
396
438
|
self.labels = new_labels
|
|
397
439
|
self.show = new_shows
|
|
398
440
|
|
|
399
|
-
def _to_single_geom_type(self, gdf) -> GeoDataFrame:
|
|
441
|
+
def _to_single_geom_type(self, gdf: GeoDataFrame) -> GeoDataFrame:
|
|
400
442
|
gdf = clean_geoms(gdf)
|
|
401
443
|
|
|
402
444
|
if get_geom_type(gdf) != "mixed":
|
|
@@ -420,16 +462,16 @@ class Explore(Map):
|
|
|
420
462
|
|
|
421
463
|
assert get_geom_type(gdf) != "mixed", gdf.geom_type.value_counts()
|
|
422
464
|
|
|
423
|
-
warnings.warn(mess)
|
|
465
|
+
warnings.warn(mess, stacklevel=1)
|
|
424
466
|
|
|
425
467
|
return gdf
|
|
426
468
|
|
|
427
|
-
def _update_column(self):
|
|
469
|
+
def _update_column(self) -> None:
|
|
428
470
|
self._is_categorical = self._check_if_categorical()
|
|
429
471
|
self._fillna_if_col_is_missing()
|
|
430
472
|
self._gdf = pd.concat(self._gdfs, ignore_index=True)
|
|
431
473
|
|
|
432
|
-
def _create_categorical_map(self):
|
|
474
|
+
def _create_categorical_map(self) -> None:
|
|
433
475
|
self._get_categorical_colors()
|
|
434
476
|
|
|
435
477
|
gdf = self._prepare_gdf_for_map(self._gdf)
|
|
@@ -478,7 +520,7 @@ class Explore(Map):
|
|
|
478
520
|
|
|
479
521
|
def _add_tiles(
|
|
480
522
|
self, mapobj: folium.Map, tiles: list[str, xyzservices.TileProvider]
|
|
481
|
-
):
|
|
523
|
+
) -> None:
|
|
482
524
|
for tile in tiles:
|
|
483
525
|
to_tile(tile, max_zoom=self.max_zoom).add_to(mapobj)
|
|
484
526
|
|
|
@@ -549,7 +591,7 @@ class Explore(Map):
|
|
|
549
591
|
return [col for col in gdf.columns if col not in COLS_TO_DROP]
|
|
550
592
|
|
|
551
593
|
@staticmethod
|
|
552
|
-
def _prepare_gdf_for_map(gdf):
|
|
594
|
+
def _prepare_gdf_for_map(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
553
595
|
if isinstance(gdf, GeoSeries):
|
|
554
596
|
gdf = gdf.to_frame("geometry")
|
|
555
597
|
|
|
@@ -567,13 +609,13 @@ class Explore(Map):
|
|
|
567
609
|
|
|
568
610
|
def _make_folium_map(
|
|
569
611
|
self,
|
|
570
|
-
bounds,
|
|
571
|
-
attr=None,
|
|
572
|
-
tiles=None,
|
|
573
|
-
width="100%",
|
|
574
|
-
height="100%",
|
|
575
|
-
control_scale=True,
|
|
576
|
-
map_kwds=None,
|
|
612
|
+
bounds: tuple[float, float, float, float],
|
|
613
|
+
attr: Any = None,
|
|
614
|
+
tiles: Any = None,
|
|
615
|
+
width: str = "100%",
|
|
616
|
+
height: str = "100%",
|
|
617
|
+
control_scale: bool = True,
|
|
618
|
+
map_kwds: dict | None = None,
|
|
577
619
|
**kwargs,
|
|
578
620
|
):
|
|
579
621
|
if not map_kwds:
|
|
@@ -679,21 +721,29 @@ class Explore(Map):
|
|
|
679
721
|
|
|
680
722
|
def _make_geojson(
|
|
681
723
|
self,
|
|
682
|
-
df,
|
|
724
|
+
df: GeoDataFrame,
|
|
683
725
|
show: bool,
|
|
684
|
-
color=None,
|
|
685
|
-
tooltip=True,
|
|
686
|
-
popup=False,
|
|
687
|
-
highlight=True,
|
|
688
|
-
marker_type=None,
|
|
689
|
-
marker_kwds=
|
|
690
|
-
style_kwds=
|
|
691
|
-
highlight_kwds=
|
|
692
|
-
tooltip_kwds=
|
|
693
|
-
popup_kwds=
|
|
694
|
-
map_kwds=
|
|
726
|
+
color: str | None = None,
|
|
727
|
+
tooltip: bool = True,
|
|
728
|
+
popup: bool = False,
|
|
729
|
+
highlight: bool = True,
|
|
730
|
+
marker_type: str | None = None,
|
|
731
|
+
marker_kwds: dict | None = None,
|
|
732
|
+
style_kwds: dict | None = None,
|
|
733
|
+
highlight_kwds: dict | None = None,
|
|
734
|
+
tooltip_kwds: dict | None = None,
|
|
735
|
+
popup_kwds: dict | None = None,
|
|
736
|
+
map_kwds: dict | None = None,
|
|
695
737
|
**kwargs,
|
|
696
|
-
):
|
|
738
|
+
) -> folium.GeoJson:
|
|
739
|
+
|
|
740
|
+
marker_kwds = marker_kwds or {}
|
|
741
|
+
style_kwds = style_kwds or {}
|
|
742
|
+
highlight_kwds = highlight_kwds or {}
|
|
743
|
+
tooltip_kwds = tooltip_kwds or {}
|
|
744
|
+
popup_kwds = popup_kwds or {}
|
|
745
|
+
map_kwds = map_kwds or {}
|
|
746
|
+
|
|
697
747
|
gdf = df.copy()
|
|
698
748
|
|
|
699
749
|
# convert LinearRing to LineString
|
|
@@ -816,9 +866,10 @@ class Explore(Map):
|
|
|
816
866
|
)
|
|
817
867
|
|
|
818
868
|
|
|
819
|
-
def _tooltip_popup(
|
|
820
|
-
|
|
821
|
-
|
|
869
|
+
def _tooltip_popup(
|
|
870
|
+
type_: str, fields: Any, gdf: GeoDataFrame, **kwargs
|
|
871
|
+
) -> folium.GeoJsonTooltip | folium.GeoJsonPopup:
|
|
872
|
+
"""Get tooltip or popup."""
|
|
822
873
|
# specify fields to show in the tooltip
|
|
823
874
|
if fields is False or fields is None or fields == 0:
|
|
824
875
|
return None
|
|
@@ -836,20 +887,16 @@ def _tooltip_popup(type, fields, gdf, **kwds):
|
|
|
836
887
|
|
|
837
888
|
# Cast fields to str
|
|
838
889
|
fields = list(map(str, fields))
|
|
839
|
-
if
|
|
840
|
-
return folium.GeoJsonTooltip(fields, **
|
|
841
|
-
elif
|
|
842
|
-
return folium.GeoJsonPopup(fields, **
|
|
843
|
-
|
|
890
|
+
if type_ == "tooltip":
|
|
891
|
+
return folium.GeoJsonTooltip(fields, **kwargs)
|
|
892
|
+
elif type_ == "popup":
|
|
893
|
+
return folium.GeoJsonPopup(fields, **kwargs)
|
|
844
894
|
|
|
845
|
-
def raster_dataset_to_background_map(dataset: RasterDataset):
|
|
846
|
-
crs = dataset.crs
|
|
847
|
-
bbox = dataset.bounds
|
|
848
895
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
Add categorical legend to a map
|
|
896
|
+
def _categorical_legend(
|
|
897
|
+
m: folium.Map, title: str, categories: list[str], colors: list[str]
|
|
898
|
+
) -> None:
|
|
899
|
+
"""Add categorical legend to a map.
|
|
853
900
|
|
|
854
901
|
The implementation is using the code originally written by Michel Metran
|
|
855
902
|
(@michelmetran) and released on GitHub
|
|
@@ -868,7 +915,6 @@ def _categorical_legend(m, title, categories, colors):
|
|
|
868
915
|
colors : list-like
|
|
869
916
|
list of colors (in the same order as categories)
|
|
870
917
|
"""
|
|
871
|
-
|
|
872
918
|
# Header to Add
|
|
873
919
|
head = """
|
|
874
920
|
{% macro header(this, kwargs) %}
|