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.
Files changed (50) hide show
  1. sgis/__init__.py +20 -9
  2. sgis/debug_config.py +24 -0
  3. sgis/exceptions.py +2 -2
  4. sgis/geopandas_tools/bounds.py +33 -36
  5. sgis/geopandas_tools/buffer_dissolve_explode.py +136 -35
  6. sgis/geopandas_tools/centerlines.py +4 -91
  7. sgis/geopandas_tools/cleaning.py +1576 -583
  8. sgis/geopandas_tools/conversion.py +38 -19
  9. sgis/geopandas_tools/duplicates.py +29 -8
  10. sgis/geopandas_tools/general.py +263 -100
  11. sgis/geopandas_tools/geometry_types.py +4 -4
  12. sgis/geopandas_tools/neighbors.py +19 -15
  13. sgis/geopandas_tools/overlay.py +2 -2
  14. sgis/geopandas_tools/point_operations.py +5 -5
  15. sgis/geopandas_tools/polygon_operations.py +510 -105
  16. sgis/geopandas_tools/polygons_as_rings.py +40 -8
  17. sgis/geopandas_tools/sfilter.py +29 -12
  18. sgis/helpers.py +3 -3
  19. sgis/io/dapla_functions.py +238 -19
  20. sgis/io/read_parquet.py +1 -1
  21. sgis/maps/examine.py +27 -12
  22. sgis/maps/explore.py +450 -65
  23. sgis/maps/legend.py +177 -76
  24. sgis/maps/map.py +206 -103
  25. sgis/maps/maps.py +178 -105
  26. sgis/maps/thematicmap.py +243 -83
  27. sgis/networkanalysis/_service_area.py +6 -1
  28. sgis/networkanalysis/closing_network_holes.py +2 -2
  29. sgis/networkanalysis/cutting_lines.py +15 -8
  30. sgis/networkanalysis/directednetwork.py +1 -1
  31. sgis/networkanalysis/finding_isolated_networks.py +15 -8
  32. sgis/networkanalysis/networkanalysis.py +17 -19
  33. sgis/networkanalysis/networkanalysisrules.py +1 -1
  34. sgis/networkanalysis/traveling_salesman.py +1 -1
  35. sgis/parallel/parallel.py +64 -27
  36. sgis/raster/__init__.py +0 -6
  37. sgis/raster/base.py +208 -0
  38. sgis/raster/cube.py +54 -8
  39. sgis/raster/image_collection.py +3257 -0
  40. sgis/raster/indices.py +17 -5
  41. sgis/raster/raster.py +138 -243
  42. sgis/raster/sentinel_config.py +120 -0
  43. sgis/raster/zonal.py +0 -1
  44. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/METADATA +6 -7
  45. ssb_sgis-1.0.4.dist-info/RECORD +62 -0
  46. sgis/raster/methods_as_functions.py +0 -0
  47. sgis/raster/torchgeo.py +0 -171
  48. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  49. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.4.dist-info}/LICENSE +0 -0
  50. {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
- save: str | None = None,
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
- save: Optional file path to an html file. The map will then
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 passed to
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.save = save
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
- show_was_none = True
291
+ self._show_was_none = True
228
292
  show = True
229
293
  else:
230
- show_was_none = False
294
+ self._show_was_none = False
231
295
 
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
296
+ new_gdfs = {}
297
+ self.rasters = {}
298
+ for i, gdf in enumerate(gdfs):
299
+ name = _determine_best_name(gdf, column, i)
238
300
 
239
- super().__init__(*gdfs, column=column, show=show, **kwargs)
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._gdf = pd.concat(new_gdfs, ignore_index=True)
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 show_was_none and len(self._gdfs) > 6:
283
- self.show = [False] * len(self._gdfs)
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
- self.kwargs["crs"] = "Simple"
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
- if not any(len(gdf) for gdf in self._gdfs) and not len(self.raster_datasets):
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 = 1000,
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
- if sample_from_first:
361
- sample = self._gdfs[0].sample(1)
362
- else:
363
- sample = self._gdf.sample(1)
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
- if get_geom_type(sample) == "polygon":
370
- random_point = sample.sample_points(size=1)
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
- # if point or mixed geometries
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
- gdfs: tuple[GeoDataFrame] = ()
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._get_unique_values()
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._gdf = pd.concat(gdfs, ignore_index=True)
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.save:
422
- with open(os.getcwd() + "/" + self.save.strip(".html") + ".html", "w") as f:
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._get_categorical_colors()
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
- gdf = self._prepare_gdf_for_map(self._gdf)
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=gdf.total_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=gdf.total_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
- f.add_child(gjs)
582
- self.map.add_child(f)
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
- kwargs["crs"] = "Simple"
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: