ssb-sgis 1.0.2__py3-none-any.whl → 1.0.3__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 (42) hide show
  1. sgis/__init__.py +10 -6
  2. sgis/exceptions.py +2 -2
  3. sgis/geopandas_tools/bounds.py +17 -15
  4. sgis/geopandas_tools/buffer_dissolve_explode.py +24 -5
  5. sgis/geopandas_tools/conversion.py +15 -6
  6. sgis/geopandas_tools/duplicates.py +2 -2
  7. sgis/geopandas_tools/general.py +9 -5
  8. sgis/geopandas_tools/geometry_types.py +3 -3
  9. sgis/geopandas_tools/neighbors.py +3 -3
  10. sgis/geopandas_tools/point_operations.py +2 -2
  11. sgis/geopandas_tools/polygon_operations.py +5 -5
  12. sgis/geopandas_tools/sfilter.py +3 -3
  13. sgis/helpers.py +3 -3
  14. sgis/io/read_parquet.py +1 -1
  15. sgis/maps/examine.py +16 -2
  16. sgis/maps/explore.py +370 -57
  17. sgis/maps/legend.py +164 -72
  18. sgis/maps/map.py +184 -90
  19. sgis/maps/maps.py +92 -90
  20. sgis/maps/thematicmap.py +236 -83
  21. sgis/networkanalysis/closing_network_holes.py +2 -2
  22. sgis/networkanalysis/cutting_lines.py +3 -3
  23. sgis/networkanalysis/directednetwork.py +1 -1
  24. sgis/networkanalysis/finding_isolated_networks.py +2 -2
  25. sgis/networkanalysis/networkanalysis.py +7 -7
  26. sgis/networkanalysis/networkanalysisrules.py +1 -1
  27. sgis/networkanalysis/traveling_salesman.py +1 -1
  28. sgis/parallel/parallel.py +39 -19
  29. sgis/raster/__init__.py +0 -6
  30. sgis/raster/cube.py +51 -5
  31. sgis/raster/image_collection.py +2560 -0
  32. sgis/raster/indices.py +14 -5
  33. sgis/raster/raster.py +131 -236
  34. sgis/raster/sentinel_config.py +104 -0
  35. sgis/raster/zonal.py +0 -1
  36. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +1 -1
  37. ssb_sgis-1.0.3.dist-info/RECORD +61 -0
  38. sgis/raster/methods_as_functions.py +0 -0
  39. sgis/raster/torchgeo.py +0 -171
  40. ssb_sgis-1.0.2.dist-info/RECORD +0 -61
  41. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +0 -0
  42. {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.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,37 @@ 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
28
34
  from shapely.geometry import LineString
29
35
 
36
+ from ..geopandas_tools.bounds import get_total_bounds
37
+ from ..geopandas_tools.conversion import to_bbox
30
38
  from ..geopandas_tools.conversion import to_gdf
39
+ from ..geopandas_tools.conversion import to_shapely
31
40
  from ..geopandas_tools.general import clean_geoms
32
41
  from ..geopandas_tools.general import make_all_singlepart
33
42
  from ..geopandas_tools.geometry_types import get_geom_type
34
43
  from ..geopandas_tools.geometry_types import to_single_geom_type
44
+
45
+ try:
46
+ from ..raster.image_collection import Band
47
+ from ..raster.image_collection import Image
48
+ from ..raster.image_collection import ImageCollection
49
+ except ImportError:
50
+
51
+ class Band:
52
+ """Placeholder."""
53
+
54
+ class Image:
55
+ """Placeholder."""
56
+
57
+ class ImageCollection:
58
+ """Placeholder."""
59
+
60
+
35
61
  from .httpserver import run_html_server
36
62
  from .map import Map
63
+ from .map import _determine_best_name
37
64
  from .tilesources import kartverket
38
65
  from .tilesources import xyz
39
66
 
@@ -153,6 +180,167 @@ def to_tile(tile: str | xyzservices.TileProvider, max_zoom: int) -> folium.TileL
153
180
  return folium.TileLayer(provider, name=name, attr=attr, max_zoom=max_zoom)
154
181
 
155
182
 
183
+ def _single_band_to_arr(band, mask, name, raster_data_dict):
184
+ try:
185
+ arr = band.values
186
+ except (ValueError, AttributeError):
187
+ arr = band.load(indexes=1, bounds=mask).values
188
+ bounds: tuple = (
189
+ _any_to_bbox_crs4326(mask, band.crs)
190
+ if mask is not None
191
+ else gpd.GeoSeries(box(*band.bounds), crs=band.crs)
192
+ .to_crs(4326)
193
+ .unary_union.bounds
194
+ )
195
+ # if np.max(arr) > 0:
196
+ # arr = arr / 255
197
+ raster_data_dict["cmap"] = plt.get_cmap(band.cmap) if band.cmap else None
198
+ raster_data_dict["arr"] = arr
199
+ raster_data_dict["bounds"] = bounds
200
+ raster_data_dict["label"] = name
201
+
202
+
203
+ def _any_to_bbox_crs4326(obj, crs):
204
+ return to_bbox(to_gdf(obj, crs).to_crs(4326))
205
+
206
+
207
+ def _image_collection_to_background_map(
208
+ image_collection: ImageCollection | Image | Band,
209
+ mask: Any | None,
210
+ name: str,
211
+ max_images: int,
212
+ n_added_images: int,
213
+ rbg_bands: list[str] = (["B02", "B03", "B04"], ["B2", "B3", "B4"]),
214
+ ) -> list[dict]:
215
+ out = []
216
+
217
+ if all(isinstance(x, str) for x in rbg_bands):
218
+ rbg_bands = (rbg_bands,)
219
+
220
+ # red, blue, green = rbg_bands
221
+ if isinstance(image_collection, ImageCollection):
222
+ images = image_collection.images
223
+ name = None
224
+ elif isinstance(image_collection, Image):
225
+ img = image_collection
226
+ if mask is not None and not to_shapely(mask).intersects(to_shapely(img.bounds)):
227
+ return out
228
+
229
+ if len(img) == 1:
230
+ band = next(iter(img))
231
+ raster_data_dict = {}
232
+ out.append(raster_data_dict)
233
+ name = _determine_label(band, name, out, 0)
234
+ _single_band_to_arr(band, mask, name, raster_data_dict)
235
+ return out
236
+ elif len(img) < 3:
237
+ raster_data_dict = {}
238
+ out.append(raster_data_dict)
239
+ for i, band in enumerate(img):
240
+ name = _determine_label(band, None, out, i)
241
+ _single_band_to_arr(band, mask, name, raster_data_dict)
242
+ return out
243
+ else:
244
+ images = [image_collection]
245
+
246
+ elif isinstance(image_collection, Band):
247
+ band = image_collection
248
+
249
+ if mask is not None and not to_shapely(mask).intersects(
250
+ to_shapely(band.bounds)
251
+ ):
252
+ return out
253
+
254
+ raster_data_dict = {}
255
+ out.append(raster_data_dict)
256
+ _single_band_to_arr(band, mask, name, raster_data_dict)
257
+ return out
258
+
259
+ else:
260
+ raise TypeError(type(image_collection))
261
+
262
+ if len(images) + n_added_images > max_images:
263
+ warnings.warn(
264
+ f"Showing only a sample of {max_images}. Set 'max_images.", stacklevel=1
265
+ )
266
+ random.shuffle(images)
267
+ images = images[: (max_images - n_added_images)]
268
+ try:
269
+ images = list(sorted(images))
270
+ except Exception:
271
+ pass
272
+
273
+ i = -1
274
+ for image in images:
275
+ i += 1
276
+ if mask is not None and not to_shapely(mask).intersects(
277
+ to_shapely(image.bounds)
278
+ ):
279
+ continue
280
+
281
+ raster_data_dict = {}
282
+ out.append(raster_data_dict)
283
+
284
+ if len(image) < 3:
285
+ for band in image:
286
+ name = _determine_label(band, None, out, i)
287
+ _single_band_to_arr(band, mask, name, raster_data_dict)
288
+ i += 1
289
+ continue
290
+
291
+ for red, blue, green in rbg_bands:
292
+ try:
293
+ red_band = image[red].load(indexes=1, bounds=mask).values
294
+ except KeyError:
295
+ continue
296
+ try:
297
+ blue_band = image[blue].load(indexes=1, bounds=mask).values
298
+ except KeyError:
299
+ continue
300
+ try:
301
+ green_band = image[green].load(indexes=1, bounds=mask).values
302
+ except KeyError:
303
+ continue
304
+ break
305
+
306
+ if mask is not None:
307
+ print(mask)
308
+ print(to_gdf(mask).area.sum())
309
+ print(to_gdf(image.bounds).area.sum())
310
+ print(red_band.shape)
311
+ print(image.bounds)
312
+
313
+ if red_band.shape[0] == 0:
314
+ continue
315
+ if blue_band.shape[0] == 0:
316
+ continue
317
+ if green_band.shape[0] == 0:
318
+ continue
319
+
320
+ crs = image.crs
321
+ bounds: tuple = (
322
+ _any_to_bbox_crs4326(mask, crs)
323
+ if mask is not None
324
+ else (
325
+ gpd.GeoSeries(box(*image.bounds), crs=crs)
326
+ .to_crs(4326)
327
+ .unary_union.bounds
328
+ )
329
+ )
330
+ print(bounds)
331
+ print(to_gdf(bounds).area.sum())
332
+
333
+ # to 3d array in shape (x, y, 3)
334
+ rbg_image = np.stack([red_band, blue_band, green_band], axis=2)
335
+
336
+ raster_data_dict["arr"] = rbg_image
337
+ raster_data_dict["bounds"] = bounds
338
+ raster_data_dict["cmap"] = None
339
+ raster_data_dict["label"] = _determine_label(image, name, out, i)
340
+
341
+ return out
342
+
343
+
156
344
  class Explore(Map):
157
345
  """Class for displaying and saving html maps of multiple GeoDataFrames."""
158
346
 
@@ -176,10 +364,11 @@ class Explore(Map):
176
364
  prefer_canvas: bool = True,
177
365
  measure_control: bool = True,
178
366
  geocoder: bool = False,
179
- save: str | None = None,
367
+ out_path: str | None = None,
180
368
  show: bool | Iterable[bool] | None = None,
181
369
  text: str | None = None,
182
370
  decimals: int = 6,
371
+ max_images: int = 15,
183
372
  **kwargs,
184
373
  ) -> None:
