ssb-sgis 1.0.8__tar.gz → 1.0.9__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.
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/PKG-INFO +1 -2
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/pyproject.toml +1 -2
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/conversion.py +6 -5
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/io/dapla_functions.py +2 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/io/opener.py +2 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/explore.py +18 -8
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/legend.py +3 -1
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/map.py +4 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/thematicmap.py +53 -26
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/base.py +60 -23
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/image_collection.py +702 -652
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/regex.py +2 -2
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/zonal.py +1 -58
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/LICENSE +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/README.md +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/__init__.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/debug_config.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/exceptions.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/__init__.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/bounds.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/centerlines.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/cleaning.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/duplicates.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/general.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/geocoding.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/geometry_types.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/neighbors.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/overlay.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/point_operations.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/polygon_operations.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/polygons_as_rings.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/geopandas_tools/sfilter.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/helpers.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/io/_is_dapla.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/io/read_parquet.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/__init__.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/examine.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/httpserver.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/maps.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/maps/tilesources.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/__init__.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/_get_route.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/_od_cost_matrix.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/_points.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/_service_area.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/closing_network_holes.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/cutting_lines.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/directednetwork.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/finding_isolated_networks.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/network.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/networkanalysis.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/networkanalysisrules.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/nodes.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/networkanalysis/traveling_salesman.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/parallel/parallel.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/py.typed +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/__init__.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/indices.py +0 -0
- {ssb_sgis-1.0.8 → ssb_sgis-1.0.9}/src/sgis/raster/sentinel_config.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ssb-sgis
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.9
|
|
4
4
|
Summary: GIS functions used at Statistics Norway.
|
|
5
5
|
Home-page: https://github.com/statisticsnorway/ssb-sgis
|
|
6
6
|
License: MIT
|
|
@@ -34,7 +34,6 @@ Requires-Dist: joblib (>=1.4.0)
|
|
|
34
34
|
Requires-Dist: mapclassify (>=2.5.0)
|
|
35
35
|
Requires-Dist: matplotlib (>=3.7.0)
|
|
36
36
|
Requires-Dist: networkx (>=3.0)
|
|
37
|
-
Requires-Dist: numba (>=0.60.0)
|
|
38
37
|
Requires-Dist: numpy (>=1.26.4)
|
|
39
38
|
Requires-Dist: pandas (>=2.2.1)
|
|
40
39
|
Requires-Dist: pyarrow (>=11.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ssb-sgis"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.9"
|
|
4
4
|
description = "GIS functions used at Statistics Norway."
|
|
5
5
|
authors = ["Morten Letnes <morten.letnes@ssb.no>"]
|
|
6
6
|
license = "MIT"
|
|
@@ -32,7 +32,6 @@ joblib = ">=1.4.0"
|
|
|
32
32
|
mapclassify = ">=2.5.0"
|
|
33
33
|
matplotlib = ">=3.7.0"
|
|
34
34
|
networkx = ">=3.0"
|
|
35
|
-
numba = { version = ">=0.60.0", optional = true }
|
|
36
35
|
numpy = ">=1.26.4"
|
|
37
36
|
pandas = ">=2.2.1"
|
|
38
37
|
pyarrow = ">=11.0.0"
|
|
@@ -446,12 +446,13 @@ def to_gdf(
|
|
|
446
446
|
geom_col: str = _find_geometry_column(obj, geometry) # type: ignore[no-redef]
|
|
447
447
|
index = kwargs.pop("index", None)
|
|
448
448
|
|
|
449
|
-
# get done with iterators that get consumed by 'all'
|
|
449
|
+
# get done with iterators that would get consumed by 'all' later
|
|
450
450
|
if isinstance(obj, Iterator) and not isinstance(obj, Sized):
|
|
451
|
-
obj =
|
|
452
|
-
|
|
453
|
-
)
|
|
454
|
-
|
|
451
|
+
obj = list(obj)
|
|
452
|
+
# obj = GeoSeries(
|
|
453
|
+
# (_make_one_shapely_geom(g) for g in obj if g is not None), index=index
|
|
454
|
+
# )
|
|
455
|
+
# return GeoDataFrame({geom_col: obj}, geometry=geom_col, crs=crs, **kwargs)
|
|
455
456
|
|
|
456
457
|
if hasattr(obj, "__len__") and not len(obj):
|
|
457
458
|
return GeoDataFrame({"geometry": []}, crs=crs)
|
|
@@ -183,9 +183,11 @@ def to_tile(tile: str | xyzservices.TileProvider, max_zoom: int) -> folium.TileL
|
|
|
183
183
|
|
|
184
184
|
|
|
185
185
|
def _single_band_to_arr(band, mask, name, raster_data_dict):
|
|
186
|
-
|
|
186
|
+
if band.has_array and mask is None:
|
|
187
187
|
arr = band.values
|
|
188
|
-
|
|
188
|
+
elif band.has_array:
|
|
189
|
+
arr = band.clip(mask).values
|
|
190
|
+
else:
|
|
189
191
|
arr = band.load(indexes=1, bounds=mask).values
|
|
190
192
|
bounds: tuple = (
|
|
191
193
|
_any_to_bbox_crs4326(mask, band.crs)
|
|
@@ -533,8 +535,8 @@ class Explore(Map):
|
|
|
533
535
|
arr = np.where(arr, 1, 0)
|
|
534
536
|
try:
|
|
535
537
|
arr = (arr - np.min(arr)) / (np.max(arr) - np.min(arr))
|
|
536
|
-
except Exception:
|
|
537
|
-
|
|
538
|
+
except Exception as e:
|
|
539
|
+
warnings.warn(str(e), stacklevel=1)
|
|
538
540
|
|
|
539
541
|
label = raster_data_dict["label"]
|
|
540
542
|
bounds = raster_data_dict["bounds"]
|
|
@@ -1066,7 +1068,7 @@ class Explore(Map):
|
|
|
1066
1068
|
name: str,
|
|
1067
1069
|
max_images: int,
|
|
1068
1070
|
n_added_images: int,
|
|
1069
|
-
rbg_bands: list[str] = (
|
|
1071
|
+
rbg_bands: list[str] = (("B04", "B02", "B03"), ("B4", "B2", "B3")),
|
|
1070
1072
|
) -> tuple[list[dict], int]:
|
|
1071
1073
|
out = []
|
|
1072
1074
|
|
|
@@ -1162,17 +1164,25 @@ class Explore(Map):
|
|
|
1162
1164
|
n_added_images += 1
|
|
1163
1165
|
continue
|
|
1164
1166
|
|
|
1167
|
+
def load(band_id: str) -> Band:
|
|
1168
|
+
band = image[band_id]
|
|
1169
|
+
if band.has_array and mask is not None:
|
|
1170
|
+
band = band.clip(mask, copy=True)
|
|
1171
|
+
elif not band.has_array:
|
|
1172
|
+
band = band.load(indexes=1, bounds=mask)
|
|
1173
|
+
return band
|
|
1174
|
+
|
|
1165
1175
|
for red, blue, green in rbg_bands:
|
|
1166
1176
|
try:
|
|
1167
|
-
red_band =
|
|
1177
|
+
red_band = load(red)
|
|
1168
1178
|
except KeyError:
|
|
1169
1179
|
continue
|
|
1170
1180
|
try:
|
|
1171
|
-
blue_band =
|
|
1181
|
+
blue_band = load(blue)
|
|
1172
1182
|
except KeyError:
|
|
1173
1183
|
continue
|
|
1174
1184
|
try:
|
|
1175
|
-
green_band =
|
|
1185
|
+
green_band = load(green)
|
|
1176
1186
|
except KeyError:
|
|
1177
1187
|
continue
|
|
1178
1188
|
break
|
|
@@ -73,8 +73,10 @@ LOWERCASE_WORDS = {
|
|
|
73
73
|
|
|
74
74
|
def prettify_label(label: str) -> str:
|
|
75
75
|
"""Replace underscores with spaces and capitalize words that are all lowecase."""
|
|
76
|
+
if len(label) == 1:
|
|
77
|
+
return label
|
|
76
78
|
return " ".join(
|
|
77
|
-
word.title() if word.islower() and word not in LOWERCASE_WORDS else word
|
|
79
|
+
(word.title() if word.islower() and word not in LOWERCASE_WORDS else word)
|
|
78
80
|
for word in label.replace("_", " ").split()
|
|
79
81
|
)
|
|
80
82
|
|
|
@@ -232,6 +232,10 @@ class Map:
|
|
|
232
232
|
self._nan_idx = self._gdf[self._column].isna()
|
|
233
233
|
self._get_unique_values()
|
|
234
234
|
|
|
235
|
+
def __getattr__(self, attr: str) -> Any:
|
|
236
|
+
"""Search for attribute in kwargs."""
|
|
237
|
+
return self.kwargs.get(attr, super().__getattribute__(attr))
|
|
238
|
+
|
|
235
239
|
def __bool__(self) -> bool:
|
|
236
240
|
"""True of any gdfs with more than 0 rows."""
|
|
237
241
|
return bool(len(self._gdfs) + len(self._gdf))
|
|
@@ -10,6 +10,8 @@ import numpy as np
|
|
|
10
10
|
import pandas as pd
|
|
11
11
|
from geopandas import GeoDataFrame
|
|
12
12
|
|
|
13
|
+
from ..geopandas_tools.conversion import to_bbox
|
|
14
|
+
from ..helpers import is_property
|
|
13
15
|
from .legend import LEGEND_KWARGS
|
|
14
16
|
from .legend import ContinousLegend
|
|
15
17
|
from .legend import Legend
|
|
@@ -37,9 +39,11 @@ MAP_KWARGS = {
|
|
|
37
39
|
"facecolor",
|
|
38
40
|
"labelcolor",
|
|
39
41
|
"nan_color",
|
|
42
|
+
# "alpha",
|
|
40
43
|
"title_kwargs",
|
|
41
44
|
"bg_gdf_color",
|
|
42
45
|
"title_position",
|
|
46
|
+
# "linewidth",
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
|
|
@@ -49,6 +53,7 @@ class ThematicMap(Map):
|
|
|
49
53
|
Args:
|
|
50
54
|
*gdfs: One or more GeoDataFrames.
|
|
51
55
|
column: The name of the column to plot.
|
|
56
|
+
bounds: Optional bounding box for the map.
|
|
52
57
|
title: Title of the plot.
|
|
53
58
|
title_position: Title position. Either "center" (default), "left" or "right".
|
|
54
59
|
size: Width and height of the plot in inches. Fontsize of title and legend is
|
|
@@ -155,6 +160,7 @@ class ThematicMap(Map):
|
|
|
155
160
|
self,
|
|
156
161
|
*gdfs: GeoDataFrame,
|
|
157
162
|
column: str | None = None,
|
|
163
|
+
bounds: tuple | None = None,
|
|
158
164
|
title: str | None = None,
|
|
159
165
|
title_position: tuple[float, float] | None = None,
|
|
160
166
|
size: int = 25,
|
|
@@ -166,7 +172,7 @@ class ThematicMap(Map):
|
|
|
166
172
|
nan_label: str = "Missing",
|
|
167
173
|
legend_kwargs: dict | None = None,
|
|
168
174
|
title_kwargs: dict | None = None,
|
|
169
|
-
legend: bool =
|
|
175
|
+
legend: bool = True,
|
|
170
176
|
**kwargs,
|
|
171
177
|
) -> None:
|
|
172
178
|
"""Initialiser."""
|
|
@@ -179,9 +185,6 @@ class ThematicMap(Map):
|
|
|
179
185
|
nan_label=nan_label,
|
|
180
186
|
)
|
|
181
187
|
|
|
182
|
-
if not legend:
|
|
183
|
-
self.legend = None
|
|
184
|
-
|
|
185
188
|
self.title = title
|
|
186
189
|
self._size = size
|
|
187
190
|
self._dark = dark
|
|
@@ -222,21 +225,23 @@ class ThematicMap(Map):
|
|
|
222
225
|
if not self.cmap and not self._is_categorical:
|
|
223
226
|
self._choose_cmap()
|
|
224
227
|
|
|
228
|
+
if not legend:
|
|
229
|
+
self.legend = None
|
|
230
|
+
else:
|
|
231
|
+
self._create_legend()
|
|
232
|
+
|
|
225
233
|
self._dark_or_light()
|
|
226
|
-
self._create_legend()
|
|
227
234
|
|
|
228
235
|
if cmap:
|
|
229
236
|
self._cmap = cmap
|
|
230
237
|
|
|
231
238
|
for key, value in kwargs.items():
|
|
232
239
|
if key not in MAP_KWARGS:
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
)
|
|
236
|
-
try:
|
|
237
|
-
setattr(self, key, value)
|
|
238
|
-
except Exception:
|
|
240
|
+
self.kwargs[key] = value
|
|
241
|
+
elif is_property(self, key):
|
|
239
242
|
setattr(self, f"_{key}", value)
|
|
243
|
+
else:
|
|
244
|
+
setattr(self, key, value)
|
|
240
245
|
|
|
241
246
|
for key, value in legend_kwargs.items():
|
|
242
247
|
if key not in LEGEND_KWARGS:
|
|
@@ -249,6 +254,13 @@ class ThematicMap(Map):
|
|
|
249
254
|
except Exception:
|
|
250
255
|
setattr(self.legend, f"_{key}", value)
|
|
251
256
|
|
|
257
|
+
self.bounds = (
|
|
258
|
+
to_bbox(bounds) if bounds is not None else to_bbox(self._gdf.total_bounds)
|
|
259
|
+
)
|
|
260
|
+
self.minx, self.miny, self.maxx, self.maxy = self.bounds
|
|
261
|
+
self.diffx = self.maxx - self.minx
|
|
262
|
+
self.diffy = self.maxy - self.miny
|
|
263
|
+
|
|
252
264
|
@property
|
|
253
265
|
def valid_keywords(self) -> set[str]:
|
|
254
266
|
"""List all valid keywords for the class initialiser."""
|
|
@@ -285,16 +297,17 @@ class ThematicMap(Map):
|
|
|
285
297
|
self._background_gdfs = pd.concat(
|
|
286
298
|
[self._background_gdfs, gdf], ignore_index=True
|
|
287
299
|
)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
self.diffy = self.maxy - self.miny
|
|
300
|
+
if self.bounds is None:
|
|
301
|
+
self.bounds = to_bbox(self._gdf.total_bounds)
|
|
291
302
|
return self
|
|
292
303
|
|
|
293
304
|
def plot(self, **kwargs) -> None:
|
|
294
305
|
"""Creates the final plot.
|
|
295
306
|
|
|
296
307
|
This method should be run after customising the map, but before saving.
|
|
308
|
+
|
|
297
309
|
"""
|
|
310
|
+
kwargs = kwargs | self.kwargs
|
|
298
311
|
__test = kwargs.pop("__test", False)
|
|
299
312
|
include_legend = bool(kwargs.pop("legend", self.legend))
|
|
300
313
|
|
|
@@ -379,6 +392,13 @@ class ThematicMap(Map):
|
|
|
379
392
|
|
|
380
393
|
if hasattr(self, "_background_gdfs"):
|
|
381
394
|
self._actually_add_background()
|
|
395
|
+
elif self.bounds is not None:
|
|
396
|
+
self.ax.set_xlim(
|
|
397
|
+
[self.minx - self.diffx * 0.03, self.maxx + self.diffx * 0.03]
|
|
398
|
+
)
|
|
399
|
+
self.ax.set_ylim(
|
|
400
|
+
[self.miny - self.diffy * 0.03, self.maxy + self.diffy * 0.03]
|
|
401
|
+
)
|
|
382
402
|
|
|
383
403
|
if self.title:
|
|
384
404
|
self.ax.set_title(
|
|
@@ -468,18 +488,10 @@ class ThematicMap(Map):
|
|
|
468
488
|
|
|
469
489
|
def _create_legend(self) -> None:
|
|
470
490
|
"""Instantiate the Legend class."""
|
|
471
|
-
if self.legend is None:
|
|
472
|
-
return
|
|
473
|
-
kwargs = {}
|
|
474
|
-
if self._dark:
|
|
475
|
-
kwargs["facecolor"] = "#0f0f0f"
|
|
476
|
-
kwargs["labelcolor"] = "#fefefe"
|
|
477
|
-
kwargs["title_color"] = "#fefefe"
|
|
478
|
-
|
|
479
491
|
if self._is_categorical:
|
|
480
|
-
self.legend = Legend(title=self._column, size=self._size
|
|
492
|
+
self.legend = Legend(title=self._column, size=self._size)
|
|
481
493
|
else:
|
|
482
|
-
self.legend = ContinousLegend(title=self._column, size=self._size
|
|
494
|
+
self.legend = ContinousLegend(title=self._column, size=self._size)
|
|
483
495
|
|
|
484
496
|
def _choose_cmap(self) -> None:
|
|
485
497
|
"""Kwargs is to catch start and stop points for the cmap in __init__."""
|
|
@@ -524,16 +536,32 @@ class ThematicMap(Map):
|
|
|
524
536
|
if not self._is_categorical:
|
|
525
537
|
self.change_cmap("viridis")
|
|
526
538
|
|
|
539
|
+
if self.legend is not None:
|
|
540
|
+
for key, color in {
|
|
541
|
+
"facecolor": "#0f0f0f",
|
|
542
|
+
"labelcolor": "#fefefe",
|
|
543
|
+
"title_color": "#fefefe",
|
|
544
|
+
}.items():
|
|
545
|
+
setattr(self.legend, key, color)
|
|
546
|
+
|
|
527
547
|
else:
|
|
528
548
|
self.facecolor, self.title_color, self.bg_gdf_color = (
|
|
529
549
|
"#fefefe",
|
|
530
550
|
"#0f0f0f",
|
|
531
|
-
"#
|
|
551
|
+
"#e8e6e6",
|
|
532
552
|
)
|
|
533
553
|
self.nan_color = "#c2c2c2"
|
|
534
554
|
if not self._is_categorical:
|
|
535
555
|
self.change_cmap("RdPu", start=23)
|
|
536
556
|
|
|
557
|
+
if self.legend is not None:
|
|
558
|
+
for key, color in {
|
|
559
|
+
"facecolor": "#fefefe",
|
|
560
|
+
"labelcolor": "#0f0f0f",
|
|
561
|
+
"title_color": "#0f0f0f",
|
|
562
|
+
}.items():
|
|
563
|
+
setattr(self.legend, key, color)
|
|
564
|
+
|
|
537
565
|
@property
|
|
538
566
|
def dark(self) -> bool:
|
|
539
567
|
"""Whether to use dark background and light text colors."""
|
|
@@ -543,7 +571,6 @@ class ThematicMap(Map):
|
|
|
543
571
|
def dark(self, new_value: bool):
|
|
544
572
|
self._dark = new_value
|
|
545
573
|
self._dark_or_light()
|
|
546
|
-
self._create_legend()
|
|
547
574
|
|
|
548
575
|
@property
|
|
549
576
|
def title_fontsize(self) -> int:
|
|
@@ -9,6 +9,7 @@ import joblib
|
|
|
9
9
|
import numpy as np
|
|
10
10
|
import pandas as pd
|
|
11
11
|
import rasterio
|
|
12
|
+
import shapely
|
|
12
13
|
from affine import Affine
|
|
13
14
|
from geopandas import GeoDataFrame
|
|
14
15
|
from geopandas import GeoSeries
|
|
@@ -20,8 +21,21 @@ from shapely.geometry import shape
|
|
|
20
21
|
from ..geopandas_tools.conversion import to_bbox
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def _get_res_from_bounds(
|
|
25
|
+
obj: GeoDataFrame | GeoSeries | Geometry | tuple, shape: tuple[int, ...]
|
|
26
|
+
) -> tuple[int, int] | None:
|
|
27
|
+
minx, miny, maxx, maxy = to_bbox(obj)
|
|
28
|
+
try:
|
|
29
|
+
height, width = shape[-2:]
|
|
30
|
+
except IndexError:
|
|
31
|
+
return None
|
|
32
|
+
resx = (maxx - minx) / width
|
|
33
|
+
resy = (maxy - miny) / height
|
|
34
|
+
return resx, resy
|
|
35
|
+
|
|
36
|
+
|
|
23
37
|
def _get_transform_from_bounds(
|
|
24
|
-
obj: GeoDataFrame | GeoSeries | Geometry | tuple, shape: tuple[
|
|
38
|
+
obj: GeoDataFrame | GeoSeries | Geometry | tuple, shape: tuple[int, ...]
|
|
25
39
|
) -> Affine:
|
|
26
40
|
minx, miny, maxx, maxy = to_bbox(obj)
|
|
27
41
|
if len(shape) == 2:
|
|
@@ -34,12 +48,16 @@ def _get_transform_from_bounds(
|
|
|
34
48
|
return rasterio.transform.from_bounds(minx, miny, maxx, maxy, width, height)
|
|
35
49
|
|
|
36
50
|
|
|
51
|
+
def _res_as_tuple(res: int | float | tuple[int | float]) -> tuple[int | float]:
|
|
52
|
+
return (res, res) if isinstance(res, numbers.Number) else res
|
|
53
|
+
|
|
54
|
+
|
|
37
55
|
def _get_shape_from_bounds(
|
|
38
56
|
obj: GeoDataFrame | GeoSeries | Geometry | tuple,
|
|
39
57
|
res: int,
|
|
40
58
|
indexes: int | tuple[int],
|
|
41
59
|
) -> tuple[int, int]:
|
|
42
|
-
resx, resy = (res
|
|
60
|
+
resx, resy = _res_as_tuple(res)
|
|
43
61
|
|
|
44
62
|
minx, miny, maxx, maxy = to_bbox(obj)
|
|
45
63
|
|
|
@@ -111,7 +129,9 @@ def _value_geom_pair(value, geom):
|
|
|
111
129
|
|
|
112
130
|
def _gdf_to_arr(
|
|
113
131
|
gdf: GeoDataFrame,
|
|
114
|
-
res: int | float,
|
|
132
|
+
res: int | float | None = None,
|
|
133
|
+
out_shape: tuple[int, int] | None = None,
|
|
134
|
+
bounds: tuple[float] | None = None,
|
|
115
135
|
fill: int = 0,
|
|
116
136
|
all_touched: bool = False,
|
|
117
137
|
merge_alg: Callable = MergeAlg.replace,
|
|
@@ -125,6 +145,7 @@ def _gdf_to_arr(
|
|
|
125
145
|
Args:
|
|
126
146
|
gdf: The GeoDataFrame to rasterize.
|
|
127
147
|
res: Resolution of the raster in units of the GeoDataFrame's coordinate reference system.
|
|
148
|
+
bounds: Optional bounds to box 'gdf' into (so both clip and extend to).
|
|
128
149
|
fill: Fill value for areas outside of input geometries (default is 0).
|
|
129
150
|
all_touched: Whether to consider all pixels touched by geometries,
|
|
130
151
|
not just those whose center is within the polygon (default is False).
|
|
@@ -134,6 +155,7 @@ def _gdf_to_arr(
|
|
|
134
155
|
(default is 1).
|
|
135
156
|
dtype: Data type of the output array. If None, it will be
|
|
136
157
|
determined automatically.
|
|
158
|
+
out_shape: Optional 2 dimensional shape of the resulting array.
|
|
137
159
|
|
|
138
160
|
Returns:
|
|
139
161
|
A Raster instance based on the specified GeoDataFrame and parameters.
|
|
@@ -142,33 +164,48 @@ def _gdf_to_arr(
|
|
|
142
164
|
TypeError: If 'transform' is provided in kwargs, as this is
|
|
143
165
|
computed based on the GeoDataFrame bounds and resolution.
|
|
144
166
|
"""
|
|
167
|
+
if res is not None and out_shape is not None:
|
|
168
|
+
raise TypeError("Cannot specify both 'res' and 'out_shape'")
|
|
169
|
+
if res is None and out_shape is None:
|
|
170
|
+
raise TypeError("Must specify either 'res' or 'out_shape'")
|
|
171
|
+
|
|
145
172
|
if isinstance(gdf, GeoSeries):
|
|
146
|
-
values = gdf.index
|
|
147
173
|
gdf = gdf.to_frame("geometry")
|
|
148
|
-
elif isinstance(gdf, GeoDataFrame):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
elif not isinstance(gdf, GeoDataFrame):
|
|
175
|
+
raise TypeError(type(gdf))
|
|
176
|
+
|
|
177
|
+
if bounds is not None:
|
|
178
|
+
gdf = gdf.clip(bounds)
|
|
179
|
+
bounds_gdf = GeoDataFrame({"geometry": [shapely.box(*bounds)]}, crs=gdf.crs)
|
|
180
|
+
|
|
181
|
+
if len(gdf.columns) > 2:
|
|
182
|
+
raise ValueError(
|
|
183
|
+
"gdf should have only a geometry column and one numeric column to "
|
|
184
|
+
"use as array values. "
|
|
185
|
+
"Alternatively only a geometry column and a numeric index."
|
|
186
|
+
)
|
|
187
|
+
elif len(gdf.columns) == 1:
|
|
188
|
+
values = np.full(len(gdf), default_value)
|
|
189
|
+
else:
|
|
190
|
+
col: str = next(iter(gdf.columns.difference({gdf.geometry.name})))
|
|
191
|
+
values = gdf[col].values
|
|
192
|
+
|
|
193
|
+
if bounds is not None:
|
|
194
|
+
gdf = pd.concat([bounds_gdf, gdf])
|
|
195
|
+
values = np.concatenate([np.array([fill]), values])
|
|
196
|
+
|
|
197
|
+
if out_shape is None:
|
|
198
|
+
assert res is not None
|
|
199
|
+
out_shape = _get_shape_from_bounds(gdf.total_bounds, res=res, indexes=1)
|
|
162
200
|
|
|
163
|
-
if
|
|
164
|
-
|
|
201
|
+
if not len(gdf):
|
|
202
|
+
return np.full(out_shape, fill)
|
|
165
203
|
|
|
166
|
-
|
|
167
|
-
transform = _get_transform_from_bounds(gdf.total_bounds, shape)
|
|
204
|
+
transform = _get_transform_from_bounds(gdf.total_bounds, out_shape)
|
|
168
205
|
|
|
169
206
|
return features.rasterize(
|
|
170
207
|
_gdf_to_geojson_with_col(gdf, values),
|
|
171
|
-
out_shape=
|
|
208
|
+
out_shape=out_shape,
|
|
172
209
|
transform=transform,
|
|
173
210
|
fill=fill,
|
|
174
211
|
all_touched=all_touched,
|