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.
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/PKG-INFO +1 -1
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/pyproject.toml +1 -1
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/bounds.py +2 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/explore.py +198 -172
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/maps.py +16 -43
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/image_collection.py +19 -21
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/LICENSE +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/README.md +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/__init__.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/debug_config.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/exceptions.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/__init__.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/buffer_dissolve_explode.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/centerlines.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/cleaning.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/conversion.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/duplicates.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/general.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/geocoding.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/geometry_types.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/neighbors.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/overlay.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/point_operations.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/polygon_operations.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/polygons_as_rings.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/geopandas_tools/sfilter.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/helpers.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/_is_dapla.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/dapla_functions.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/opener.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/io/read_parquet.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/__init__.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/examine.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/httpserver.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/legend.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/map.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/thematicmap.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/maps/tilesources.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/__init__.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_get_route.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_od_cost_matrix.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_points.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/_service_area.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/closing_network_holes.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/cutting_lines.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/directednetwork.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/finding_isolated_networks.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/network.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/networkanalysis.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/networkanalysisrules.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/nodes.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/networkanalysis/traveling_salesman.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/parallel/parallel.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/py.typed +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/__init__.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/base.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/indices.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/regex.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/sentinel_config.py +0 -0
- {ssb_sgis-1.0.10 → ssb_sgis-1.0.12}/src/sgis/raster/zonal.py +0 -0
|
@@ -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 = (
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
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"
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|