ssb-sgis 1.0.1__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 +107 -121
- sgis/exceptions.py +5 -3
- sgis/geopandas_tools/__init__.py +1 -0
- sgis/geopandas_tools/bounds.py +86 -47
- sgis/geopandas_tools/buffer_dissolve_explode.py +62 -39
- sgis/geopandas_tools/centerlines.py +53 -44
- sgis/geopandas_tools/cleaning.py +87 -104
- sgis/geopandas_tools/conversion.py +164 -107
- sgis/geopandas_tools/duplicates.py +33 -19
- sgis/geopandas_tools/general.py +84 -52
- sgis/geopandas_tools/geometry_types.py +24 -10
- sgis/geopandas_tools/neighbors.py +23 -11
- sgis/geopandas_tools/overlay.py +136 -53
- sgis/geopandas_tools/point_operations.py +11 -10
- sgis/geopandas_tools/polygon_operations.py +53 -61
- sgis/geopandas_tools/polygons_as_rings.py +121 -78
- sgis/geopandas_tools/sfilter.py +17 -17
- sgis/helpers.py +116 -58
- sgis/io/dapla_functions.py +32 -23
- sgis/io/opener.py +13 -6
- sgis/io/read_parquet.py +2 -2
- sgis/maps/examine.py +55 -28
- sgis/maps/explore.py +471 -112
- sgis/maps/httpserver.py +12 -12
- sgis/maps/legend.py +285 -134
- sgis/maps/map.py +248 -129
- sgis/maps/maps.py +123 -119
- sgis/maps/thematicmap.py +260 -94
- sgis/maps/tilesources.py +3 -8
- sgis/networkanalysis/_get_route.py +5 -4
- sgis/networkanalysis/_od_cost_matrix.py +44 -1
- sgis/networkanalysis/_points.py +10 -4
- sgis/networkanalysis/_service_area.py +5 -2
- sgis/networkanalysis/closing_network_holes.py +22 -64
- sgis/networkanalysis/cutting_lines.py +58 -46
- sgis/networkanalysis/directednetwork.py +16 -8
- sgis/networkanalysis/finding_isolated_networks.py +6 -5
- sgis/networkanalysis/network.py +15 -13
- sgis/networkanalysis/networkanalysis.py +79 -61
- sgis/networkanalysis/networkanalysisrules.py +21 -17
- sgis/networkanalysis/nodes.py +2 -3
- sgis/networkanalysis/traveling_salesman.py +6 -3
- sgis/parallel/parallel.py +372 -142
- sgis/raster/base.py +9 -3
- sgis/raster/cube.py +331 -213
- sgis/raster/cubebase.py +15 -29
- sgis/raster/image_collection.py +2560 -0
- sgis/raster/indices.py +17 -12
- sgis/raster/raster.py +356 -275
- sgis/raster/sentinel_config.py +104 -0
- sgis/raster/zonal.py +38 -14
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/LICENSE +1 -1
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/METADATA +87 -16
- ssb_sgis-1.0.3.dist-info/RECORD +61 -0
- {ssb_sgis-1.0.1.dist-info → ssb_sgis-1.0.3.dist-info}/WHEEL +1 -1
- sgis/raster/bands.py +0 -48
- sgis/raster/gradient.py +0 -78
- sgis/raster/methods_as_functions.py +0 -124
- sgis/raster/torchgeo.py +0 -150
- ssb_sgis-1.0.1.dist-info/RECORD +0 -63
sgis/maps/explore.py
CHANGED
|
@@ -5,39 +5,71 @@ 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
|
|
15
|
+
from typing import Any
|
|
16
|
+
from typing import ClassVar
|
|
12
17
|
|
|
13
18
|
import branca as bc
|
|
14
19
|
import folium
|
|
20
|
+
import geopandas as gpd
|
|
15
21
|
import matplotlib
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
16
23
|
import numpy as np
|
|
17
24
|
import pandas as pd
|
|
18
25
|
import xyzservices
|
|
19
26
|
from folium import plugins
|
|
20
|
-
from geopandas import GeoDataFrame
|
|
27
|
+
from geopandas import GeoDataFrame
|
|
28
|
+
from geopandas import GeoSeries
|
|
21
29
|
from IPython.display import display
|
|
22
30
|
from jinja2 import Template
|
|
23
31
|
from pandas.api.types import is_datetime64_any_dtype
|
|
24
32
|
from shapely import Geometry
|
|
33
|
+
from shapely import box
|
|
25
34
|
from shapely.geometry import LineString
|
|
26
35
|
|
|
27
|
-
from ..geopandas_tools.
|
|
28
|
-
from ..geopandas_tools.
|
|
29
|
-
from ..geopandas_tools.
|
|
36
|
+
from ..geopandas_tools.bounds import get_total_bounds
|
|
37
|
+
from ..geopandas_tools.conversion import to_bbox
|
|
38
|
+
from ..geopandas_tools.conversion import to_gdf
|
|
39
|
+
from ..geopandas_tools.conversion import to_shapely
|
|
40
|
+
from ..geopandas_tools.general import clean_geoms
|
|
41
|
+
from ..geopandas_tools.general import make_all_singlepart
|
|
42
|
+
from ..geopandas_tools.geometry_types import get_geom_type
|
|
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
|
+
|
|
30
61
|
from .httpserver import run_html_server
|
|
31
62
|
from .map import Map
|
|
32
|
-
from .
|
|
33
|
-
|
|
63
|
+
from .map import _determine_best_name
|
|
64
|
+
from .tilesources import kartverket
|
|
65
|
+
from .tilesources import xyz
|
|
34
66
|
|
|
35
67
|
try:
|
|
36
68
|
from torchgeo.datasets.geo import RasterDataset
|
|
37
69
|
except ImportError:
|
|
38
70
|
|
|
39
71
|
class RasterDataset:
|
|
40
|
-
"""Placeholder"""
|
|
72
|
+
"""Placeholder."""
|
|
41
73
|
|
|
42
74
|
|
|
43
75
|
# the geopandas._explore raises a deprication warning. Ignoring for now.
|
|
@@ -96,7 +128,10 @@ class MeasureControlFix(plugins.MeasureControl):
|
|
|
96
128
|
"""
|
|
97
129
|
)
|
|
98
130
|
|
|
99
|
-
def __init__(
|
|
131
|
+
def __init__(
|
|
132
|
+
self, active_color: str = "red", completed_color: str = "red", **kwargs
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Run super __init__ after the new _template class attribute is made."""
|
|
100
135
|
super().__init__(
|
|
101
136
|
active_color=active_color, completed_color=completed_color, **kwargs
|
|
102
137
|
)
|
|
@@ -145,9 +180,172 @@ def to_tile(tile: str | xyzservices.TileProvider, max_zoom: int) -> folium.TileL
|
|
|
145
180
|
return folium.TileLayer(provider, name=name, attr=attr, max_zoom=max_zoom)
|
|
146
181
|
|
|
147
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
|
+
|
|
148
344
|
class Explore(Map):
|
|
345
|
+
"""Class for displaying and saving html maps of multiple GeoDataFrames."""
|
|
346
|
+
|
|
149
347
|
# class attribute that can be overridden locally
|
|
150
|
-
tiles = (
|
|
348
|
+
tiles: ClassVar[tuple[str]] = (
|
|
151
349
|
"grunnkart",
|
|
152
350
|
"norge_i_bilder",
|
|
153
351
|
"dark",
|
|
@@ -161,27 +359,57 @@ class Explore(Map):
|
|
|
161
359
|
column: str | None = None,
|
|
162
360
|
popup: bool = True,
|
|
163
361
|
max_zoom: int = 40,
|
|
164
|
-
smooth_factor: float = 1.
|
|
362
|
+
smooth_factor: float = 1.1,
|
|
165
363
|
browser: bool = False,
|
|
166
364
|
prefer_canvas: bool = True,
|
|
167
365
|
measure_control: bool = True,
|
|
168
366
|
geocoder: bool = False,
|
|
169
|
-
|
|
367
|
+
out_path: str | None = None,
|
|
170
368
|
show: bool | Iterable[bool] | None = None,
|
|
171
369
|
text: str | None = None,
|
|
172
370
|
decimals: int = 6,
|
|
371
|
+
max_images: int = 15,
|
|
173
372
|
**kwargs,
|
|
174
|
-
):
|
|
373
|
+
) -> None:
|
|
374
|
+
"""Initialiser.
|
|
375
|
+
|
|
376
|
+
Args:
|
|
377
|
+
*gdfs: One or more GeoDataFrames.
|
|
378
|
+
mask: Optional mask to clip to.
|
|
379
|
+
column: Optional column to color the data by.
|
|
380
|
+
popup: Whether to make the data popups clickable.
|
|
381
|
+
max_zoom: Max levels of zoom.
|
|
382
|
+
smooth_factor: Float of 1 or higher, 1 meaning no smoothing
|
|
383
|
+
of lines.
|
|
384
|
+
browser: Whether to open the map in a browser tab.
|
|
385
|
+
prefer_canvas: Option.
|
|
386
|
+
measure_control: Whether to include measurement box.
|
|
387
|
+
geocoder: Whether to include search bar for addresses.
|
|
388
|
+
out_path: Optional file path to an html file. The map will then
|
|
389
|
+
be saved instead of displayed.
|
|
390
|
+
show: Whether to show or hide the data upon creating the map.
|
|
391
|
+
If False, the data can be toggled on later. 'show' can also be
|
|
392
|
+
a sequence of boolean values the same length as the number of
|
|
393
|
+
GeoDataFrames.
|
|
394
|
+
max_images: Maximum number of images (Image, ImageCollection, Band) to show per
|
|
395
|
+
map. Defaults to 15.
|
|
396
|
+
text: Optional text for a text box in the map.
|
|
397
|
+
decimals: Number of decimals in the coordinates.
|
|
398
|
+
**kwargs: Additional keyword arguments. Can also be geometry-like objects
|
|
399
|
+
where the key is the label.
|
|
400
|
+
"""
|
|
175
401
|
self.popup = popup
|
|
176
402
|
self.max_zoom = max_zoom
|
|
177
403
|
self.smooth_factor = smooth_factor
|
|
178
404
|
self.prefer_canvas = prefer_canvas
|
|
179
405
|
self.measure_control = measure_control
|
|
180
406
|
self.geocoder = geocoder
|
|
181
|
-
self.
|
|
407
|
+
self.out_path = out_path
|
|
182
408
|
self.mask = mask
|
|
183
409
|
self.text = text
|
|
184
410
|
self.decimals = decimals
|
|
411
|
+
self.max_images = max_images
|
|
412
|
+
self.legend = None
|
|
185
413
|
|
|
186
414
|
self.browser = browser
|
|
187
415
|
if not self.browser and "show_in_browser" in kwargs:
|
|
@@ -195,14 +423,30 @@ class Explore(Map):
|
|
|
195
423
|
else:
|
|
196
424
|
show_was_none = False
|
|
197
425
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
)
|
|
203
|
-
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)
|
|
204
430
|
|
|
205
|
-
|
|
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)
|
|
206
450
|
|
|
207
451
|
if self.gdfs is None:
|
|
208
452
|
return
|
|
@@ -239,10 +483,13 @@ class Explore(Map):
|
|
|
239
483
|
gdf.index = gdf.index.astype(str)
|
|
240
484
|
except Exception:
|
|
241
485
|
pass
|
|
242
|
-
new_gdfs.append(gdf)
|
|
486
|
+
new_gdfs.append(to_gdf(gdf))
|
|
243
487
|
show_new.append(show)
|
|
244
488
|
self._gdfs = new_gdfs
|
|
245
|
-
self.
|
|
489
|
+
if self._gdfs:
|
|
490
|
+
self._gdf = pd.concat(new_gdfs, ignore_index=True)
|
|
491
|
+
else:
|
|
492
|
+
self._gdf = GeoDataFrame({"geometry": [], self._column: []})
|
|
246
493
|
self.show = show_new
|
|
247
494
|
|
|
248
495
|
if show_was_none and len(self._gdfs) > 6:
|
|
@@ -254,22 +501,34 @@ class Explore(Map):
|
|
|
254
501
|
else:
|
|
255
502
|
if not self._cmap:
|
|
256
503
|
self._cmap = "viridis"
|
|
257
|
-
self.cmap_start = kwargs.pop("cmap_start", 0)
|
|
258
|
-
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)
|
|
259
506
|
|
|
260
|
-
if self._gdf.crs is None:
|
|
261
|
-
|
|
507
|
+
# if self._gdf.crs is None:
|
|
508
|
+
# self.kwargs["crs"] = "Simple"
|
|
262
509
|
|
|
263
510
|
self.original_crs = self.gdf.crs
|
|
264
511
|
|
|
265
|
-
def __repr__(self):
|
|
512
|
+
def __repr__(self) -> str:
|
|
513
|
+
"""Representation."""
|
|
266
514
|
return f"{self.__class__.__name__}()"
|
|
267
515
|
|
|
268
516
|
def explore(
|
|
269
|
-
self,
|
|
517
|
+
self,
|
|
518
|
+
column: str | None = None,
|
|
519
|
+
center: Any | None = None,
|
|
520
|
+
size: int | None = None,
|
|
521
|
+
mask: Any | None = None,
|
|
522
|
+
**kwargs,
|
|
270
523
|
) -> None:
|
|
271
|
-
|
|
272
|
-
|
|
524
|
+
"""Explore all the data."""
|
|
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
|
+
):
|
|
531
|
+
warnings.warn("None of the GeoDataFrames have rows.", stacklevel=1)
|
|
273
532
|
return
|
|
274
533
|
if column:
|
|
275
534
|
self._column = column
|
|
@@ -306,51 +565,44 @@ class Explore(Map):
|
|
|
306
565
|
|
|
307
566
|
def samplemap(
|
|
308
567
|
self,
|
|
309
|
-
size: int
|
|
568
|
+
size: int,
|
|
569
|
+
sample: Any,
|
|
310
570
|
column: str | None = None,
|
|
311
|
-
sample_from_first: bool = True,
|
|
312
571
|
**kwargs,
|
|
313
572
|
) -> None:
|
|
573
|
+
"""Explore a sample of the data."""
|
|
314
574
|
if column:
|
|
315
575
|
self._column = column
|
|
316
576
|
self._update_column()
|
|
317
577
|
kwargs.pop("column", None)
|
|
318
578
|
|
|
319
|
-
|
|
320
|
-
sample =
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
# convert lines to polygons
|
|
325
|
-
if get_geom_type(sample) == "line":
|
|
326
|
-
sample["geometry"] = sample.buffer(1)
|
|
579
|
+
try:
|
|
580
|
+
sample = sample.sample(1)
|
|
581
|
+
except Exception:
|
|
582
|
+
pass
|
|
327
583
|
|
|
328
|
-
|
|
329
|
-
|
|
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)
|
|
330
588
|
|
|
331
|
-
|
|
332
|
-
else:
|
|
333
|
-
random_point = sample.centroid
|
|
589
|
+
random_point = sample.sample_points(size=1)
|
|
334
590
|
|
|
335
591
|
self.center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
|
|
336
592
|
print(f"center={self.center}, size={size}")
|
|
337
593
|
|
|
338
|
-
|
|
339
|
-
for gdf in self._gdfs:
|
|
340
|
-
gdf = gdf.clip(random_point.buffer(size))
|
|
341
|
-
gdfs = gdfs + (gdf,)
|
|
342
|
-
self._gdfs = gdfs
|
|
343
|
-
self._gdf = pd.concat(gdfs, ignore_index=True)
|
|
594
|
+
mask = random_point.buffer(size)
|
|
344
595
|
|
|
345
|
-
self.
|
|
346
|
-
self._explore(**kwargs)
|
|
596
|
+
return self.clipmap(mask, column, **kwargs)
|
|
347
597
|
|
|
348
598
|
def clipmap(
|
|
349
599
|
self,
|
|
350
|
-
mask,
|
|
600
|
+
mask: Any,
|
|
351
601
|
column: str | None = None,
|
|
352
602
|
**kwargs,
|
|
353
603
|
) -> None:
|
|
604
|
+
"""Explore the data within a mask extent."""
|
|
605
|
+
self.mask = mask
|
|
354
606
|
if column:
|
|
355
607
|
self._column = column
|
|
356
608
|
self._update_column()
|
|
@@ -358,17 +610,56 @@ class Explore(Map):
|
|
|
358
610
|
|
|
359
611
|
gdfs: tuple[GeoDataFrame] = ()
|
|
360
612
|
for gdf in self._gdfs:
|
|
361
|
-
gdf = gdf.clip(mask)
|
|
613
|
+
gdf = gdf.clip(self.mask)
|
|
362
614
|
collections = gdf.loc[gdf.geom_type == "GeometryCollection"]
|
|
363
615
|
if len(collections):
|
|
364
616
|
collections = make_all_singlepart(collections)
|
|
365
617
|
gdf = pd.concat([gdf, collections], ignore_index=False)
|
|
366
618
|
gdfs = gdfs + (gdf,)
|
|
367
619
|
self._gdfs = gdfs
|
|
368
|
-
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
|
+
|
|
369
625
|
self._explore(**kwargs)
|
|
370
626
|
|
|
371
|
-
def
|
|
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
|
+
|
|
662
|
+
def _explore(self, **kwargs) -> None:
|
|
372
663
|
self.kwargs = self.kwargs | kwargs
|
|
373
664
|
|
|
374
665
|
if self._is_categorical:
|
|
@@ -376,15 +667,17 @@ class Explore(Map):
|
|
|
376
667
|
else:
|
|
377
668
|
self._create_continous_map()
|
|
378
669
|
|
|
379
|
-
if self.
|
|
380
|
-
with open(
|
|
670
|
+
if self.out_path:
|
|
671
|
+
with open(
|
|
672
|
+
os.getcwd() + "/" + self.out_path.strip(".html") + ".html", "w"
|
|
673
|
+
) as f:
|
|
381
674
|
f.write(self.map._repr_html_())
|
|
382
675
|
elif self.browser:
|
|
383
676
|
run_html_server(self.map._repr_html_())
|
|
384
677
|
else:
|
|
385
678
|
display(self.map)
|
|
386
679
|
|
|
387
|
-
def _split_categories(self):
|
|
680
|
+
def _split_categories(self) -> None:
|
|
388
681
|
new_gdfs, new_labels, new_shows = [], [], []
|
|
389
682
|
for cat in self._unique_values:
|
|
390
683
|
gdf = self.gdf.loc[self.gdf[self.column] == cat]
|
|
@@ -396,7 +689,7 @@ class Explore(Map):
|
|
|
396
689
|
self.labels = new_labels
|
|
397
690
|
self.show = new_shows
|
|
398
691
|
|
|
399
|
-
def _to_single_geom_type(self, gdf) -> GeoDataFrame:
|
|
692
|
+
def _to_single_geom_type(self, gdf: GeoDataFrame) -> GeoDataFrame:
|
|
400
693
|
gdf = clean_geoms(gdf)
|
|
401
694
|
|
|
402
695
|
if get_geom_type(gdf) != "mixed":
|
|
@@ -420,21 +713,43 @@ class Explore(Map):
|
|
|
420
713
|
|
|
421
714
|
assert get_geom_type(gdf) != "mixed", gdf.geom_type.value_counts()
|
|
422
715
|
|
|
423
|
-
warnings.warn(mess)
|
|
716
|
+
warnings.warn(mess, stacklevel=1)
|
|
424
717
|
|
|
425
718
|
return gdf
|
|
426
719
|
|
|
427
|
-
def _update_column(self):
|
|
720
|
+
def _update_column(self) -> None:
|
|
428
721
|
self._is_categorical = self._check_if_categorical()
|
|
429
722
|
self._fillna_if_col_is_missing()
|
|
430
723
|
self._gdf = pd.concat(self._gdfs, ignore_index=True)
|
|
431
724
|
|
|
432
|
-
def
|
|
433
|
-
self
|
|
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
|
|
434
735
|
|
|
435
|
-
|
|
736
|
+
def _create_categorical_map(self) -> None:
|
|
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: []})
|
|
743
|
+
|
|
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
|
|
436
751
|
self.map = self._make_folium_map(
|
|
437
|
-
bounds=
|
|
752
|
+
bounds=bounds,
|
|
438
753
|
max_zoom=self.max_zoom,
|
|
439
754
|
popup=self.popup,
|
|
440
755
|
prefer_canvas=self.prefer_canvas,
|
|
@@ -445,8 +760,6 @@ class Explore(Map):
|
|
|
445
760
|
if not len(gdf):
|
|
446
761
|
continue
|
|
447
762
|
|
|
448
|
-
f = folium.FeatureGroup(name=label)
|
|
449
|
-
|
|
450
763
|
gdf = self._to_single_geom_type(gdf)
|
|
451
764
|
gdf = self._prepare_gdf_for_map(gdf)
|
|
452
765
|
|
|
@@ -464,9 +777,10 @@ class Explore(Map):
|
|
|
464
777
|
)
|
|
465
778
|
gjs.layer_name = label
|
|
466
779
|
|
|
467
|
-
gjs.add_to(f)
|
|
468
780
|
gjs.add_to(self.map)
|
|
469
781
|
|
|
782
|
+
self._rasters_to_background_maps()
|
|
783
|
+
|
|
470
784
|
_categorical_legend(
|
|
471
785
|
self.map,
|
|
472
786
|
self._column,
|
|
@@ -478,7 +792,7 @@ class Explore(Map):
|
|
|
478
792
|
|
|
479
793
|
def _add_tiles(
|
|
480
794
|
self, mapobj: folium.Map, tiles: list[str, xyzservices.TileProvider]
|
|
481
|
-
):
|
|
795
|
+
) -> None:
|
|
482
796
|
for tile in tiles:
|
|
483
797
|
to_tile(tile, max_zoom=self.max_zoom).add_to(mapobj)
|
|
484
798
|
|
|
@@ -490,9 +804,15 @@ class Explore(Map):
|
|
|
490
804
|
n_colors = len(np.unique(classified_sequential)) - any(self._nan_idx)
|
|
491
805
|
unique_colors = self._get_continous_colors(n=n_colors)
|
|
492
806
|
|
|
807
|
+
self._load_rasters_as_images()
|
|
808
|
+
|
|
493
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
|
|
494
814
|
self.map = self._make_folium_map(
|
|
495
|
-
bounds=
|
|
815
|
+
bounds=bounds,
|
|
496
816
|
max_zoom=self.max_zoom,
|
|
497
817
|
popup=self.popup,
|
|
498
818
|
prefer_canvas=self.prefer_canvas,
|
|
@@ -510,7 +830,6 @@ class Explore(Map):
|
|
|
510
830
|
for gdf, label, show in zip(self._gdfs, self.labels, self.show, strict=True):
|
|
511
831
|
if not len(gdf):
|
|
512
832
|
continue
|
|
513
|
-
f = folium.FeatureGroup(name=label)
|
|
514
833
|
|
|
515
834
|
gdf = self._to_single_geom_type(gdf)
|
|
516
835
|
gdf = self._prepare_gdf_for_map(gdf)
|
|
@@ -532,12 +851,15 @@ class Explore(Map):
|
|
|
532
851
|
**{
|
|
533
852
|
key: value
|
|
534
853
|
for key, value in self.kwargs.items()
|
|
535
|
-
if key not in ["title"]
|
|
854
|
+
if key not in ["title", "tiles"]
|
|
536
855
|
},
|
|
537
856
|
)
|
|
538
857
|
|
|
539
|
-
|
|
540
|
-
|
|
858
|
+
gjs.layer_name = label
|
|
859
|
+
|
|
860
|
+
gjs.add_to(self.map)
|
|
861
|
+
|
|
862
|
+
self._rasters_to_background_maps()
|
|
541
863
|
|
|
542
864
|
self.map.add_child(colorbar)
|
|
543
865
|
self.map.add_child(folium.LayerControl())
|
|
@@ -549,7 +871,7 @@ class Explore(Map):
|
|
|
549
871
|
return [col for col in gdf.columns if col not in COLS_TO_DROP]
|
|
550
872
|
|
|
551
873
|
@staticmethod
|
|
552
|
-
def _prepare_gdf_for_map(gdf):
|
|
874
|
+
def _prepare_gdf_for_map(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
553
875
|
if isinstance(gdf, GeoSeries):
|
|
554
876
|
gdf = gdf.to_frame("geometry")
|
|
555
877
|
|
|
@@ -567,13 +889,13 @@ class Explore(Map):
|
|
|
567
889
|
|
|
568
890
|
def _make_folium_map(
|
|
569
891
|
self,
|
|
570
|
-
bounds,
|
|
571
|
-
attr=None,
|
|
572
|
-
tiles=None,
|
|
573
|
-
width="100%",
|
|
574
|
-
height="100%",
|
|
575
|
-
control_scale=True,
|
|
576
|
-
map_kwds=None,
|
|
892
|
+
bounds: tuple[float, float, float, float],
|
|
893
|
+
attr: Any = None,
|
|
894
|
+
tiles: Any = None,
|
|
895
|
+
width: str = "100%",
|
|
896
|
+
height: str = "100%",
|
|
897
|
+
control_scale: bool = True,
|
|
898
|
+
map_kwds: dict | None = None,
|
|
577
899
|
**kwargs,
|
|
578
900
|
):
|
|
579
901
|
if not map_kwds:
|
|
@@ -679,21 +1001,29 @@ class Explore(Map):
|
|
|
679
1001
|
|
|
680
1002
|
def _make_geojson(
|
|
681
1003
|
self,
|
|
682
|
-
df,
|
|
1004
|
+
df: GeoDataFrame,
|
|
683
1005
|
show: bool,
|
|
684
|
-
color=None,
|
|
685
|
-
tooltip=True,
|
|
686
|
-
popup=False,
|
|
687
|
-
highlight=True,
|
|
688
|
-
marker_type=None,
|
|
689
|
-
marker_kwds=
|
|
690
|
-
style_kwds=
|
|
691
|
-
highlight_kwds=
|
|
692
|
-
tooltip_kwds=
|
|
693
|
-
popup_kwds=
|
|
694
|
-
map_kwds=
|
|
1006
|
+
color: str | None = None,
|
|
1007
|
+
tooltip: bool = True,
|
|
1008
|
+
popup: bool = False,
|
|
1009
|
+
highlight: bool = True,
|
|
1010
|
+
marker_type: str | None = None,
|
|
1011
|
+
marker_kwds: dict | None = None,
|
|
1012
|
+
style_kwds: dict | None = None,
|
|
1013
|
+
highlight_kwds: dict | None = None,
|
|
1014
|
+
tooltip_kwds: dict | None = None,
|
|
1015
|
+
popup_kwds: dict | None = None,
|
|
1016
|
+
map_kwds: dict | None = None,
|
|
695
1017
|
**kwargs,
|
|
696
|
-
):
|
|
1018
|
+
) -> folium.GeoJson:
|
|
1019
|
+
|
|
1020
|
+
marker_kwds = marker_kwds or {}
|
|
1021
|
+
style_kwds = style_kwds or {}
|
|
1022
|
+
highlight_kwds = highlight_kwds or {}
|
|
1023
|
+
tooltip_kwds = tooltip_kwds or {}
|
|
1024
|
+
popup_kwds = popup_kwds or {}
|
|
1025
|
+
map_kwds = map_kwds or {}
|
|
1026
|
+
|
|
697
1027
|
gdf = df.copy()
|
|
698
1028
|
|
|
699
1029
|
# convert LinearRing to LineString
|
|
@@ -704,7 +1034,8 @@ class Explore(Map):
|
|
|
704
1034
|
)
|
|
705
1035
|
|
|
706
1036
|
if gdf.crs is None:
|
|
707
|
-
|
|
1037
|
+
pass
|
|
1038
|
+
# kwargs["crs"] = "Simple"
|
|
708
1039
|
elif not gdf.crs.equals(4326):
|
|
709
1040
|
gdf = gdf.to_crs(4326)
|
|
710
1041
|
|
|
@@ -816,9 +1147,10 @@ class Explore(Map):
|
|
|
816
1147
|
)
|
|
817
1148
|
|
|
818
1149
|
|
|
819
|
-
def _tooltip_popup(
|
|
820
|
-
|
|
821
|
-
|
|
1150
|
+
def _tooltip_popup(
|
|
1151
|
+
type_: str, fields: Any, gdf: GeoDataFrame, **kwargs
|
|
1152
|
+
) -> folium.GeoJsonTooltip | folium.GeoJsonPopup:
|
|
1153
|
+
"""Get tooltip or popup."""
|
|
822
1154
|
# specify fields to show in the tooltip
|
|
823
1155
|
if fields is False or fields is None or fields == 0:
|
|
824
1156
|
return None
|
|
@@ -836,20 +1168,48 @@ def _tooltip_popup(type, fields, gdf, **kwds):
|
|
|
836
1168
|
|
|
837
1169
|
# Cast fields to str
|
|
838
1170
|
fields = list(map(str, fields))
|
|
839
|
-
if
|
|
840
|
-
return folium.GeoJsonTooltip(fields, **
|
|
841
|
-
elif
|
|
842
|
-
return folium.GeoJsonPopup(fields, **
|
|
1171
|
+
if type_ == "tooltip":
|
|
1172
|
+
return folium.GeoJsonTooltip(fields, **kwargs)
|
|
1173
|
+
elif type_ == "popup":
|
|
1174
|
+
return folium.GeoJsonPopup(fields, **kwargs)
|
|
1175
|
+
|
|
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]
|
|
843
1202
|
|
|
1203
|
+
if name in [x["label"] for x in out if "label" in x]:
|
|
1204
|
+
name += f"_{i}"
|
|
844
1205
|
|
|
845
|
-
|
|
846
|
-
crs = dataset.crs
|
|
847
|
-
bbox = dataset.bounds
|
|
1206
|
+
return name
|
|
848
1207
|
|
|
849
1208
|
|
|
850
|
-
def _categorical_legend(
|
|
851
|
-
|
|
852
|
-
|
|
1209
|
+
def _categorical_legend(
|
|
1210
|
+
m: folium.Map, title: str, categories: list[str], colors: list[str]
|
|
1211
|
+
) -> None:
|
|
1212
|
+
"""Add categorical legend to a map.
|
|
853
1213
|
|
|
854
1214
|
The implementation is using the code originally written by Michel Metran
|
|
855
1215
|
(@michelmetran) and released on GitHub
|
|
@@ -868,7 +1228,6 @@ def _categorical_legend(m, title, categories, colors):
|
|
|
868
1228
|
colors : list-like
|
|
869
1229
|
list of colors (in the same order as categories)
|
|
870
1230
|
"""
|
|
871
|
-
|
|
872
1231
|
# Header to Add
|
|
873
1232
|
head = """
|
|
874
1233
|
{% macro header(this, kwargs) %}
|