ssb-sgis 1.0.2__py3-none-any.whl → 1.0.4__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 +20 -9
- sgis/debug_config.py +24 -0
- sgis/exceptions.py +2 -2
- sgis/geopandas_tools/bounds.py +33 -36
- sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
- sgis/geopandas_tools/centerlines.py +4 -91
- sgis/geopandas_tools/cleaning.py +1576 -583
- sgis/geopandas_tools/conversion.py +38 -19
- sgis/geopandas_tools/duplicates.py +29 -8
- sgis/geopandas_tools/general.py +263 -100
- sgis/geopandas_tools/geometry_types.py +4 -4
- sgis/geopandas_tools/neighbors.py +19 -15
- sgis/geopandas_tools/overlay.py +2 -2
- sgis/geopandas_tools/point_operations.py +5 -5
- sgis/geopandas_tools/polygon_operations.py +510 -105
- sgis/geopandas_tools/polygons_as_rings.py +40 -8
- sgis/geopandas_tools/sfilter.py +29 -12
- sgis/helpers.py +3 -3
- sgis/io/dapla_functions.py +238 -19
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +27 -12
- sgis/maps/explore.py +450 -65
- sgis/maps/legend.py +177 -76
- sgis/maps/map.py +206 -103
- sgis/maps/maps.py +178 -105
- sgis/maps/thematicmap.py +243 -83
- sgis/networkanalysis/_service_area.py +6 -1
- sgis/networkanalysis/closing_network_holes.py +2 -2
- sgis/networkanalysis/cutting_lines.py +15 -8
- sgis/networkanalysis/directednetwork.py +1 -1
- sgis/networkanalysis/finding_isolated_networks.py +15 -8
- sgis/networkanalysis/networkanalysis.py +17 -19
- sgis/networkanalysis/networkanalysisrules.py +1 -1
- sgis/networkanalysis/traveling_salesman.py +1 -1
- sgis/parallel/parallel.py +64 -27
- sgis/raster/__init__.py +0 -6
- sgis/raster/base.py +208 -0
- sgis/raster/cube.py +54 -8
- sgis/raster/image_collection.py +3257 -0
- sgis/raster/indices.py +17 -5
- sgis/raster/raster.py +138 -243
- sgis/raster/sentinel_config.py +120 -0
- sgis/raster/zonal.py +0 -1
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
- ssb_sgis-1.0.4.dist-info/RECORD +62 -0
- sgis/raster/methods_as_functions.py +0 -0
- sgis/raster/torchgeo.py +0 -171
- ssb_sgis-1.0.2.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/WHEEL +0 -0
sgis/maps/explore.py
CHANGED
|
@@ -5,16 +5,21 @@ clipmap functions from the 'maps' module.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import os
|
|
8
|
+
import random
|
|
9
|
+
import re
|
|
8
10
|
import warnings
|
|
9
11
|
from collections.abc import Iterable
|
|
10
12
|
from numbers import Number
|
|
13
|
+
from pathlib import Path
|
|
11
14
|
from statistics import mean
|
|
12
15
|
from typing import Any
|
|
13
16
|
from typing import ClassVar
|
|
14
17
|
|
|
15
18
|
import branca as bc
|
|
16
19
|
import folium
|
|
20
|
+
import geopandas as gpd
|
|
17
21
|
import matplotlib
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
18
23
|
import numpy as np
|
|
19
24
|
import pandas as pd
|
|
20
25
|
import xyzservices
|
|
@@ -25,15 +30,38 @@ from IPython.display import display
|
|
|
25
30
|
from jinja2 import Template
|
|
26
31
|
from pandas.api.types import is_datetime64_any_dtype
|
|
27
32
|
from shapely import Geometry
|
|
33
|
+
from shapely import box
|
|
34
|
+
from shapely import union_all
|
|
28
35
|
from shapely.geometry import LineString
|
|
29
36
|
|
|
37
|
+
from ..geopandas_tools.bounds import get_total_bounds
|
|
38
|
+
from ..geopandas_tools.conversion import to_bbox
|
|
30
39
|
from ..geopandas_tools.conversion import to_gdf
|
|
40
|
+
from ..geopandas_tools.conversion import to_shapely
|
|
31
41
|
from ..geopandas_tools.general import clean_geoms
|
|
32
42
|
from ..geopandas_tools.general import make_all_singlepart
|
|
33
43
|
from ..geopandas_tools.geometry_types import get_geom_type
|
|
34
44
|
from ..geopandas_tools.geometry_types import to_single_geom_type
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
from ..raster.image_collection import Band
|
|
48
|
+
from ..raster.image_collection import Image
|
|
49
|
+
from ..raster.image_collection import ImageCollection
|
|
50
|
+
except ImportError:
|
|
51
|
+
|
|
52
|
+
class Band:
|
|
53
|
+
"""Placeholder."""
|
|
54
|
+
|
|
55
|
+
class Image:
|
|
56
|
+
"""Placeholder."""
|
|
57
|
+
|
|
58
|
+
class ImageCollection:
|
|
59
|
+
"""Placeholder."""
|
|
60
|
+
|
|
61
|
+
|
|
35
62
|
from .httpserver import run_html_server
|
|
36
63
|
from .map import Map
|
|
64
|
+
from .map import _determine_best_name
|
|
37
65
|
from .tilesources import kartverket
|
|
38
66
|
from .tilesources import xyz
|
|
39
67
|
|
|
@@ -153,15 +181,45 @@ def to_tile(tile: str | xyzservices.TileProvider, max_zoom: int) -> folium.TileL
|
|
|
153
181
|
return folium.TileLayer(provider, name=name, attr=attr, max_zoom=max_zoom)
|
|
154
182
|
|
|
155
183
|
|
|
184
|
+
def _single_band_to_arr(band, mask, name, raster_data_dict):
|
|
185
|
+
try:
|
|
186
|
+
arr = band.values
|
|
187
|
+
except (ValueError, AttributeError):
|
|
188
|
+
arr = band.load(indexes=1, bounds=mask).values
|
|
189
|
+
bounds: tuple = (
|
|
190
|
+
_any_to_bbox_crs4326(mask, band.crs)
|
|
191
|
+
if mask is not None
|
|
192
|
+
else union_all(
|
|
193
|
+
gpd.GeoSeries(box(*band.bounds), crs=band.crs).to_crs(4326).geometry.values
|
|
194
|
+
).bounds
|
|
195
|
+
)
|
|
196
|
+
# if np.max(arr) > 0:
|
|
197
|
+
# arr = arr / 255
|
|
198
|
+
try:
|
|
199
|
+
raster_data_dict["cmap"] = band.get_cmap(arr)
|
|
200
|
+
except Exception:
|
|
201
|
+
try:
|
|
202
|
+
raster_data_dict["cmap"] = plt.get_cmap(band.cmap)
|
|
203
|
+
except Exception:
|
|
204
|
+
raster_data_dict["cmap"] = band.cmap or "Grays"
|
|
205
|
+
raster_data_dict["arr"] = arr
|
|
206
|
+
raster_data_dict["bounds"] = bounds
|
|
207
|
+
raster_data_dict["label"] = name
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _any_to_bbox_crs4326(obj, crs):
|
|
211
|
+
return to_bbox(to_gdf(obj, crs).to_crs(4326))
|
|
212
|
+
|
|
213
|
+
|
|
156
214
|
class Explore(Map):
|
|
157
215
|
"""Class for displaying and saving html maps of multiple GeoDataFrames."""
|
|
158
216
|
|
|
159
217
|
# class attribute that can be overridden locally
|
|
160
218
|
tiles: ClassVar[tuple[str]] = (
|
|
161
|
-
"grunnkart",
|
|
162
|
-
"norge_i_bilder",
|
|
163
|
-
"dark",
|
|
164
219
|
"OpenStreetMap",
|
|
220
|
+
"dark",
|
|
221
|
+
"norge_i_bilder",
|
|
222
|
+
"grunnkart",
|
|
165
223
|
)
|
|
166
224
|
|
|
167
225
|
def __init__(
|
|
@@ -176,10 +234,11 @@ class Explore(Map):
|
|
|
176
234
|
prefer_canvas: bool = True,
|
|
177
235
|
measure_control: bool = True,
|
|
178
236
|
geocoder: bool = False,
|
|
179
|
-
|
|
237
|
+
out_path: str | None = None,
|
|
180
238
|
show: bool | Iterable[bool] | None = None,
|
|
181
239
|
text: str | None = None,
|
|
182
240
|
decimals: int = 6,
|
|
241
|
+
max_images: int = 10,
|
|
183
242
|
**kwargs,
|
|
184
243
|
) -> None:
|
|
185
244
|
"""Initialiser.
|
|
@@ -196,15 +255,18 @@ class Explore(Map):
|
|
|
196
255
|
prefer_canvas: Option.
|
|
197
256
|
measure_control: Whether to include measurement box.
|
|
198
257
|
geocoder: Whether to include search bar for addresses.
|
|
199
|
-
|
|
258
|
+
out_path: Optional file path to an html file. The map will then
|
|
200
259
|
be saved instead of displayed.
|
|
201
260
|
show: Whether to show or hide the data upon creating the map.
|
|
202
261
|
If False, the data can be toggled on later. 'show' can also be
|
|
203
262
|
a sequence of boolean values the same length as the number of
|
|
204
263
|
GeoDataFrames.
|
|
264
|
+
max_images: Maximum number of images (Image, ImageCollection, Band) to show per
|
|
265
|
+
map. Defaults to 15.
|
|
205
266
|
text: Optional text for a text box in the map.
|
|
206
267
|
decimals: Number of decimals in the coordinates.
|
|
207
|
-
**kwargs: Additional keyword arguments
|
|
268
|
+
**kwargs: Additional keyword arguments. Can also be geometry-like objects
|
|
269
|
+
where the key is the label.
|
|
208
270
|
"""
|
|
209
271
|
self.popup = popup
|
|
210
272
|
self.max_zoom = max_zoom
|
|
@@ -212,10 +274,12 @@ class Explore(Map):
|
|
|
212
274
|
self.prefer_canvas = prefer_canvas
|
|
213
275
|
self.measure_control = measure_control
|
|
214
276
|
self.geocoder = geocoder
|
|
215
|
-
self.
|
|
277
|
+
self.out_path = out_path
|
|
216
278
|
self.mask = mask
|
|
217
279
|
self.text = text
|
|
218
280
|
self.decimals = decimals
|
|
281
|
+
self.max_images = max_images
|
|
282
|
+
self.legend = None
|
|
219
283
|
|
|
220
284
|
self.browser = browser
|
|
221
285
|
if not self.browser and "show_in_browser" in kwargs:
|
|
@@ -224,19 +288,35 @@ class Explore(Map):
|
|
|
224
288
|
self.browser = kwargs.pop("in_browser")
|
|
225
289
|
|
|
226
290
|
if show is None:
|
|
227
|
-
|
|
291
|
+
self._show_was_none = True
|
|
228
292
|
show = True
|
|
229
293
|
else:
|
|
230
|
-
|
|
294
|
+
self._show_was_none = False
|
|
231
295
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
# )
|
|
237
|
-
# self.tiles # += self.raster_datasets
|
|
296
|
+
new_gdfs = {}
|
|
297
|
+
self.rasters = {}
|
|
298
|
+
for i, gdf in enumerate(gdfs):
|
|
299
|
+
name = _determine_best_name(gdf, column, i)
|
|
238
300
|
|
|
239
|
-
|
|
301
|
+
if name in new_gdfs or name in self.rasters:
|
|
302
|
+
name += str(i)
|
|
303
|
+
|
|
304
|
+
if isinstance(gdf, (ImageCollection | Image | Band)):
|
|
305
|
+
self.rasters[name] = gdf.copy()
|
|
306
|
+
continue
|
|
307
|
+
try:
|
|
308
|
+
new_gdfs[name] = to_gdf(gdf)
|
|
309
|
+
except Exception:
|
|
310
|
+
continue
|
|
311
|
+
|
|
312
|
+
new_kwargs = {}
|
|
313
|
+
for key, value in kwargs.items():
|
|
314
|
+
if isinstance(value, (ImageCollection | Image | Band)):
|
|
315
|
+
self.rasters[key] = value
|
|
316
|
+
else:
|
|
317
|
+
new_kwargs[key] = value
|
|
318
|
+
|
|
319
|
+
super().__init__(column=column, show=show, **(new_kwargs | new_gdfs))
|
|
240
320
|
|
|
241
321
|
if self.gdfs is None:
|
|
242
322
|
return
|
|
@@ -273,14 +353,17 @@ class Explore(Map):
|
|
|
273
353
|
gdf.index = gdf.index.astype(str)
|
|
274
354
|
except Exception:
|
|
275
355
|
pass
|
|
276
|
-
new_gdfs.append(gdf)
|
|
356
|
+
new_gdfs.append(to_gdf(gdf))
|
|
277
357
|
show_new.append(show)
|
|
278
358
|
self._gdfs = new_gdfs
|
|
279
|
-
self.
|
|
359
|
+
if self._gdfs:
|
|
360
|
+
self._gdf = pd.concat(new_gdfs, ignore_index=True)
|
|
361
|
+
else:
|
|
362
|
+
self._gdf = GeoDataFrame({"geometry": [], self._column: []})
|
|
280
363
|
self.show = show_new
|
|
281
364
|
|
|
282
|
-
if
|
|
283
|
-
|
|
365
|
+
# if self._show_was_none and len(self._gdfs) > 6:
|
|
366
|
+
# self.show = [False] * len(self._gdfs)
|
|
284
367
|
|
|
285
368
|
if self._is_categorical:
|
|
286
369
|
if len(self.gdfs) == 1:
|
|
@@ -288,27 +371,45 @@ class Explore(Map):
|
|
|
288
371
|
else:
|
|
289
372
|
if not self._cmap:
|
|
290
373
|
self._cmap = "viridis"
|
|
291
|
-
self.cmap_start = kwargs.pop("cmap_start", 0)
|
|
292
|
-
self.cmap_stop = kwargs.pop("cmap_stop", 256)
|
|
374
|
+
self.cmap_start = self.kwargs.pop("cmap_start", 0)
|
|
375
|
+
self.cmap_stop = self.kwargs.pop("cmap_stop", 256)
|
|
293
376
|
|
|
294
|
-
if self._gdf.crs is None:
|
|
295
|
-
|
|
377
|
+
# if self._gdf.crs is None:
|
|
378
|
+
# self.kwargs["crs"] = "Simple"
|
|
296
379
|
|
|
297
380
|
self.original_crs = self.gdf.crs
|
|
298
381
|
|
|
299
382
|
def __repr__(self) -> str:
|
|
300
383
|
"""Representation."""
|
|
301
|
-
return f"{self.__class__.__name__}()"
|
|
384
|
+
return f"{self.__class__.__name__}({len(self)})"
|
|
385
|
+
|
|
386
|
+
def __len__(self) -> int:
|
|
387
|
+
"""Number of gdfs that have rows plus number of raster images."""
|
|
388
|
+
try:
|
|
389
|
+
rasters = self.raster_data
|
|
390
|
+
except AttributeError:
|
|
391
|
+
rasters = self.rasters
|
|
392
|
+
return len([gdf for gdf in self._gdfs if len(gdf)]) + len(rasters)
|
|
393
|
+
|
|
394
|
+
def __bool__(self) -> bool:
|
|
395
|
+
"""True if any gdfs have rows or there are any raster images."""
|
|
396
|
+
return bool(len(self))
|
|
302
397
|
|
|
303
398
|
def explore(
|
|
304
399
|
self,
|
|
305
400
|
column: str | None = None,
|
|
306
401
|
center: Any | None = None,
|
|
307
402
|
size: int | None = None,
|
|
403
|
+
mask: Any | None = None,
|
|
308
404
|
**kwargs,
|
|
309
405
|
) -> None:
|
|
310
406
|
"""Explore all the data."""
|
|
311
|
-
|
|
407
|
+
self.mask = mask if mask is not None else self.mask
|
|
408
|
+
if (
|
|
409
|
+
self._gdfs
|
|
410
|
+
and not any(len(gdf) for gdf in self._gdfs)
|
|
411
|
+
and not len(self.rasters)
|
|
412
|
+
):
|
|
312
413
|
warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
|
|
313
414
|
return
|
|
314
415
|
if column:
|
|
@@ -346,9 +447,9 @@ class Explore(Map):
|
|
|
346
447
|
|
|
347
448
|
def samplemap(
|
|
348
449
|
self,
|
|
349
|
-
size: int
|
|
450
|
+
size: int,
|
|
451
|
+
sample: Any,
|
|
350
452
|
column: str | None = None,
|
|
351
|
-
sample_from_first: bool = True,
|
|
352
453
|
**kwargs,
|
|
353
454
|
) -> None:
|
|
354
455
|
"""Explore a sample of the data."""
|
|
@@ -357,34 +458,24 @@ class Explore(Map):
|
|
|
357
458
|
self._update_column()
|
|
358
459
|
kwargs.pop("column", None)
|
|
359
460
|
|
|
360
|
-
|
|
361
|
-
sample =
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# convert lines to polygons
|
|
366
|
-
if get_geom_type(sample) == "line":
|
|
367
|
-
sample["geometry"] = sample.buffer(1)
|
|
461
|
+
try:
|
|
462
|
+
sample = sample.sample(1)
|
|
463
|
+
except Exception:
|
|
464
|
+
pass
|
|
368
465
|
|
|
369
|
-
|
|
370
|
-
|
|
466
|
+
try:
|
|
467
|
+
sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
|
|
468
|
+
except Exception:
|
|
469
|
+
sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
|
|
371
470
|
|
|
372
|
-
|
|
373
|
-
else:
|
|
374
|
-
random_point = sample.centroid
|
|
471
|
+
random_point = sample.sample_points(size=1)
|
|
375
472
|
|
|
376
473
|
self.center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
|
|
377
474
|
print(f"center={self.center}, size={size}")
|
|
378
475
|
|
|
379
|
-
|
|
380
|
-
for gdf in self._gdfs:
|
|
381
|
-
gdf = gdf.clip(random_point.buffer(size))
|
|
382
|
-
gdfs = gdfs + (gdf,)
|
|
383
|
-
self._gdfs = gdfs
|
|
384
|
-
self._gdf = pd.concat(gdfs, ignore_index=True)
|
|
476
|
+
mask = random_point.buffer(size)
|
|
385
477
|
|
|
386
|
-
self.
|
|
387
|
-
self._explore(**kwargs)
|
|
478
|
+
return self.clipmap(mask, column, **kwargs)
|
|
388
479
|
|
|
389
480
|
def clipmap(
|
|
390
481
|
self,
|
|
@@ -393,6 +484,7 @@ class Explore(Map):
|
|
|
393
484
|
**kwargs,
|
|
394
485
|
) -> None:
|
|
395
486
|
"""Explore the data within a mask extent."""
|
|
487
|
+
self.mask = mask
|
|
396
488
|
if column:
|
|
397
489
|
self._column = column
|
|
398
490
|
self._update_column()
|
|
@@ -400,26 +492,89 @@ class Explore(Map):
|
|
|
400
492
|
|
|
401
493
|
gdfs: tuple[GeoDataFrame] = ()
|
|
402
494
|
for gdf in self._gdfs:
|
|
403
|
-
gdf = gdf.clip(mask)
|
|
495
|
+
gdf = gdf.clip(self.mask)
|
|
404
496
|
collections = gdf.loc[gdf.geom_type == "GeometryCollection"]
|
|
405
497
|
if len(collections):
|
|
406
498
|
collections = make_all_singlepart(collections)
|
|
407
499
|
gdf = pd.concat([gdf, collections], ignore_index=False)
|
|
408
500
|
gdfs = gdfs + (gdf,)
|
|
409
501
|
self._gdfs = gdfs
|
|
410
|
-
self.
|
|
502
|
+
if self._gdfs:
|
|
503
|
+
self._gdf = pd.concat(self._gdfs, ignore_index=True)
|
|
504
|
+
else:
|
|
505
|
+
self._gdf = GeoDataFrame({"geometry": [], self._column: []})
|
|
506
|
+
|
|
411
507
|
self._explore(**kwargs)
|
|
412
508
|
|
|
509
|
+
def _load_rasters_as_images(self):
|
|
510
|
+
self.raster_data = []
|
|
511
|
+
n_added_images = 0
|
|
512
|
+
self._show_rasters = True
|
|
513
|
+
for name, value in self.rasters.items():
|
|
514
|
+
data, n_added_images = self._image_collection_to_background_map(
|
|
515
|
+
value,
|
|
516
|
+
self.mask,
|
|
517
|
+
name,
|
|
518
|
+
max_images=self.max_images,
|
|
519
|
+
n_added_images=n_added_images,
|
|
520
|
+
)
|
|
521
|
+
self.raster_data += data
|
|
522
|
+
|
|
523
|
+
def _rasters_to_background_maps(self):
|
|
524
|
+
for raster_data_dict in self.raster_data:
|
|
525
|
+
try:
|
|
526
|
+
arr = raster_data_dict["arr"]
|
|
527
|
+
except KeyError:
|
|
528
|
+
continue
|
|
529
|
+
if (arr.shape) == 1:
|
|
530
|
+
continue
|
|
531
|
+
if hasattr(arr, "mask"):
|
|
532
|
+
arr = arr.data
|
|
533
|
+
if "bool" in str(arr.dtype):
|
|
534
|
+
arr = np.where(arr, 1, 0)
|
|
535
|
+
# if np.max(arr[~np.isnan(arr)]) > 255:
|
|
536
|
+
# arr = (arr - np.min(arr)) / (np.max(arr) - np.min(arr))
|
|
537
|
+
try:
|
|
538
|
+
arr = (arr - np.min(arr)) / (np.max(arr) - np.min(arr))
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
label = raster_data_dict["label"]
|
|
543
|
+
bounds = raster_data_dict["bounds"]
|
|
544
|
+
if raster_data_dict["cmap"] is not None:
|
|
545
|
+
kwargs = {"colormap": raster_data_dict["cmap"]}
|
|
546
|
+
else:
|
|
547
|
+
kwargs = {}
|
|
548
|
+
minx, miny, maxx, maxy = bounds
|
|
549
|
+
image_overlay = folium.raster_layers.ImageOverlay(
|
|
550
|
+
arr,
|
|
551
|
+
bounds=[[miny, minx], [maxy, maxx]],
|
|
552
|
+
show=self._show_rasters,
|
|
553
|
+
**kwargs,
|
|
554
|
+
)
|
|
555
|
+
image_overlay.layer_name = Path(label).stem
|
|
556
|
+
image_overlay.add_to(self.map)
|
|
557
|
+
|
|
558
|
+
def save(self, path: str) -> None:
|
|
559
|
+
"""Save the map to local disk as an html document."""
|
|
560
|
+
with open(path, "w") as f:
|
|
561
|
+
f.write(self.map._repr_html_())
|
|
562
|
+
|
|
413
563
|
def _explore(self, **kwargs) -> None:
|
|
414
564
|
self.kwargs = self.kwargs | kwargs
|
|
415
565
|
|
|
566
|
+
if self._show_was_none and len([gdf for gdf in self._gdfs if len(gdf)]) > 6:
|
|
567
|
+
self.show = [False] * len(self._gdfs)
|
|
568
|
+
|
|
416
569
|
if self._is_categorical:
|
|
417
570
|
self._create_categorical_map()
|
|
418
571
|
else:
|
|
419
572
|
self._create_continous_map()
|
|
420
573
|
|
|
421
|
-
if self.
|
|
422
|
-
with open(
|
|
574
|
+
if self.out_path:
|
|
575
|
+
with open(
|
|
576
|
+
os.getcwd() + "/" + self.out_path.strip(".html") + ".html", "w"
|
|
577
|
+
) as f:
|
|
423
578
|
f.write(self.map._repr_html_())
|
|
424
579
|
elif self.browser:
|
|
425
580
|
run_html_server(self.map._repr_html_())
|
|
@@ -467,16 +622,40 @@ class Explore(Map):
|
|
|
467
622
|
return gdf
|
|
468
623
|
|
|
469
624
|
def _update_column(self) -> None:
|
|
625
|
+
if not self._gdfs:
|
|
626
|
+
return
|
|
470
627
|
self._is_categorical = self._check_if_categorical()
|
|
471
628
|
self._fillna_if_col_is_missing()
|
|
472
629
|
self._gdf = pd.concat(self._gdfs, ignore_index=True)
|
|
473
630
|
|
|
631
|
+
def _get_bounds(
|
|
632
|
+
self, gdf: GeoDataFrame
|
|
633
|
+
) -> tuple[float, float, float, float] | None:
|
|
634
|
+
if not len(gdf) or all(x is None for x in gdf.total_bounds):
|
|
635
|
+
try:
|
|
636
|
+
return get_total_bounds([x["bounds"] for x in self.raster_data])
|
|
637
|
+
except Exception:
|
|
638
|
+
return None
|
|
639
|
+
|
|
640
|
+
return gdf.total_bounds
|
|
641
|
+
|
|
474
642
|
def _create_categorical_map(self) -> None:
|
|
475
|
-
self.
|
|
643
|
+
self._make_categories_colors_dict()
|
|
644
|
+
if self._gdf is not None and len(self._gdf):
|
|
645
|
+
self._fix_nans()
|
|
646
|
+
gdf = self._prepare_gdf_for_map(self._gdf)
|
|
647
|
+
else:
|
|
648
|
+
gdf = GeoDataFrame({"geometry": [], self._column: []})
|
|
476
649
|
|
|
477
|
-
|
|
650
|
+
self._load_rasters_as_images()
|
|
651
|
+
|
|
652
|
+
bounds = self._get_bounds(gdf)
|
|
653
|
+
|
|
654
|
+
if bounds is None:
|
|
655
|
+
self.map = None
|
|
656
|
+
return
|
|
478
657
|
self.map = self._make_folium_map(
|
|
479
|
-
bounds=
|
|
658
|
+
bounds=bounds,
|
|
480
659
|
max_zoom=self.max_zoom,
|
|
481
660
|
popup=self.popup,
|
|
482
661
|
prefer_canvas=self.prefer_canvas,
|
|
@@ -487,8 +666,6 @@ class Explore(Map):
|
|
|
487
666
|
if not len(gdf):
|
|
488
667
|
continue
|
|
489
668
|
|
|
490
|
-
f = folium.FeatureGroup(name=label)
|
|
491
|
-
|
|
492
669
|
gdf = self._to_single_geom_type(gdf)
|
|
493
670
|
gdf = self._prepare_gdf_for_map(gdf)
|
|
494
671
|
|
|
@@ -506,9 +683,10 @@ class Explore(Map):
|
|
|
506
683
|
)
|
|
507
684
|
gjs.layer_name = label
|
|
508
685
|
|
|
509
|
-
gjs.add_to(f)
|
|
510
686
|
gjs.add_to(self.map)
|
|
511
687
|
|
|
688
|
+
self._rasters_to_background_maps()
|
|
689
|
+
|
|
512
690
|
_categorical_legend(
|
|
513
691
|
self.map,
|
|
514
692
|
self._column,
|
|
@@ -532,9 +710,15 @@ class Explore(Map):
|
|
|
532
710
|
n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
|
|
533
711
|
unique_colors = self._get_continous_colors(n=n_colors)
|
|
534
712
|
|
|
713
|
+
self._load_rasters_as_images()
|
|
714
|
+
|
|
535
715
|
gdf = self._prepare_gdf_for_map(self._gdf)
|
|
716
|
+
bounds = self._get_bounds(gdf)
|
|
717
|
+
if bounds is None:
|
|
718
|
+
self.map = None
|
|
719
|
+
return
|
|
536
720
|
self.map = self._make_folium_map(
|
|
537
|
-
bounds=
|
|
721
|
+
bounds=bounds,
|
|
538
722
|
max_zoom=self.max_zoom,
|
|
539
723
|
popup=self.popup,
|
|
540
724
|
prefer_canvas=self.prefer_canvas,
|
|
@@ -552,7 +736,6 @@ class Explore(Map):
|
|
|
552
736
|
for gdf, label, show in zip(self._gdfs, self.labels, self.show, strict=True):
|
|
553
737
|
if not len(gdf):
|
|
554
738
|
continue
|
|
555
|
-
f = folium.FeatureGroup(name=label)
|
|
556
739
|
|
|
557
740
|
gdf = self._to_single_geom_type(gdf)
|
|
558
741
|
gdf = self._prepare_gdf_for_map(gdf)
|
|
@@ -574,12 +757,15 @@ class Explore(Map):
|
|
|
574
757
|
**{
|
|
575
758
|
key: value
|
|
576
759
|
for key, value in self.kwargs.items()
|
|
577
|
-
if key not in ["title"]
|
|
760
|
+
if key not in ["title", "tiles"]
|
|
578
761
|
},
|
|
579
762
|
)
|
|
580
763
|
|
|
581
|
-
|
|
582
|
-
|
|
764
|
+
gjs.layer_name = label
|
|
765
|
+
|
|
766
|
+
gjs.add_to(self.map)
|
|
767
|
+
|
|
768
|
+
self._rasters_to_background_maps()
|
|
583
769
|
|
|
584
770
|
self.map.add_child(colorbar)
|
|
585
771
|
self.map.add_child(folium.LayerControl())
|
|
@@ -754,7 +940,8 @@ class Explore(Map):
|
|
|
754
940
|
)
|
|
755
941
|
|
|
756
942
|
if gdf.crs is None:
|
|
757
|
-
|
|
943
|
+
pass
|
|
944
|
+
# kwargs["crs"] = "Simple"
|
|
758
945
|
elif not gdf.crs.equals(4326):
|
|
759
946
|
gdf = gdf.to_crs(4326)
|
|
760
947
|
|
|
@@ -865,6 +1052,163 @@ class Explore(Map):
|
|
|
865
1052
|
**kwargs,
|
|
866
1053
|
)
|
|
867
1054
|
|
|
1055
|
+
def _image_collection_to_background_map(
|
|
1056
|
+
self,
|
|
1057
|
+
image_collection: ImageCollection | Image | Band,
|
|
1058
|
+
mask: Any | None,
|
|
1059
|
+
name: str,
|
|
1060
|
+
max_images: int,
|
|
1061
|
+
n_added_images: int,
|
|
1062
|
+
rbg_bands: list[str] = (["B02", "B03", "B04"], ["B2", "B3", "B4"]),
|
|
1063
|
+
) -> tuple[list[dict], int]:
|
|
1064
|
+
out = []
|
|
1065
|
+
|
|
1066
|
+
if all(isinstance(x, str) for x in rbg_bands):
|
|
1067
|
+
rbg_bands = (rbg_bands,)
|
|
1068
|
+
|
|
1069
|
+
if isinstance(image_collection, ImageCollection):
|
|
1070
|
+
images = image_collection.images
|
|
1071
|
+
name = None
|
|
1072
|
+
elif isinstance(image_collection, Image):
|
|
1073
|
+
img = image_collection
|
|
1074
|
+
if not _intersects_if_not_none_or_empty(
|
|
1075
|
+
mask, img.bounds
|
|
1076
|
+
): # is not None and not to_shapely(mask).intersects(
|
|
1077
|
+
# to_shapely(img.bounds)
|
|
1078
|
+
# ):
|
|
1079
|
+
return out, n_added_images
|
|
1080
|
+
|
|
1081
|
+
if len(img) == 1:
|
|
1082
|
+
band = next(iter(img))
|
|
1083
|
+
raster_data_dict = {}
|
|
1084
|
+
out.append(raster_data_dict)
|
|
1085
|
+
name = _determine_label(band, name, out, n_added_images)
|
|
1086
|
+
_single_band_to_arr(band, mask, name, raster_data_dict)
|
|
1087
|
+
n_added_images += 1
|
|
1088
|
+
return out, n_added_images
|
|
1089
|
+
elif len(img) < 3:
|
|
1090
|
+
raster_data_dict = {}
|
|
1091
|
+
out.append(raster_data_dict)
|
|
1092
|
+
for band in img:
|
|
1093
|
+
name = _determine_label(band, None, out, n_added_images)
|
|
1094
|
+
_single_band_to_arr(band, mask, name, raster_data_dict)
|
|
1095
|
+
n_added_images += 1
|
|
1096
|
+
return out, n_added_images
|
|
1097
|
+
else:
|
|
1098
|
+
images = [image_collection]
|
|
1099
|
+
|
|
1100
|
+
elif isinstance(image_collection, Band):
|
|
1101
|
+
band = image_collection
|
|
1102
|
+
|
|
1103
|
+
if not _intersects_if_not_none_or_empty(
|
|
1104
|
+
mask, band.bounds
|
|
1105
|
+
): # mask is not None and not to_shapely(mask).intersects(
|
|
1106
|
+
# to_shapely(band.bounds)
|
|
1107
|
+
# ):
|
|
1108
|
+
return out, n_added_images
|
|
1109
|
+
|
|
1110
|
+
raster_data_dict = {}
|
|
1111
|
+
out.append(raster_data_dict)
|
|
1112
|
+
_single_band_to_arr(band, mask, name, raster_data_dict)
|
|
1113
|
+
return out, n_added_images
|
|
1114
|
+
|
|
1115
|
+
else:
|
|
1116
|
+
raise TypeError(type(image_collection))
|
|
1117
|
+
|
|
1118
|
+
if max(len(out), len(images)) + n_added_images > max_images:
|
|
1119
|
+
warnings.warn(
|
|
1120
|
+
f"Showing only a sample of {max_images}. Set 'max_images.", stacklevel=1
|
|
1121
|
+
)
|
|
1122
|
+
self._show_rasters = False
|
|
1123
|
+
random.shuffle(images)
|
|
1124
|
+
|
|
1125
|
+
images = images[: (max_images - n_added_images)]
|
|
1126
|
+
images = (
|
|
1127
|
+
list(sorted([img for img in images if img.date is not None]))
|
|
1128
|
+
+ sorted(
|
|
1129
|
+
[
|
|
1130
|
+
img
|
|
1131
|
+
for img in images
|
|
1132
|
+
if img.date is None and img.path is not None
|
|
1133
|
+
],
|
|
1134
|
+
key=lambda x: x.path,
|
|
1135
|
+
)
|
|
1136
|
+
+ [img for img in images if img.date is None and img.path is None]
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
for image in images:
|
|
1140
|
+
|
|
1141
|
+
if not _intersects_if_not_none_or_empty(
|
|
1142
|
+
mask, image.bounds
|
|
1143
|
+
): # mask is not None and not to_shapely(mask).intersects(
|
|
1144
|
+
# to_shapely(image.bounds)
|
|
1145
|
+
# ):
|
|
1146
|
+
continue
|
|
1147
|
+
|
|
1148
|
+
raster_data_dict = {}
|
|
1149
|
+
out.append(raster_data_dict)
|
|
1150
|
+
|
|
1151
|
+
if len(image) < 3:
|
|
1152
|
+
for band in image:
|
|
1153
|
+
name = _determine_label(band, None, out, n_added_images)
|
|
1154
|
+
_single_band_to_arr(band, mask, name, raster_data_dict)
|
|
1155
|
+
n_added_images += 1
|
|
1156
|
+
continue
|
|
1157
|
+
|
|
1158
|
+
for red, blue, green in rbg_bands:
|
|
1159
|
+
try:
|
|
1160
|
+
red_band = image[red].load(indexes=1, bounds=mask)
|
|
1161
|
+
except KeyError:
|
|
1162
|
+
continue
|
|
1163
|
+
try:
|
|
1164
|
+
blue_band = image[blue].load(indexes=1, bounds=mask)
|
|
1165
|
+
except KeyError:
|
|
1166
|
+
continue
|
|
1167
|
+
try:
|
|
1168
|
+
green_band = image[green].load(indexes=1, bounds=mask)
|
|
1169
|
+
except KeyError:
|
|
1170
|
+
continue
|
|
1171
|
+
break
|
|
1172
|
+
|
|
1173
|
+
crs = red_band.crs
|
|
1174
|
+
|
|
1175
|
+
bounds: tuple = (
|
|
1176
|
+
_any_to_bbox_crs4326(mask, crs)
|
|
1177
|
+
if mask is not None
|
|
1178
|
+
else (
|
|
1179
|
+
union_all(
|
|
1180
|
+
gpd.GeoSeries(box(*red_band.bounds), crs=crs)
|
|
1181
|
+
.to_crs(4326)
|
|
1182
|
+
.geometry.values
|
|
1183
|
+
).bounds
|
|
1184
|
+
)
|
|
1185
|
+
)
|
|
1186
|
+
|
|
1187
|
+
red_band = red_band.values
|
|
1188
|
+
blue_band = blue_band.values
|
|
1189
|
+
green_band = green_band.values
|
|
1190
|
+
|
|
1191
|
+
if red_band.shape[0] == 0:
|
|
1192
|
+
continue
|
|
1193
|
+
if blue_band.shape[0] == 0:
|
|
1194
|
+
continue
|
|
1195
|
+
if green_band.shape[0] == 0:
|
|
1196
|
+
continue
|
|
1197
|
+
|
|
1198
|
+
# to 3d array in shape (x, y, 3)
|
|
1199
|
+
rbg_image = np.stack([red_band, blue_band, green_band], axis=2)
|
|
1200
|
+
|
|
1201
|
+
raster_data_dict["arr"] = rbg_image
|
|
1202
|
+
raster_data_dict["bounds"] = bounds
|
|
1203
|
+
raster_data_dict["cmap"] = None
|
|
1204
|
+
raster_data_dict["label"] = _determine_label(
|
|
1205
|
+
image, name, out, n_added_images
|
|
1206
|
+
)
|
|
1207
|
+
|
|
1208
|
+
n_added_images += 1
|
|
1209
|
+
|
|
1210
|
+
return out, n_added_images
|
|
1211
|
+
|
|
868
1212
|
|
|
869
1213
|
def _tooltip_popup(
|
|
870
1214
|
type_: str, fields: Any, gdf: GeoDataFrame, **kwargs
|
|
@@ -893,6 +1237,47 @@ def _tooltip_popup(
|
|
|
893
1237
|
return folium.GeoJsonPopup(fields, **kwargs)
|
|
894
1238
|
|
|
895
1239
|
|
|
1240
|
+
def _intersects_if_not_none_or_empty(obj: Any, other: Any) -> bool:
|
|
1241
|
+
if obj is None:
|
|
1242
|
+
return True
|
|
1243
|
+
obj = to_shapely(obj)
|
|
1244
|
+
if obj is None or obj.is_empty:
|
|
1245
|
+
return True
|
|
1246
|
+
return obj.intersects(to_shapely(other))
|
|
1247
|
+
|
|
1248
|
+
|
|
1249
|
+
def _determine_label(
|
|
1250
|
+
obj: Image | Band | ImageCollection, obj_name: str | None, out: list[dict], i: int
|
|
1251
|
+
) -> str:
|
|
1252
|
+
# Prefer the object's name
|
|
1253
|
+
if obj_name:
|
|
1254
|
+
# Avoid the generic label e.g. Image(1)
|
|
1255
|
+
does_not_have_generic_name = (
|
|
1256
|
+
re.sub("(\d+)", "", obj_name) != f"{obj.__class__.__name__}()"
|
|
1257
|
+
)
|
|
1258
|
+
if does_not_have_generic_name:
|
|
1259
|
+
return obj_name
|
|
1260
|
+
# try:
|
|
1261
|
+
# if obj.tile and obj.date:
|
|
1262
|
+
# name = f"{obj.tile}_{obj.date[:8]}"
|
|
1263
|
+
# except (ValueError, AttributeError):
|
|
1264
|
+
# name = None
|
|
1265
|
+
|
|
1266
|
+
try:
|
|
1267
|
+
# Images/Bands/Collections constructed from arrays have no path stems
|
|
1268
|
+
if obj.stem:
|
|
1269
|
+
name = obj.stem
|
|
1270
|
+
else:
|
|
1271
|
+
name = str(obj)[:23]
|
|
1272
|
+
except (AttributeError, ValueError):
|
|
1273
|
+
name = str(obj)[:23]
|
|
1274
|
+
|
|
1275
|
+
if name in [x["label"] for x in out if "label" in x]:
|
|
1276
|
+
name += f"_{i}"
|
|
1277
|
+
|
|
1278
|
+
return name
|
|
1279
|
+
|
|
1280
|
+
|
|
896
1281
|
def _categorical_legend(
|
|
897
1282
|
m: folium.Map, title: str, categories: list[str], colors: list[str]
|
|
898
1283
|
) -> None:
|