185
374
  """Initialiser.
@@ -196,15 +385,18 @@ class Explore(Map):
196
385
  prefer_canvas: Option.
197
386
  measure_control: Whether to include measurement box.
198
387
  geocoder: Whether to include search bar for addresses.
199
- save: Optional file path to an html file. The map will then
388
+ out_path: Optional file path to an html file. The map will then
200
389
  be saved instead of displayed.
201
390
  show: Whether to show or hide the data upon creating the map.
202
391
  If False, the data can be toggled on later. 'show' can also be
203
392
  a sequence of boolean values the same length as the number of
204
393
  GeoDataFrames.
394
+ max_images: Maximum number of images (Image, ImageCollection, Band) to show per
395
+ map. Defaults to 15.
205
396
  text: Optional text for a text box in the map.
206
397
  decimals: Number of decimals in the coordinates.
207
- **kwargs: Additional keyword arguments passed to
398
+ **kwargs: Additional keyword arguments. Can also be geometry-like objects
399
+ where the key is the label.
208
400
  """
209
401
  self.popup = popup
210
402
  self.max_zoom = max_zoom
@@ -212,10 +404,12 @@ class Explore(Map):
212
404
  self.prefer_canvas = prefer_canvas
213
405
  self.measure_control = measure_control
214
406
  self.geocoder = geocoder
215
- self.save = save
407
+ self.out_path = out_path
216
408
  self.mask = mask
217
409
  self.text = text
218
410
  self.decimals = decimals
411
+ self.max_images = max_images
412
+ self.legend = None
219
413
 
220
414
  self.browser = browser
221
415
  if not self.browser and "show_in_browser" in kwargs:
@@ -229,14 +423,30 @@ class Explore(Map):
229
423
  else:
230
424
  show_was_none = False
231
425
 
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
426
+ new_gdfs = {}
427
+ self.rasters = {}
428
+ for i, gdf in enumerate(gdfs):
429
+ name = _determine_best_name(gdf, column, i)
238
430
 
239
- super().__init__(*gdfs, column=column, show=show, **kwargs)
431
+ if name in new_gdfs or name in self.rasters:
432
+ name += str(i)
433
+
434
+ if isinstance(gdf, (ImageCollection | Image | Band)):
435
+ self.rasters[name] = gdf.copy()
436
+ continue
437
+ try:
438
+ new_gdfs[name] = to_gdf(gdf)
439
+ except Exception:
440
+ continue
441
+
442
+ new_kwargs = {}
443
+ for key, value in kwargs.items():
444
+ if isinstance(value, (ImageCollection | Image | Band)):
445
+ self.rasters[key] = value
446
+ else:
447
+ new_kwargs[key] = value
448
+
449
+ super().__init__(column=column, show=show, **new_kwargs, **new_gdfs)
240
450
 
241
451
  if self.gdfs is None:
242
452
  return
@@ -273,10 +483,13 @@ class Explore(Map):
273
483
  gdf.index = gdf.index.astype(str)
274
484
  except Exception:
275
485
  pass
276
- new_gdfs.append(gdf)
486
+ new_gdfs.append(to_gdf(gdf))
277
487
  show_new.append(show)
278
488
  self._gdfs = new_gdfs
279
- self._gdf = pd.concat(new_gdfs, ignore_index=True)
489
+ if self._gdfs:
490
+ self._gdf = pd.concat(new_gdfs, ignore_index=True)
491
+ else:
492
+ self._gdf = GeoDataFrame({"geometry": [], self._column: []})
280
493
  self.show = show_new
281
494
 
282
495
  if show_was_none and len(self._gdfs) > 6:
@@ -288,11 +501,11 @@ class Explore(Map):
288
501
  else:
289
502
  if not self._cmap:
290
503
  self._cmap = "viridis"
291
- self.cmap_start = kwargs.pop("cmap_start", 0)
292
- self.cmap_stop = kwargs.pop("cmap_stop", 256)
504
+ self.cmap_start = self.kwargs.pop("cmap_start", 0)
505
+ self.cmap_stop = self.kwargs.pop("cmap_stop", 256)
293
506
 
294
- if self._gdf.crs is None:
295
- self.kwargs["crs"] = "Simple"
507
+ # if self._gdf.crs is None:
508
+ # self.kwargs["crs"] = "Simple"
296
509
 
297
510
  self.original_crs = self.gdf.crs
298
511
 
@@ -305,10 +518,16 @@ class Explore(Map):
305
518
  column: str | None = None,
306
519
  center: Any | None = None,
307
520
  size: int | None = None,
521
+ mask: Any | None = None,
308
522
  **kwargs,
309
523
  ) -> None:
310
524
  """Explore all the data."""
311
- if not any(len(gdf) for gdf in self._gdfs) and not len(self.raster_datasets):
525
+ self.mask = mask if mask is not None else self.mask
526
+ if (
527
+ self._gdfs
528
+ and not any(len(gdf) for gdf in self._gdfs)
529
+ and not len(self.rasters)
530
+ ):
312
531
  warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
313
532
  return
314
533
  if column:
@@ -346,9 +565,9 @@ class Explore(Map):
346
565
 
347
566
  def samplemap(
348
567
  self,
349
- size: int = 1000,
568
+ size: int,
569
+ sample: Any,
350
570
  column: str | None = None,
351
- sample_from_first: bool = True,
352
571
  **kwargs,
353
572
  ) -> None:
354
573
  """Explore a sample of the data."""
@@ -357,34 +576,24 @@ class Explore(Map):
357
576
  self._update_column()
358
577
  kwargs.pop("column", None)
359
578
 
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)
579
+ try:
580
+ sample = sample.sample(1)
581
+ except Exception:
582
+ pass
368
583
 
369
- if get_geom_type(sample) == "polygon":
370
- random_point = sample.sample_points(size=1)
584
+ try:
585
+ sample = to_gdf(to_shapely(sample)).explode(ignore_index=True)
586
+ except Exception:
587
+ sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
371
588
 
372
- # if point or mixed geometries
373
- else:
374
- random_point = sample.centroid
589
+ random_point = sample.sample_points(size=1)
375
590
 
376
591
  self.center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
377
592
  print(f"center={self.center}, size={size}")
378
593
 
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)
594
+ mask = random_point.buffer(size)
385
595
 
386
- self._get_unique_values()
387
- self._explore(**kwargs)
596
+ return self.clipmap(mask, column, **kwargs)
388
597
 
389
598
  def clipmap(
390
599
  self,
@@ -393,6 +602,7 @@ class Explore(Map):
393
602
  **kwargs,
394
603
  ) -> None:
395
604
  """Explore the data within a mask extent."""
605
+ self.mask = mask
396
606
  if column:
397
607
  self._column = column
398
608
  self._update_column()
@@ -400,16 +610,55 @@ class Explore(Map):
400
610
 
401
611
  gdfs: tuple[GeoDataFrame] = ()
402
612
  for gdf in self._gdfs:
403
- gdf = gdf.clip(mask)
613
+ gdf = gdf.clip(self.mask)
404
614
  collections = gdf.loc[gdf.geom_type == "GeometryCollection"]
405
615
  if len(collections):
406
616
  collections = make_all_singlepart(collections)
407
617
  gdf = pd.concat([gdf, collections], ignore_index=False)
408
618
  gdfs = gdfs + (gdf,)
409
619
  self._gdfs = gdfs
410
- self._gdf = pd.concat(gdfs, ignore_index=True)
620
+ if self._gdfs:
621
+ self._gdf = pd.concat(self._gdfs, ignore_index=True)
622
+ else:
623
+ self._gdf = GeoDataFrame({"geometry": [], self._column: []})
624
+
411
625
  self._explore(**kwargs)
412
626
 
627
+ def _load_rasters_as_images(self):
628
+ self.raster_data = []
629
+ n_added_images = 0
630
+ for name, value in self.rasters.items():
631
+ data = _image_collection_to_background_map(
632
+ value,
633
+ self.mask,
634
+ name,
635
+ max_images=self.max_images,
636
+ n_added_images=n_added_images,
637
+ )
638
+ n_added_images += len(data)
639
+ self.raster_data += data
640
+
641
+ def _rasters_to_background_maps(self):
642
+ for raster_data_dict in self.raster_data:
643
+ arr = raster_data_dict["arr"]
644
+ label = raster_data_dict["label"]
645
+ bounds = raster_data_dict["bounds"]
646
+ if raster_data_dict["cmap"] is not None:
647
+ kwargs = {"colormap": raster_data_dict["cmap"]}
648
+ else:
649
+ kwargs = {}
650
+ minx, miny, maxx, maxy = bounds
651
+ image_overlay = folium.raster_layers.ImageOverlay(
652
+ arr, bounds=[[miny, minx], [maxy, maxx]], **kwargs
653
+ )
654
+ image_overlay.layer_name = Path(label).stem
655
+ image_overlay.add_to(self.map)
656
+
657
+ def save(self, path: str) -> None:
658
+ """Save the map to local disk as an html document."""
659
+ with open(path, "w") as f:
660
+ f.write(self.map._repr_html_())
661
+
413
662
  def _explore(self, **kwargs) -> None:
414
663
  self.kwargs = self.kwargs | kwargs
415
664
 
@@ -418,8 +667,10 @@ class Explore(Map):
418
667
  else:
419
668
  self._create_continous_map()
420
669
 
421
- if self.save:
422
- with open(os.getcwd() + "/" + self.save.strip(".html") + ".html", "w") as f:
670
+ if self.out_path:
671
+ with open(
672
+ os.getcwd() + "/" + self.out_path.strip(".html") + ".html", "w"
673
+ ) as f:
423
674
  f.write(self.map._repr_html_())
424
675
  elif self.browser:
425
676
  run_html_server(self.map._repr_html_())
@@ -471,12 +722,34 @@ class Explore(Map):
471
722
  self._fillna_if_col_is_missing()
472
723
  self._gdf = pd.concat(self._gdfs, ignore_index=True)
473
724
 
725
+ def _get_bounds(
726
+ self, gdf: GeoDataFrame
727
+ ) -> tuple[float, float, float, float] | None:
728
+ if not len(gdf) or all(x is None for x in gdf.total_bounds):
729
+ try:
730
+ return get_total_bounds([x["bounds"] for x in self.raster_data])
731
+ except Exception:
732
+ return None
733
+
734
+ return gdf.total_bounds
735
+
474
736
  def _create_categorical_map(self) -> None:
475
- self._get_categorical_colors()
737
+ self._make_categories_colors_dict()
738
+ if self._gdf is not None and len(self._gdf):
739
+ self._fix_nans()
740
+ gdf = self._prepare_gdf_for_map(self._gdf)
741
+ else:
742
+ gdf = GeoDataFrame({"geometry": [], self._column: []})
476
743
 
477
- gdf = self._prepare_gdf_for_map(self._gdf)
744
+ self._load_rasters_as_images()
745
+
746
+ bounds = self._get_bounds(gdf)
747
+
748
+ if bounds is None:
749
+ self.map = None
750
+ return
478
751
  self.map = self._make_folium_map(
479
- bounds=gdf.total_bounds,
752
+ bounds=bounds,
480
753
  max_zoom=self.max_zoom,
481
754
  popup=self.popup,
482
755
  prefer_canvas=self.prefer_canvas,
@@ -487,8 +760,6 @@ class Explore(Map):
487
760
  if not len(gdf):
488
761
  continue
489
762
 
490
- f = folium.FeatureGroup(name=label)
491
-
492
763
  gdf = self._to_single_geom_type(gdf)
493
764
  gdf = self._prepare_gdf_for_map(gdf)
494
765
 
@@ -506,9 +777,10 @@ class Explore(Map):
506
777
  )
