ssb-sgis 1.0.10__tar.gz → 1.0.12__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.
Files changed (60) hide show
  1. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/PKG-INFO +1 -1
  2. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/pyproject.toml +1 -1
  3. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/bounds.py +2 -0
  4. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/explore.py +198 -172
  5. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/maps.py +16 -43
  6. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/image_collection.py +19 -21
  7. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/LICENSE +0 -0
  8. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/README.md +0 -0
  9. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/__init__.py +0 -0
  10. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/debug_config.py +0 -0
  11. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/exceptions.py +0 -0
  12. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/__init__.py +0 -0
  13. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
  14. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/centerlines.py +0 -0
  15. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/cleaning.py +0 -0
  16. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/conversion.py +0 -0
  17. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/duplicates.py +0 -0
  18. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/general.py +0 -0
  19. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/geocoding.py +0 -0
  20. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/geometry_types.py +0 -0
  21. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/neighbors.py +0 -0
  22. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/overlay.py +0 -0
  23. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/point_operations.py +0 -0
  24. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/polygon_operations.py +0 -0
  25. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/polygons_as_rings.py +0 -0
  26. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/sfilter.py +0 -0
  27. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/helpers.py +0 -0
  28. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/_is_dapla.py +0 -0
  29. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/dapla_functions.py +0 -0
  30. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/opener.py +0 -0
  31. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/read_parquet.py +0 -0
  32. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/__init__.py +0 -0
  33. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/examine.py +0 -0
  34. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/httpserver.py +0 -0
  35. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/legend.py +0 -0
  36. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/map.py +0 -0
  37. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/thematicmap.py +0 -0
  38. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/tilesources.py +0 -0
  39. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/__init__.py +0 -0
  40. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_get_route.py +0 -0
  41. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_od_cost_matrix.py +0 -0
  42. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_points.py +0 -0
  43. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_service_area.py +0 -0
  44. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/closing_network_holes.py +0 -0
  45. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/cutting_lines.py +0 -0
  46. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/directednetwork.py +0 -0
  47. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/finding_isolated_networks.py +0 -0
  48. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/network.py +0 -0
  49. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/networkanalysis.py +0 -0
  50. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/networkanalysisrules.py +0 -0
  51. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/nodes.py +0 -0
  52. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/traveling_salesman.py +0 -0
  53. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/parallel/parallel.py +0 -0
  54. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/py.typed +0 -0
  55. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/__init__.py +0 -0
  56. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/base.py +0 -0
  57. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/indices.py +0 -0
  58. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/regex.py +0 -0
  59. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/sentinel_config.py +0 -0
  60. {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/zonal.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ssb-sgis
3
- Version: 1.0.10
3
+ Version: 1.0.12
4
4
  Summary: GIS functions used at Statistics Norway.
5
5
  Home-page: https://github.com/statisticsnorway/ssb-sgis
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "ssb-sgis"
3
- version = "1.0.10"
3
+ version = "1.0.12"
4
4
  description = "GIS functions used at Statistics Norway."
5
5
  authors = ["Morten Letnes <morten.letnes@ssb.no>"]
6
6
  license = "MIT"
@@ -697,4 +697,6 @@ def get_total_bounds(
697
697
  raise e2 from e
698
698
  else:
699
699
  continue
700
+ if not xs or not ys:
701
+ raise ValueError(f"No bounds found for {geometries}")
700
702
  return min(xs), min(ys), max(xs), max(ys)
@@ -18,6 +18,7 @@ from typing import ClassVar
18
18
  import branca as bc
19
19
  import folium
20
20
  import geopandas as gpd
21
+ import joblib
21
22
  import matplotlib
22
23
  import matplotlib.pyplot as plt
23
24
  import numpy as np
@@ -189,6 +190,10 @@ def _single_band_to_arr(band, mask, name, raster_data_dict):
189
190
  arr = band.clip(mask).values
190
191
  else:
191
192
  arr = band.load(indexes=1, bounds=mask).values
193
+
194
+ if _is_too_much_nodata([arr], band.nodata):
195
+ return False
196
+
192
197
  bounds: tuple = (
193
198
  _any_to_bbox_crs4326(mask, band.crs)
194
199
  if mask is not None
@@ -205,7 +210,32 @@ def _single_band_to_arr(band, mask, name, raster_data_dict):
205
210
  raster_data_dict["cmap"] = band.cmap or "Grays"
206
211
  raster_data_dict["arr"] = arr
207
212
  raster_data_dict["bounds"] = bounds
208
- raster_data_dict["label"] = name
213
+ raster_data_dict["label"] = band.name or name
214
+ try:
215
+ raster_data_dict["date"] = band.date
216
+ except Exception:
217
+ raster_data_dict["date"] = None
218
+
219
+ return True
220
+
221
+
222
+ def _is_too_much_nodata(
223
+ arrays: list[np.ndarray],
224
+ nodata: int | None = None,
225
+ max_nodata_percentage: int = 100,
226
+ ) -> bool:
227
+ return (
228
+ any(arr.shape[0] == 0 for arr in arrays)
229
+ or any(
230
+ (
231
+ isinstance(arr, np.ma.core.MaskedArray)
232
+ and np.mean((arr.mask) | (arr.data == nodata) | (np.isnan(arr.data)))
233
+ > (max_nodata_percentage / 100)
234
+ )
235
+ for arr in arrays
236
+ )
237
+ or any(np.mean(arr == nodata) > (max_nodata_percentage / 100) for arr in arrays)
238
+ )
209
239
 
210
240
 
211
241
  def _any_to_bbox_crs4326(obj, crs):
@@ -240,6 +270,7 @@ class Explore(Map):
240
270
  text: str | None = None,
241
271
  decimals: int = 6,
242
272
  max_images: int = 10,
273
+ max_nodata_percentage: int = 100,
243
274
  **kwargs,
244
275
  ) -> None:
245
276
  """Initialiser.
@@ -266,6 +297,8 @@ class Explore(Map):
266
297
  map. Defaults to 15.
267
298
  text: Optional text for a text box in the map.
268
299
  decimals: Number of decimals in the coordinates.
300
+ max_nodata_percentage: Maximum percentage nodata values (e.g. clouds) ro allow in
301
+ image arrays.
269
302
  **kwargs: Additional keyword arguments. Can also be geometry-like objects
270
303
  where the key is the label.
271
304
  """
@@ -280,6 +313,7 @@ class Explore(Map):
280
313
  self.text = text
281
314
  self.decimals = decimals
282
315
  self.max_images = max_images
316
+ self.max_nodata_percentage = max_nodata_percentage
283
317
  self.legend = None
284
318
 
285
319
  self.browser = browser
@@ -471,7 +505,10 @@ class Explore(Map):
471
505
 
472
506
  random_point = sample.sample_points(size=1)
473
507
 
474
- self.center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
508
+ self.center = (
509
+ float(random_point.geometry.iloc[0].x),
510
+ float(random_point.geometry.iloc[0].y),
511
+ )
475
512
  print(f"center={self.center}, size={size}")
476
513
 
477
514
  mask = random_point.buffer(size)
@@ -509,18 +546,26 @@ class Explore(Map):
509
546
 
510
547
  def _load_rasters_as_images(self):
511
548
  self.raster_data = []
512
- n_added_images = 0
513
549
  self._show_rasters = True
514
- for name, value in self.rasters.items():
515
- data, n_added_images = self._image_collection_to_background_map(
516
- value,
517
- self.mask,
518
- name,
519
- max_images=self.max_images,
520
- n_added_images=n_added_images,
550
+
551
+ with joblib.Parallel(len(self.rasters) or 1, backend="threading") as parallel:
552
+ results = parallel(
553
+ joblib.delayed(_image_collection_to_background_map)(
554
+ raster,
555
+ name,
556
+ self.mask,
557
+ self.max_images,
558
+ self.max_nodata_percentage,
559
+ )
560
+ for name, raster in self.rasters.items()
521
561
  )
562
+
563
+ for data in results:
522
564
  self.raster_data += data
523
565
 
566
+ if len(self.raster_data) > 6:
567
+ self._show_rasters = False
568
+
524
569
  def _rasters_to_background_maps(self):
525
570
  for raster_data_dict in self.raster_data:
526
571
  try:
@@ -1061,160 +1106,6 @@ class Explore(Map):
1061
1106
  **kwargs,
1062
1107
  )
1063
1108
 
1064
- def _image_collection_to_background_map(
1065
- self,
1066
- image_collection: ImageCollection | Image | Band,
1067
- mask: Any | None,
1068
- name: str,
1069
- max_images: int,
1070
- n_added_images: int,
1071
- rbg_bands: list[str] = (("B04", "B02", "B03"), ("B4", "B2", "B3")),
1072
- ) -> tuple[list[dict], int]:
1073
- out = []
1074
-
1075
- if all(isinstance(x, str) for x in rbg_bands):
1076
- rbg_bands = (rbg_bands,)
1077
-
1078
- if isinstance(image_collection, ImageCollection):
1079
- images = image_collection.images
1080
- name = None
1081
- elif isinstance(image_collection, Image):
1082
- img = image_collection
1083
- if not _intersects_if_not_none_or_empty(
1084
- mask, img.bounds
1085
- ): # is not None and not to_shapely(mask).intersects(
1086
- # to_shapely(img.bounds)
1087
- # ):
1088
- return out, n_added_images
1089
-
1090
- if len(img) == 1:
1091
- band = next(iter(img))
1092
- raster_data_dict = {}
1093
- out.append(raster_data_dict)
1094
- name = _determine_label(band, name, out, n_added_images)
1095
- _single_band_to_arr(band, mask, name, raster_data_dict)
1096
- n_added_images += 1
1097
- return out, n_added_images
1098
- elif len(img) < 3:
1099
- raster_data_dict = {}
1100
- out.append(raster_data_dict)
1101
- for band in img:
1102
- name = _determine_label(band, None, out, n_added_images)
1103
- _single_band_to_arr(band, mask, name, raster_data_dict)
1104
- n_added_images += 1
1105
- return out, n_added_images
1106
- else:
1107
- images = [image_collection]
1108
-
1109
- elif isinstance(image_collection, Band):
1110
- band = image_collection
1111
-
1112
- if not _intersects_if_not_none_or_empty(
1113
- mask, band.bounds
1114
- ): # mask is not None and not to_shapely(mask).intersects(
1115
- # to_shapely(band.bounds)
1116
- # ):
1117
- return out, n_added_images
1118
-
1119
- raster_data_dict = {}
1120
- out.append(raster_data_dict)
1121
- _single_band_to_arr(band, mask, name, raster_data_dict)
1122
- return out, n_added_images
1123
-
1124
- else:
1125
- raise TypeError(type(image_collection))
1126
-
1127
- if max(len(out), len(images)) + n_added_images > max_images:
1128
- warnings.warn(
1129
- f"Showing only a sample of {max_images}. Set 'max_images.", stacklevel=1
1130
- )
1131
- self._show_rasters = False
1132
- random.shuffle(images)
1133
-
1134
- images = images[: (max_images - n_added_images)]
1135
- images = (
1136
- list(sorted([img for img in images if img.date is not None]))
1137
- + sorted(
1138
- [
1139
- img
1140
- for img in images
1141
- if img.date is None and img.path is not None
1142
- ],
1143
- key=lambda x: x.path,
1144
- )
1145
- + [img for img in images if img.date is None and img.path is None]
1146
- )
1147
-
1148
- for image in images:
1149
-
1150
- if not _intersects_if_not_none_or_empty(
1151
- mask, image.bounds
1152
- ): # mask is not None and not to_shapely(mask).intersects(
1153
- # to_shapely(image.bounds)
1154
- # ):
1155
- continue
1156
-
1157
- raster_data_dict = {}
1158
- out.append(raster_data_dict)
1159
-
1160
- if len(image) < 3:
1161
- for band in image:
1162
- name = _determine_label(band, None, out, n_added_images)
1163
- _single_band_to_arr(band, mask, name, raster_data_dict)
1164
- n_added_images += 1
1165
- continue
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
-
1175
- for red, blue, green in rbg_bands:
1176
- try:
1177
- red_band = load(red)
1178
- except KeyError:
1179
- continue
1180
- try:
1181
- blue_band = load(blue)
1182
- except KeyError:
1183
- continue
1184
- try:
1185
- green_band = load(green)
1186
- except KeyError:
1187
- continue
1188
- break
1189
-
1190
- crs = red_band.crs
1191
- bounds = to_bbox(to_gdf(red_band.bounds, crs).to_crs(4326))
1192
-
1193
- red_band = red_band.values
1194
- blue_band = blue_band.values
1195
- green_band = green_band.values
1196
-
1197
- if (
1198
- red_band.shape[0] == 0
1199
- or blue_band.shape[0] == 0
1200
- or green_band.shape[0] == 0
1201
- ):
1202
- continue
1203
-
1204
- # to 3d array in shape (x, y, 3)
1205
- rbg_image = np.stack([red_band, blue_band, green_band], axis=2)
1206
-
1207
- raster_data_dict["arr"] = rbg_image
1208
- raster_data_dict["bounds"] = bounds
1209
- raster_data_dict["cmap"] = None
1210
- raster_data_dict["label"] = _determine_label(
1211
- image, name, out, n_added_images
1212
- )
1213
-
1214
- n_added_images += 1
1215
-
1216
- return out, n_added_images
1217
-
1218
1109
 
1219
1110
  def _tooltip_popup(
1220
1111
  type_: str, fields: Any, gdf: GeoDataFrame, **kwargs
@@ -1252,29 +1143,24 @@ def _intersects_if_not_none_or_empty(obj: Any, other: Any) -> bool:
1252
1143
  return obj.intersects(to_shapely(other))
1253
1144
 
1254
1145
 
1255
- def _determine_label(
1256
- obj: Image | Band | ImageCollection, obj_name: str | None, out: list[dict], i: int
1257
- ) -> str:
1146
+ def _determine_label(obj: Image | Band | ImageCollection, obj_name: str | None) -> str:
1258
1147
  # Prefer the object's name
1259
1148
  if obj_name:
1260
1149
  # Avoid the generic label e.g. Image(1)
1261
1150
  does_not_have_generic_name = (
1262
- re.sub("(\d+)", "", obj_name) != f"{obj.__class__.__name__}()"
1151
+ re.sub(r"(\d+)", "", obj_name) != f"{obj.__class__.__name__}()"
1263
1152
  )
1264
1153
  if does_not_have_generic_name:
1265
1154
  return obj_name
1266
1155
  try:
1267
- # Images/Bands/Collections constructed from arrays have no path stems
1268
1156
  if obj.name:
1269
1157
  name = obj.name
1270
1158
  else:
1159
+ # Images/Bands/Collections constructed from arrays have no path stems
1271
1160
  name = str(obj)[:23]
1272
1161
  except (AttributeError, ValueError):
1273
1162
  name = str(obj)[:23]
1274
1163
 
1275
- if name in [x["label"] for x in out if "label" in x]:
1276
- name += f"_{i}"
1277
-
1278
1164
  return name
1279
1165
 
1280
1166
 
@@ -1448,3 +1334,143 @@ def get_textbox(text: str) -> str:
1448
1334
  </style>
1449
1335
  {{% endmacro %}}
1450
1336
  """
1337
+
1338
+
1339
+ def _add_one_image(
1340
+ image: Image, mask, rbg_bands, name: str, max_nodata_percentage: int
1341
+ ) -> dict:
1342
+
1343
+ raster_data_dict = {}
1344
+
1345
+ if len(image) < 3:
1346
+ for band in image:
1347
+ name = _determine_label(band, band.name or name)
1348
+ _single_band_to_arr(band, mask, name, raster_data_dict)
1349
+ return raster_data_dict
1350
+
1351
+ def load(band_id: str) -> Band:
1352
+ band = image[band_id]
1353
+ if band.has_array and mask is not None:
1354
+ band = band.clip(mask, copy=True)
1355
+ elif not band.has_array:
1356
+ band = band.load(indexes=1, bounds=mask)
1357
+ return band
1358
+
1359
+ for red, blue, green in rbg_bands:
1360
+ try:
1361
+ red_band = load(red)
1362
+ except KeyError:
1363
+ continue
1364
+ try:
1365
+ blue_band = load(blue)
1366
+ except KeyError:
1367
+ continue
1368
+ try:
1369
+ green_band = load(green)
1370
+ except KeyError:
1371
+ continue
1372
+ break
1373
+
1374
+ crs = red_band.crs
1375
+ bounds = to_bbox(to_gdf(red_band.bounds, crs).to_crs(4326))
1376
+
1377
+ red_band = red_band.values
1378
+ blue_band = blue_band.values
1379
+ green_band = green_band.values
1380
+
1381
+ if _is_too_much_nodata(
1382
+ [red_band, blue_band, green_band], image.nodata, max_nodata_percentage
1383
+ ):
1384
+ return
1385
+
1386
+ # to 3d array in shape (x, y, 3)
1387
+ rbg_image = np.stack([red_band, blue_band, green_band], axis=2)
1388
+
1389
+ raster_data_dict["arr"] = rbg_image
1390
+ raster_data_dict["bounds"] = bounds
1391
+ raster_data_dict["cmap"] = None
1392
+ raster_data_dict["label"] = _determine_label(image, image.name or name)
1393
+ try:
1394
+ raster_data_dict["date"] = image.date
1395
+ except Exception:
1396
+ raster_data_dict["date"] = None
1397
+
1398
+ return raster_data_dict
1399
+
1400
+
1401
+ def _image_collection_to_background_map(
1402
+ image_collection: ImageCollection | Image | Band,
1403
+ name: str,
1404
+ mask: Any | None,
1405
+ max_images: int,
1406
+ max_nodata_percentage: int,
1407
+ rbg_bands: list[str] = (("B04", "B02", "B03"), ("B4", "B2", "B3")),
1408
+ ) -> tuple[list[dict], int]:
1409
+ out = []
1410
+ n_added_images = 0
1411
+
1412
+ if all(isinstance(x, str) for x in rbg_bands):
1413
+ rbg_bands = (rbg_bands,)
1414
+
1415
+ if isinstance(image_collection, ImageCollection):
1416
+ if mask is not None:
1417
+ image_collection = image_collection.filter(bbox=mask)
1418
+ images: list[Image] = image_collection.images
1419
+ name = None
1420
+ elif isinstance(image_collection, Image):
1421
+ images: list[Image] = [image_collection]
1422
+ name = image_collection.name
1423
+
1424
+ elif isinstance(image_collection, Band):
1425
+ band = image_collection
1426
+
1427
+ if not _intersects_if_not_none_or_empty(mask, band.bounds):
1428
+ return out
1429
+
1430
+ raster_data_dict = {}
1431
+ out.append(raster_data_dict)
1432
+ _single_band_to_arr(band, mask, name, raster_data_dict)
1433
+ return out
1434
+
1435
+ else:
1436
+ raise TypeError(type(image_collection))
1437
+
1438
+ if max(len(out), len(images)) + n_added_images > max_images:
1439
+ warnings.warn(
1440
+ f"Showing only a sample of {max_images}. Set 'max_images.", stacklevel=1
1441
+ )
1442
+ random.shuffle(images)
1443
+
1444
+ while n_added_images < max_images:
1445
+ n_max = min(max_images - n_added_images, len(images))
1446
+ if not n_max:
1447
+ break
1448
+ n_images_was = len(images)
1449
+ these_images = images[:n_max]
1450
+ images = images[n_max:]
1451
+ assert n_images_was == sum([len(these_images), len(images)])
1452
+ with joblib.Parallel(n_max, backend="threading") as parallel:
1453
+ results = parallel(
1454
+ joblib.delayed(_add_one_image)(
1455
+ img, mask, rbg_bands, name, max_nodata_percentage
1456
+ )
1457
+ for img in these_images
1458
+ )
1459
+
1460
+ for x in results:
1461
+ if not x:
1462
+ continue
1463
+ i = 1
1464
+ while x["label"] in {y["label"] for y in out}:
1465
+ x["label"] = x["label"].rstrip(f"_{i}", "") + f"_{i + 1}"
1466
+ i += 1
1467
+
1468
+ n_added_images += 1
1469
+ out.append(x)
1470
+
1471
+ if all(x["date"] for x in out):
1472
+ out = sorted(out, key=lambda x: x["date"])
1473
+ else:
1474
+ out = sorted(out, key=lambda x: x["label"])
1475
+
1476
+ return out
@@ -86,7 +86,7 @@ def explore(
86
86
  smooth_factor: int | float = 1.5,
87
87
  size: int | None = None,
88
88
  max_images: int = 10,
89
- images_to_gdf: bool = False,
89
+ max_nodata_percentage: int = 100,
90
90
  **kwargs,
91
91
  ) -> Explore:
92
92
  """Interactive map of GeoDataFrames with layers that can be toggled on/off.
@@ -116,8 +116,8 @@ def explore(
116
116
  1000.
117
117
  max_images: Maximum number of images (Image, ImageCollection, Band) to show per
118
118
  map. Defaults to 10.
119
- images_to_gdf: If True (not default), images (Image, ImageCollection, Band)
120
- will be converted to GeoDataFrame and added to the map.
119
+ max_nodata_percentage: Maximum percentage nodata values (e.g. clouds) ro allow in
120
+ image arrays.
121
121
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
122
122
  instance 'cmap' to change the colors, 'scheme' to change how the data
123
123
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
@@ -165,6 +165,8 @@ def explore(
165
165
  mask=mask,
166
166
  browser=browser,
167
167
  max_zoom=max_zoom,
168
+ max_images=max_images,
169
+ max_nodata_percentage=max_nodata_percentage,
168
170
  **kwargs,
169
171
  )
170
172
 
@@ -211,46 +213,6 @@ def explore(
211
213
  else:
212
214
  mask = mask4326.to_crs(to_crs)
213
215
 
214
- # else:
215
- # mask_flipped = mask
216
-
217
- # # coords = mask.get_coordinates()
218
- # if (
219
- # (mask_flipped.distance(bounds) > size).all()
220
- # # and coords["x"].max() < 180
221
- # # and coords["y"].max() < 180
222
- # # and coords["x"].min() > -180
223
- # # and coords["y"].min() > -180
224
- # ):
225
- # try:
226
- # bounds4326 = to_gdf(bounds, to_crs).to_crs(4326).geometry.iloc[0]
227
- # except ValueError:
228
- # bounds4326 = to_gdf(bounds, to_crs).set_crs(4326).geometry.iloc[0]
229
-
230
- # mask4326 = mask.set_crs(4326, allow_override=True)
231
-
232
- # if (mask4326.distance(bounds4326) > size).all():
233
- # # try flipping coordinates
234
- # x, y = list(mask4326.geometry.iloc[0].coords)[0]
235
- # mask4326 = to_gdf([y, x], 4326)
236
-
237
- # mask = mask4326
238
-
239
- # # if mask4326.intersects(bounds4326).any():
240
- # # mask = mask4326
241
- # # else:
242
- # # try:
243
- # # mask = mask.to_crs(to_crs)
244
- # # except ValueError:
245
- # # pass
246
- # else:
247
- # mask = mask_flipped
248
-
249
- # try:
250
- # mask = mask.to_crs(to_crs)
251
- # except ValueError:
252
- # pass
253
-
254
216
  if get_geom_type(mask) in ["point", "line"]:
255
217
  mask = mask.buffer(size)
256
218
 
@@ -260,6 +222,8 @@ def explore(
260
222
  mask=mask,
261
223
  browser=browser,
262
224
  max_zoom=max_zoom,
225
+ max_images=max_images,
226
+ max_nodata_percentage=max_nodata_percentage,
263
227
  **kwargs,
264
228
  )
265
229
 
@@ -270,6 +234,7 @@ def explore(
270
234
  max_zoom=max_zoom,
271
235
  smooth_factor=smooth_factor,
272
236
  max_images=max_images,
237
+ max_nodata_percentage=max_nodata_percentage,
273
238
  **kwargs,
274
239
  )
275
240
 
@@ -294,6 +259,7 @@ def samplemap(
294
259
  explore: bool = True,
295
260
  browser: bool = False,
296
261
  max_images: int = 10,
262
+ max_nodata_percentage: int = 100,
297
263
  **kwargs,
298
264
  ) -> Explore:
299
265
  """Shows an interactive map of a random area of GeoDataFrames.
@@ -327,6 +293,8 @@ def samplemap(
327
293
  If True the maps will be opened in a browser folder.
328
294
  max_images: Maximum number of images (Image, ImageCollection, Band) to show per
329
295
  map. Defaults to 10.
296
+ max_nodata_percentage: Maximum percentage nodata values (e.g. clouds) ro allow in
297
+ image arrays.
330
298
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
331
299
  instance 'cmap' to change the colors, 'scheme' to change how the data
332
300
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
@@ -409,6 +377,7 @@ def samplemap(
409
377
  explore=explore,
410
378
  smooth_factor=smooth_factor,
411
379
  max_images=max_images,
380
+ max_nodata_percentage=max_nodata_percentage,
412
381
  **kwargs,
413
382
  )
414
383
 
@@ -422,6 +391,7 @@ def clipmap(
422
391
  smooth_factor: int | float = 1.5,
423
392
  browser: bool = False,
424
393
  max_images: int = 10,
394
+ max_nodata_percentage: int = 100,
425
395
  **kwargs,
426
396
  ) -> Explore | Map:
427
397
  """Shows an interactive map of a of GeoDataFrames clipped to the mask extent.
@@ -450,6 +420,8 @@ def clipmap(
450
420
  If True the maps will be opened in a browser folder.
451
421
  max_images: Maximum number of images (Image, ImageCollection, Band) to show per
452
422
  map. Defaults to 10.
423
+ max_nodata_percentage: Maximum percentage nodata values (e.g. clouds) ro allow in
424
+ image arrays.
453
425
  **kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
454
426
  instance 'cmap' to change the colors, 'scheme' to change how the data
455
427
  is grouped. This defaults to 'fisherjenkssampled' for numeric data.
@@ -484,6 +456,7 @@ def clipmap(
484
456
  max_zoom=max_zoom,
485
457
  smooth_factor=smooth_factor,
486
458
  max_images=max_images,
459
+ max_nodata_percentage=max_nodata_percentage,
487
460
  **kwargs,
488
461
  )
489
462
  m.mask = mask
@@ -179,7 +179,7 @@ def _get_child_paths_threaded(data: Sequence[str]) -> set[str]:
179
179
 
180
180
  @dataclass
181
181
  class PixelwiseResults:
182
- """Container of results from pixelwise operation to be converted."""
182
+ """Container of pixelwise results to be converted to numpy/geopandas."""
183
183
 
184
184
  row_indices: np.ndarray
185
185
  col_indices: np.ndarray
@@ -657,6 +657,12 @@ class _ImageBandBase(_ImageBase):
657
657
  return self._month
658
658
  return str(self.date).replace("-", "").replace("/", "")[4:6]
659
659
 
660
+ @property
661
+ def day(self) -> str:
662
+ if hasattr(self, "_day") and self._day:
663
+ return self._day
664
+ return str(self.date).replace("-", "").replace("/", "")[6:8]
665
+
660
666
  @property
661
667
  def name(self) -> str | None:
662
668
  if hasattr(self, "_name") and self._name is not None:
@@ -1572,7 +1578,7 @@ class Image(_ImageBandBase):
1572
1578
 
1573
1579
  if df is None:
1574
1580
  if not self._all_file_paths:
1575
- self._all_file_paths = [self.path]
1581
+ self._all_file_paths = {self.path}
1576
1582
  df = self._create_metadata_df(self._all_file_paths)
1577
1583
 
1578
1584
  df["image_path"] = df["image_path"].astype(str)
@@ -1597,7 +1603,7 @@ class Image(_ImageBandBase):
1597
1603
  if self.metadata:
1598
1604
  try:
1599
1605
  metadata = self.metadata[self.path]
1600
- except KeyError:
1606
+ except KeyError as e:
1601
1607
  metadata = {}
1602
1608
  for key, value in metadata.items():
1603
1609
  if key in dir(self):
@@ -2062,9 +2068,9 @@ class ImageCollection(_ImageBase):
2062
2068
  ) from e
2063
2069
  raise e
2064
2070
  if self.level:
2065
- self._all_file_paths = [
2071
+ self._all_file_paths = {
2066
2072
  path for path in self._all_file_paths if self.level in path
2067
- ]
2073
+ }
2068
2074
  self._df = self._create_metadata_df(self._all_file_paths)
2069
2075
  return
2070
2076
 
@@ -2076,9 +2082,9 @@ class ImageCollection(_ImageBase):
2076
2082
  self._all_file_paths = _get_all_file_paths(self.path)
2077
2083
 
2078
2084
  if self.level:
2079
- self._all_file_paths = [
2085
+ self._all_file_paths = {
2080
2086
  path for path in self._all_file_paths if self.level in path
2081
- ]
2087
+ }
2082
2088
 
2083
2089
  self._df = self._create_metadata_df(self._all_file_paths)
2084
2090
 
@@ -2645,15 +2651,9 @@ class ImageCollection(_ImageBase):
2645
2651
 
2646
2652
  other = to_shapely(other)
2647
2653
 
2648
- if self.processes == 1:
2649
- intersects_list: pd.Series = GeoSeries(
2650
- [img.union_all() for img in self]
2651
- ).intersects(other)
2652
- else:
2653
- with joblib.Parallel(n_jobs=self.processes, backend="loky") as parallel:
2654
- intersects_list: list[bool] = parallel(
2655
- joblib.delayed(_intesects)(image, other) for image in self
2656
- )
2654
+ intersects_list: pd.Series = GeoSeries(
2655
+ [img.union_all() for img in self]
2656
+ ).intersects(other)
2657
2657
 
2658
2658
  self.images = [
2659
2659
  image
@@ -3097,7 +3097,9 @@ class Sentinel2Config:
3097
3097
  _extract_regex_match_from_string(
3098
3098
  xml_file,
3099
3099
  (
3100
- r'<BOA_QUANTIFICATION_VALUE unit="none">-?(\d+)</BOA_QUANTIFICATION_VALUE>',
3100
+ r'<BOA_QUANTIFICATION_VALUE unit="none">(\d+)</BOA_QUANTIFICATION_VALUE>',
3101
+ # r'<BOA_QUANTIFICATION_VALUE unit="none">-?(\d+)</BOA_QUANTIFICATION_VALUE>',
3102
+ r'<QUANTIFICATION_VALUE unit="none">?(\d+)</QUANTIFICATION_VALUE>',
3101
3103
  ),
3102
3104
  )
3103
3105
  )
@@ -3517,10 +3519,6 @@ def _band_apply(band: Band, func: Callable, **kwargs) -> Band:
3517
3519
  return band.apply(func, **kwargs)
3518
3520
 
3519
3521
 
3520
- def _clip_band(band: Band, mask, **kwargs) -> Band:
3521
- return band.clip(mask, **kwargs)
3522
-
3523
-
3524
3522
  def _merge_by_band(collection: ImageCollection, **kwargs) -> Image:
3525
3523
  return collection.merge_by_band(**kwargs)
3526
3524
 
File without changes
File without changes
File without changes
File without changes