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.
- sgis/__init__.py +10 -6
- sgis/exceptions.py +2 -2
- sgis/geopandas_tools/bounds.py +17 -15
- sgis/geopandas_tools/buffer_dissolve_explode.py +24 -5
- sgis/geopandas_tools/conversion.py +15 -6
- sgis/geopandas_tools/duplicates.py +2 -2
- sgis/geopandas_tools/general.py +9 -5
- sgis/geopandas_tools/geometry_types.py +3 -3
- sgis/geopandas_tools/neighbors.py +3 -3
- sgis/geopandas_tools/point_operations.py +2 -2
- sgis/geopandas_tools/polygon_operations.py +5 -5
- sgis/geopandas_tools/sfilter.py +3 -3
- sgis/helpers.py +3 -3
- sgis/io/read_parquet.py +1 -1
- sgis/maps/examine.py +16 -2
- sgis/maps/explore.py +370 -57
- sgis/maps/legend.py +164 -72
- sgis/maps/map.py +184 -90
- sgis/maps/maps.py +92 -90
- sgis/maps/thematicmap.py +236 -83
- sgis/networkanalysis/closing_network_holes.py +2 -2
- sgis/networkanalysis/cutting_lines.py +3 -3
- sgis/networkanalysis/directednetwork.py +1 -1
- sgis/networkanalysis/finding_isolated_networks.py +2 -2
- sgis/networkanalysis/networkanalysis.py +7 -7
- sgis/networkanalysis/networkanalysisrules.py +1 -1
- sgis/networkanalysis/traveling_salesman.py +1 -1
- sgis/parallel/parallel.py +39 -19
- sgis/raster/__init__.py +0 -6
- sgis/raster/cube.py +51 -5
- sgis/raster/image_collection.py +2560 -0
- sgis/raster/indices.py +14 -5
- sgis/raster/raster.py +131 -236
- sgis/raster/sentinel_config.py +104 -0
- sgis/raster/zonal.py +0 -1
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +1 -1
- ssb_sgis-1.0.3.dist-info/RECORD +61 -0
- sgis/raster/methods_as_functions.py +0 -0
- sgis/raster/torchgeo.py +0 -171
- ssb_sgis-1.0.2.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.2.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +0 -0
- {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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
361
|
-
sample =
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
422
|
-
with open(
|
|
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.
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
582
|
-
|
|
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
|
-
|
|
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:
|