507
778
  gjs.layer_name = label
508
779
 
509
- gjs.add_to(f)
510
780
  gjs.add_to(self.map)
511
781
 
782
+ self._rasters_to_background_maps()
783
+
512
784
  _categorical_legend(
513
785
  self.map,
514
786
  self._column,
@@ -532,9 +804,15 @@ class Explore(Map):
532
804
  n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
533
805
  unique_colors = self._get_continous_colors(n=n_colors)
534
806
 
807
+ self._load_rasters_as_images()
808
+
535
809
  gdf = self._prepare_gdf_for_map(self._gdf)
810
+ bounds = self._get_bounds(gdf)
811
+ if bounds is None:
812
+ self.map = None
813
+ return
536
814
  self.map = self._make_folium_map(
537
- bounds=gdf.total_bounds,
815
+ bounds=bounds,
538
816
  max_zoom=self.max_zoom,
539
817
  popup=self.popup,
540
818
  prefer_canvas=self.prefer_canvas,
@@ -552,7 +830,6 @@ class Explore(Map):
552
830
  for gdf, label, show in zip(self._gdfs, self.labels, self.show, strict=True):
553
831
  if not len(gdf):
554
832
  continue
555
- f = folium.FeatureGroup(name=label)
556
833
 
557
834
  gdf = self._to_single_geom_type(gdf)
558
835
  gdf = self._prepare_gdf_for_map(gdf)
@@ -574,12 +851,15 @@ class Explore(Map):
574
851
  **{
575
852
  key: value
576
853
  for key, value in self.kwargs.items()
577
- if key not in ["title"]
854
+ if key not in ["title", "tiles"]
578
855
  },
579
856
  )
580
857
 
581
- f.add_child(gjs)
582
- self.map.add_child(f)
858
+ gjs.layer_name = label
859
+
860
+ gjs.add_to(self.map)
861
+
862
+ self._rasters_to_background_maps()
583
863
 
584
864
  self.map.add_child(colorbar)
585
865
  self.map.add_child(folium.LayerControl())
@@ -754,7 +1034,8 @@ class Explore(Map):
754
1034
  )
755
1035
 
756
1036
  if gdf.crs is None:
757
- kwargs["crs"] = "Simple"
1037
+ pass
1038
+ # kwargs["crs"] = "Simple"
758
1039
  elif not gdf.crs.equals(4326):
759
1040
  gdf = gdf.to_crs(4326)
760
1041
 
@@ -893,6 +1174,38 @@ def _tooltip_popup(
893
1174
  return folium.GeoJsonPopup(fields, **kwargs)
894
1175
 
895
1176
 
1177
+ def _determine_label(
1178
+ obj: Image | Band | ImageCollection, obj_name: str | None, out: list[dict], i: int
1179
+ ) -> str:
1180
+ # Prefer the object's name
1181
+ if obj_name:
1182
+ # Avoid the generic label e.g. Image(1)
1183
+ does_not_have_generic_name = (
1184
+ re.sub("(\d+)", "", obj_name) != f"{obj.__class__.__name__}()"
1185
+ )
1186
+ if does_not_have_generic_name:
1187
+ return obj_name
1188
+ # try:
1189
+ # if obj.tile and obj.date:
1190
+ # name = f"{obj.tile}_{obj.date[:8]}"
1191
+ # except (ValueError, AttributeError):
1192
+ # name = None
1193
+
1194
+ try:
1195
+ # Images/Bands/Collections constructed from arrays have no path stems
1196
+ if obj.stem:
1197
+ name = obj.stem
1198
+ else:
1199
+ name = str(obj)[:23]
1200
+ except (AttributeError, ValueError):
1201
+ name = str(obj)[:23]
1202
+
1203
+ if name in [x["label"] for x in out if "label" in x]:
1204
+ name += f"_{i}"
1205
+
1206
+ return name
1207
+
1208
+
896
1209
  def _categorical_legend(
897
1210
  m: folium.Map, title: str, categories: list[str], colors: list[str]
898
1211
  ) -> None: