ssb-sgis 1.0.3__py3-none-any.whl → 1.0.5__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 -3
- sgis/debug_config.py +24 -0
- sgis/geopandas_tools/bounds.py +16 -21
- sgis/geopandas_tools/buffer_dissolve_explode.py +112 -30
- sgis/geopandas_tools/centerlines.py +4 -91
- sgis/geopandas_tools/cleaning.py +1576 -583
- sgis/geopandas_tools/conversion.py +24 -14
- sgis/geopandas_tools/duplicates.py +27 -6
- sgis/geopandas_tools/general.py +259 -100
- sgis/geopandas_tools/geometry_types.py +1 -1
- sgis/geopandas_tools/neighbors.py +16 -12
- sgis/geopandas_tools/overlay.py +7 -3
- sgis/geopandas_tools/point_operations.py +3 -3
- sgis/geopandas_tools/polygon_operations.py +505 -100
- sgis/geopandas_tools/polygons_as_rings.py +40 -8
- sgis/geopandas_tools/sfilter.py +26 -9
- sgis/io/dapla_functions.py +238 -19
- sgis/maps/examine.py +11 -10
- sgis/maps/explore.py +227 -155
- sgis/maps/legend.py +13 -4
- sgis/maps/map.py +22 -13
- sgis/maps/maps.py +100 -29
- sgis/maps/thematicmap.py +25 -18
- sgis/networkanalysis/_service_area.py +6 -1
- sgis/networkanalysis/cutting_lines.py +12 -5
- sgis/networkanalysis/finding_isolated_networks.py +13 -6
- sgis/networkanalysis/networkanalysis.py +10 -12
- sgis/parallel/parallel.py +27 -10
- sgis/raster/base.py +208 -0
- sgis/raster/cube.py +3 -3
- sgis/raster/image_collection.py +1421 -724
- sgis/raster/indices.py +10 -7
- sgis/raster/raster.py +7 -7
- sgis/raster/sentinel_config.py +33 -17
- {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.5.dist-info}/METADATA +6 -7
- ssb_sgis-1.0.5.dist-info/RECORD +62 -0
- ssb_sgis-1.0.3.dist-info/RECORD +0 -61
- {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.5.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.3.dist-info → ssb_sgis-1.0.5.dist-info}/WHEEL +0 -0
sgis/maps/maps.py
CHANGED
|
@@ -19,6 +19,7 @@ from shapely import Geometry
|
|
|
19
19
|
from shapely import box
|
|
20
20
|
from shapely.geometry import Polygon
|
|
21
21
|
|
|
22
|
+
from ..debug_config import _NoExplore
|
|
22
23
|
from ..geopandas_tools.bounds import get_total_bounds
|
|
23
24
|
from ..geopandas_tools.conversion import to_bbox
|
|
24
25
|
from ..geopandas_tools.conversion import to_gdf
|
|
@@ -84,7 +85,8 @@ def explore(
|
|
|
84
85
|
browser: bool = False,
|
|
85
86
|
smooth_factor: int | float = 1.5,
|
|
86
87
|
size: int | None = None,
|
|
87
|
-
max_images: int =
|
|
88
|
+
max_images: int = 10,
|
|
89
|
+
images_to_gdf: bool = False,
|
|
88
90
|
**kwargs,
|
|
89
91
|
) -> Explore:
|
|
90
92
|
"""Interactive map of GeoDataFrames with layers that can be toggled on/off.
|
|
@@ -113,7 +115,9 @@ def explore(
|
|
|
113
115
|
size: The buffer distance. Only used when center is given. It then defaults to
|
|
114
116
|
1000.
|
|
115
117
|
max_images: Maximum number of images (Image, ImageCollection, Band) to show per
|
|
116
|
-
map. Defaults to
|
|
118
|
+
map. Defaults to 10.
|
|
119
|
+
images_to_gdf: If True (not default), images (Image, ImageCollection, Band)
|
|
120
|
+
will be converted to GeoDataFrame and added to the map.
|
|
117
121
|
**kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
|
|
118
122
|
instance 'cmap' to change the colors, 'scheme' to change how the data
|
|
119
123
|
is grouped. This defaults to 'fisherjenkssampled' for numeric data.
|
|
@@ -143,6 +147,9 @@ def explore(
|
|
|
143
147
|
>>> points["meters"] = points.length
|
|
144
148
|
>>> sg.explore(roads, points, column="meters", cmap="plasma", max_zoom=60, center_4326=(10.7463, 59.92, 500))
|
|
145
149
|
"""
|
|
150
|
+
if isinstance(center, _NoExplore):
|
|
151
|
+
return
|
|
152
|
+
|
|
146
153
|
gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
|
|
147
154
|
|
|
148
155
|
loc_mask, kwargs = _get_location_mask(kwargs | {"size": size}, gdfs)
|
|
@@ -186,13 +193,63 @@ def explore(
|
|
|
186
193
|
mask = to_gdf(center, crs=from_crs)
|
|
187
194
|
|
|
188
195
|
bounds: Polygon = box(*get_total_bounds(*gdfs, *list(kwargs.values())))
|
|
189
|
-
if not mask.intersects(bounds).any():
|
|
190
|
-
mask = mask.set_crs(4326, allow_override=True)
|
|
191
196
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
any_intersections: bool = mask.intersects(bounds).any()
|
|
198
|
+
if not any_intersections and to_crs is None:
|
|
199
|
+
mask = to_gdf(Polygon(), to_crs)
|
|
200
|
+
elif not any_intersections:
|
|
201
|
+
bounds4326 = to_gdf(bounds, to_crs).to_crs(25833).geometry.iloc[0]
|
|
202
|
+
mask4326 = mask.set_crs(4326, allow_override=True).to_crs(25833)
|
|
203
|
+
|
|
204
|
+
if (mask4326.distance(bounds4326) > size).all():
|
|
205
|
+
# try flipping coordinates
|
|
206
|
+
x, y = next(iter(mask.geometry.iloc[0].coords))
|
|
207
|
+
mask4326 = to_gdf([y, x], 4326).to_crs(25833)
|
|
208
|
+
|
|
209
|
+
if (mask4326.distance(bounds4326) > size).all():
|
|
210
|
+
mask = to_gdf(Polygon(), to_crs)
|
|
211
|
+
else:
|
|
212
|
+
mask = mask4326.to_crs(to_crs)
|
|
213
|
+
|
|
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
|
|
196
253
|
|
|
197
254
|
if get_geom_type(mask) in ["point", "line"]:
|
|
198
255
|
mask = mask.buffer(size)
|
|
@@ -236,7 +293,7 @@ def samplemap(
|
|
|
236
293
|
smooth_factor: int = 1.5,
|
|
237
294
|
explore: bool = True,
|
|
238
295
|
browser: bool = False,
|
|
239
|
-
max_images: int =
|
|
296
|
+
max_images: int = 10,
|
|
240
297
|
**kwargs,
|
|
241
298
|
) -> Explore:
|
|
242
299
|
"""Shows an interactive map of a random area of GeoDataFrames.
|
|
@@ -269,7 +326,7 @@ def samplemap(
|
|
|
269
326
|
browser: If False (default), the maps will be shown in Jupyter.
|
|
270
327
|
If True the maps will be opened in a browser folder.
|
|
271
328
|
max_images: Maximum number of images (Image, ImageCollection, Band) to show per
|
|
272
|
-
map. Defaults to
|
|
329
|
+
map. Defaults to 10.
|
|
273
330
|
**kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
|
|
274
331
|
instance 'cmap' to change the colors, 'scheme' to change how the data
|
|
275
332
|
is grouped. This defaults to 'fisherjenkssampled' for numeric data.
|
|
@@ -294,6 +351,9 @@ def samplemap(
|
|
|
294
351
|
>>> samplemap(roads, points, size=5_000, column="meters")
|
|
295
352
|
|
|
296
353
|
"""
|
|
354
|
+
if isinstance(kwargs.get("center", None), _NoExplore):
|
|
355
|
+
return
|
|
356
|
+
|
|
297
357
|
if gdfs and len(gdfs) > 1 and isinstance(gdfs[-1], (float, int)):
|
|
298
358
|
*gdfs, size = gdfs
|
|
299
359
|
|
|
@@ -311,7 +371,7 @@ def samplemap(
|
|
|
311
371
|
|
|
312
372
|
if mask is None:
|
|
313
373
|
try:
|
|
314
|
-
sample = sample.geometry.
|
|
374
|
+
sample = sample.geometry.loc[lambda x: ~x.is_empty].sample(1)
|
|
315
375
|
except Exception:
|
|
316
376
|
try:
|
|
317
377
|
sample = sample.sample(1)
|
|
@@ -327,11 +387,16 @@ def samplemap(
|
|
|
327
387
|
except Exception:
|
|
328
388
|
sample = to_gdf(to_shapely(to_bbox(sample))).explode(ignore_index=True)
|
|
329
389
|
|
|
330
|
-
sample = sample.clip(mask).sample(1)
|
|
390
|
+
sample = sample.clip(mask).explode(ignore_index=True).sample(1)
|
|
331
391
|
|
|
392
|
+
print(locals())
|
|
332
393
|
random_point = sample.sample_points(size=1)
|
|
333
394
|
|
|
334
|
-
|
|
395
|
+
try:
|
|
396
|
+
center = (random_point.geometry.iloc[0].x, random_point.geometry.iloc[0].y)
|
|
397
|
+
except AttributeError as e:
|
|
398
|
+
raise AttributeError(e, random_point.geometry.iloc[0]) from e
|
|
399
|
+
|
|
335
400
|
print(f"center={center}, size={size}")
|
|
336
401
|
|
|
337
402
|
mask = random_point.buffer(size)
|
|
@@ -357,7 +422,7 @@ def clipmap(
|
|
|
357
422
|
max_zoom: int = 40,
|
|
358
423
|
smooth_factor: int | float = 1.5,
|
|
359
424
|
browser: bool = False,
|
|
360
|
-
max_images: int =
|
|
425
|
+
max_images: int = 10,
|
|
361
426
|
**kwargs,
|
|
362
427
|
) -> Explore | Map:
|
|
363
428
|
"""Shows an interactive map of a of GeoDataFrames clipped to the mask extent.
|
|
@@ -385,7 +450,7 @@ def clipmap(
|
|
|
385
450
|
browser: If False (default), the maps will be shown in Jupyter.
|
|
386
451
|
If True the maps will be opened in a browser folder.
|
|
387
452
|
max_images: Maximum number of images (Image, ImageCollection, Band) to show per
|
|
388
|
-
map. Defaults to
|
|
453
|
+
map. Defaults to 10.
|
|
389
454
|
**kwargs: Keyword arguments to pass to geopandas.GeoDataFrame.explore, for
|
|
390
455
|
instance 'cmap' to change the colors, 'scheme' to change how the data
|
|
391
456
|
is grouped. This defaults to 'fisherjenkssampled' for numeric data.
|
|
@@ -395,8 +460,10 @@ def clipmap(
|
|
|
395
460
|
explore: same functionality, but shows the entire area of the geometries.
|
|
396
461
|
samplemap: same functionality, but shows only a random area of a given size.
|
|
397
462
|
"""
|
|
398
|
-
|
|
463
|
+
if isinstance(kwargs.get("center", None), _NoExplore):
|
|
464
|
+
return
|
|
399
465
|
|
|
466
|
+
gdfs, column, kwargs = Map._separate_args(gdfs, column, kwargs)
|
|
400
467
|
if mask is None and len(gdfs) > 1:
|
|
401
468
|
mask = gdfs[-1]
|
|
402
469
|
gdfs = gdfs[:-1]
|
|
@@ -450,7 +517,9 @@ def clipmap(
|
|
|
450
517
|
return m
|
|
451
518
|
|
|
452
519
|
|
|
453
|
-
def explore_locals(
|
|
520
|
+
def explore_locals(
|
|
521
|
+
*gdfs: GeoDataFrame, convert: bool = True, crs: Any | None = None, **kwargs
|
|
522
|
+
) -> None:
|
|
454
523
|
"""Displays all local variables with geometries (GeoDataFrame etc.).
|
|
455
524
|
|
|
456
525
|
Local means inside a function or file/notebook.
|
|
@@ -459,8 +528,11 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
|
|
|
459
528
|
*gdfs: Additional GeoDataFrames.
|
|
460
529
|
convert: If True (default), non-GeoDataFrames will be converted
|
|
461
530
|
to GeoDataFrames if possible.
|
|
531
|
+
crs: Optional crs if no objects have any crs.
|
|
462
532
|
**kwargs: keyword arguments passed to sg.explore.
|
|
463
533
|
"""
|
|
534
|
+
if isinstance(kwargs.get("center", None), _NoExplore):
|
|
535
|
+
return
|
|
464
536
|
|
|
465
537
|
def as_dict(obj):
|
|
466
538
|
if hasattr(obj, "__dict__"):
|
|
@@ -482,11 +554,19 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
|
|
|
482
554
|
if not convert:
|
|
483
555
|
continue
|
|
484
556
|
|
|
557
|
+
try:
|
|
558
|
+
gdf = clean_geoms(to_gdf(value, crs=crs))
|
|
559
|
+
if len(gdf):
|
|
560
|
+
local_gdfs[name] = gdf
|
|
561
|
+
continue
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
|
|
485
565
|
if isinstance(value, dict) or hasattr(value, "__dict__"):
|
|
486
566
|
# add dicts or classes with GeoDataFrames to kwargs
|
|
487
567
|
for key, val in as_dict(value).items():
|
|
488
568
|
if isinstance(val, allowed_types):
|
|
489
|
-
gdf = clean_geoms(to_gdf(val))
|
|
569
|
+
gdf = clean_geoms(to_gdf(val, crs=crs))
|
|
490
570
|
if len(gdf):
|
|
491
571
|
local_gdfs[key] = gdf
|
|
492
572
|
|
|
@@ -494,22 +574,13 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
|
|
|
494
574
|
try:
|
|
495
575
|
for k, v in val.items():
|
|
496
576
|
if isinstance(v, allowed_types):
|
|
497
|
-
gdf = clean_geoms(to_gdf(v))
|
|
577
|
+
gdf = clean_geoms(to_gdf(v, crs=crs))
|
|
498
578
|
if len(gdf):
|
|
499
579
|
local_gdfs[k] = gdf
|
|
500
580
|
except Exception:
|
|
501
581
|
# no need to raise here
|
|
502
582
|
pass
|
|
503
583
|
|
|
504
|
-
continue
|
|
505
|
-
try:
|
|
506
|
-
gdf = clean_geoms(to_gdf(value))
|
|
507
|
-
if len(gdf):
|
|
508
|
-
local_gdfs[name] = gdf
|
|
509
|
-
continue
|
|
510
|
-
except Exception:
|
|
511
|
-
pass
|
|
512
|
-
|
|
513
584
|
if local_gdfs:
|
|
514
585
|
break
|
|
515
586
|
|
|
@@ -518,7 +589,7 @@ def explore_locals(*gdfs: GeoDataFrame, convert: bool = True, **kwargs) -> None:
|
|
|
518
589
|
if not frame:
|
|
519
590
|
break
|
|
520
591
|
|
|
521
|
-
return explore(*gdfs, **local_gdfs
|
|
592
|
+
return explore(*gdfs, **(local_gdfs | kwargs))
|
|
522
593
|
|
|
523
594
|
|
|
524
595
|
def qtm(
|
sgis/maps/thematicmap.py
CHANGED
|
@@ -68,24 +68,24 @@ class ThematicMap(Map):
|
|
|
68
68
|
legend_kwargs: dictionary with attributes for the legend. E.g.:
|
|
69
69
|
title: Legend title. Defaults to the column name.
|
|
70
70
|
rounding: If positive number, it will round floats to n decimals.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
71
|
+
If negative, eg. -2, the number 3429 is rounded to 3400.
|
|
72
|
+
By default, the rounding depends on the column's maximum value
|
|
73
|
+
and standard deviation.
|
|
74
74
|
position: The legend's x and y position in the plot. By default, it's
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
decided dynamically by finding the space with most distance to
|
|
76
|
+
the geometries. To be specified as a tuple of
|
|
77
|
+
x and y position between 0 and 1. E.g. position=(0.8, 0.2) for a position
|
|
78
|
+
in the bottom right corner, (0.2, 0.8) for the upper left corner.
|
|
79
79
|
pretty_labels: Whether to capitalize words in text categories.
|
|
80
80
|
label_suffix: For numeric columns. The text to put after each number
|
|
81
|
-
|
|
81
|
+
in the legend labels. Defaults to None.
|
|
82
82
|
label_sep: For numeric columns. Text to put in between the two numbers
|
|
83
|
-
|
|
83
|
+
in each color group in the legend. Defaults to '-'.
|
|
84
84
|
thousand_sep: For numeric columns. Separator between each thousand for
|
|
85
|
-
|
|
85
|
+
large numbers. Defaults to None, meaning no separator.
|
|
86
86
|
decimal_mark: For numeric columns. Text to use as decimal point.
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
Defaults to None, meaning '.' (dot) unless 'thousand_sep' is
|
|
88
|
+
'.'. In this case, ',' (comma) will be used as decimal mark.
|
|
89
89
|
**kwargs: Additional attributes for the map. E.g.:
|
|
90
90
|
title_color (str): Color of the title font.
|
|
91
91
|
title_fontsize (int): Color of the title font.
|
|
@@ -96,7 +96,7 @@ class ThematicMap(Map):
|
|
|
96
96
|
nan_color: Color for missing data.
|
|
97
97
|
|
|
98
98
|
Examples:
|
|
99
|
-
|
|
99
|
+
---------
|
|
100
100
|
>>> import sgis as sg
|
|
101
101
|
>>> points = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
|
|
102
102
|
>>> points2 = sg.random_points(100, loc=1000).pipe(sg.buff, np.random.rand(100) * 100)
|
|
@@ -166,6 +166,7 @@ class ThematicMap(Map):
|
|
|
166
166
|
nan_label: str = "Missing",
|
|
167
167
|
legend_kwargs: dict | None = None,
|
|
168
168
|
title_kwargs: dict | None = None,
|
|
169
|
+
legend: bool = False,
|
|
169
170
|
**kwargs,
|
|
170
171
|
) -> None:
|
|
171
172
|
"""Initialiser."""
|
|
@@ -178,6 +179,9 @@ class ThematicMap(Map):
|
|
|
178
179
|
nan_label=nan_label,
|
|
179
180
|
)
|
|
180
181
|
|
|
182
|
+
if not legend:
|
|
183
|
+
self.legend = None
|
|
184
|
+
|
|
181
185
|
self.title = title
|
|
182
186
|
self._size = size
|
|
183
187
|
self._dark = dark
|
|
@@ -239,10 +243,11 @@ class ThematicMap(Map):
|
|
|
239
243
|
raise TypeError(
|
|
240
244
|
f"{self.__class__.__name__} legend_kwargs got an unexpected key {key}"
|
|
241
245
|
)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
if self.legend is not None:
|
|
247
|
+
try:
|
|
248
|
+
setattr(self.legend, key, value)
|
|
249
|
+
except Exception:
|
|
250
|
+
setattr(self.legend, f"_{key}", value)
|
|
246
251
|
|
|
247
252
|
@property
|
|
248
253
|
def valid_keywords(self) -> set[str]:
|
|
@@ -398,7 +403,7 @@ class ThematicMap(Map):
|
|
|
398
403
|
return kwargs
|
|
399
404
|
|
|
400
405
|
else:
|
|
401
|
-
if self.legend.rounding and self.legend.rounding < 0:
|
|
406
|
+
if self.legend and self.legend.rounding and self.legend.rounding < 0:
|
|
402
407
|
self.bins = prettify_bins(self.bins, self.legend.rounding)
|
|
403
408
|
self.bins = list({round(bin_, 5) for bin_ in self.bins})
|
|
404
409
|
self.bins.sort()
|
|
@@ -463,6 +468,8 @@ class ThematicMap(Map):
|
|
|
463
468
|
|
|
464
469
|
def _create_legend(self) -> None:
|
|
465
470
|
"""Instantiate the Legend class."""
|
|
471
|
+
if self.legend is None:
|
|
472
|
+
return
|
|
466
473
|
kwargs = {}
|
|
467
474
|
if self._dark:
|
|
468
475
|
kwargs["facecolor"] = "#0f0f0f"
|
|
@@ -5,6 +5,7 @@ from igraph import Graph
|
|
|
5
5
|
from shapely import force_2d
|
|
6
6
|
from shapely import reverse
|
|
7
7
|
from shapely import unary_union
|
|
8
|
+
from shapely import union_all
|
|
8
9
|
from shapely.geometry import MultiPoint
|
|
9
10
|
from shapely.geometry import Point
|
|
10
11
|
from shapely.ops import nearest_points
|
|
@@ -113,7 +114,11 @@ def _service_area(
|
|
|
113
114
|
else:
|
|
114
115
|
snapped_origin: Point = nearest_points(
|
|
115
116
|
nodes_union,
|
|
116
|
-
|
|
117
|
+
union_all(
|
|
118
|
+
origins.loc[
|
|
119
|
+
origins["temp_idx"] == idx, "geometry"
|
|
120
|
+
].geometry.values
|
|
121
|
+
),
|
|
117
122
|
)[0]
|
|
118
123
|
|
|
119
124
|
within = sfilter(within, snapped_origin.buffer(0.01))
|
|
@@ -77,7 +77,7 @@ def split_lines_by_nearest_point(
|
|
|
77
77
|
"""
|
|
78
78
|
PRECISION = 1e-6
|
|
79
79
|
|
|
80
|
-
if not len(gdf):
|
|
80
|
+
if not len(gdf) or not len(points):
|
|
81
81
|
return gdf
|
|
82
82
|
|
|
83
83
|
if (points.crs is not None and gdf.crs is not None) and not points.crs.equals(
|
|
@@ -86,10 +86,14 @@ def split_lines_by_nearest_point(
|
|
|
86
86
|
raise ValueError("crs mismatch:", points.crs, "and", gdf.crs)
|
|
87
87
|
|
|
88
88
|
if get_geom_type(gdf) != "line":
|
|
89
|
-
raise ValueError(
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"'gdf' should only have line geometriess. Got {gdf.geom_type.value_counts()}"
|
|
91
|
+
)
|
|
90
92
|
|
|
91
93
|
if get_geom_type(points) != "point":
|
|
92
|
-
raise ValueError(
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"'points' should only have point geometries. Got {points.geom_type.value_counts()}"
|
|
96
|
+
)
|
|
93
97
|
|
|
94
98
|
gdf = gdf.copy()
|
|
95
99
|
|
|
@@ -230,9 +234,12 @@ def _change_line_endpoint(
|
|
|
230
234
|
.values
|
|
231
235
|
)
|
|
232
236
|
|
|
233
|
-
|
|
237
|
+
is_line = relevant_lines.groupby(level=0).size() > 1
|
|
238
|
+
relevant_lines_mapped = (
|
|
239
|
+
relevant_lines.loc[is_line].groupby(level=0)["geometry"].agg(LineString)
|
|
240
|
+
)
|
|
234
241
|
|
|
235
|
-
gdf.loc[
|
|
242
|
+
gdf.loc[relevant_lines_mapped.index, "geometry"] = relevant_lines_mapped
|
|
236
243
|
|
|
237
244
|
return gdf
|
|
238
245
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Functions for Finding network components in a GeoDataFrame of lines."""
|
|
2
2
|
|
|
3
3
|
import networkx as nx
|
|
4
|
+
import pandas as pd
|
|
4
5
|
from geopandas import GeoDataFrame
|
|
5
6
|
|
|
6
7
|
from .nodes import make_node_ids
|
|
@@ -81,7 +82,7 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
|
81
82
|
>>> roads = read_parquet_url("https://media.githubusercontent.com/media/statisticsnorway/ssb-sgis/main/tests/testdata/roads_oslo_2022.parquet")
|
|
82
83
|
|
|
83
84
|
>>> roads = get_component_size(roads)
|
|
84
|
-
>>> roads
|
|
85
|
+
>>> roads["component_size"].value_counts().head()
|
|
85
86
|
component_size
|
|
86
87
|
79180 85638
|
|
87
88
|
2 1601
|
|
@@ -101,11 +102,17 @@ def get_component_size(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
|
101
102
|
graph.add_edges_from(edges)
|
|
102
103
|
components = [list(x) for x in nx.connected_components(graph)]
|
|
103
104
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
mapper = pd.DataFrame(
|
|
106
|
+
{
|
|
107
|
+
idx: [i, len(component)]
|
|
108
|
+
for i, component in enumerate(components)
|
|
109
|
+
for idx in component
|
|
110
|
+
},
|
|
111
|
+
).transpose()
|
|
112
|
+
mapper.columns = ["component_index", "component_size"]
|
|
113
|
+
|
|
114
|
+
gdf["component_index"] = gdf["source"].map(mapper["component_index"])
|
|
115
|
+
gdf["component_size"] = gdf["source"].map(mapper["component_size"])
|
|
109
116
|
|
|
110
117
|
gdf = gdf.drop(
|
|
111
118
|
["source_wkt", "target_wkt", "source", "target", "n_source", "n_target"], axis=1
|
|
@@ -1027,7 +1027,7 @@ class NetworkAnalysis:
|
|
|
1027
1027
|
|
|
1028
1028
|
if dissolve:
|
|
1029
1029
|
results = results.dissolve(by=["origin", self.rules.weight]).loc[
|
|
1030
|
-
:, [
|
|
1030
|
+
:, [results.geometry.name]
|
|
1031
1031
|
]
|
|
1032
1032
|
|
|
1033
1033
|
results = results.reset_index()
|
|
@@ -1038,7 +1038,7 @@ class NetworkAnalysis:
|
|
|
1038
1038
|
].rename(columns={"temp_idx": "origin"})[["origin"]]
|
|
1039
1039
|
|
|
1040
1040
|
if len(missing):
|
|
1041
|
-
missing[
|
|
1041
|
+
missing[results.geometry.name] = pd.NA
|
|
1042
1042
|
results = pd.concat([results, missing], ignore_index=True)
|
|
1043
1043
|
|
|
1044
1044
|
results["origin"] = results["origin"].map(self.origins.idx_dict)
|
|
@@ -1151,7 +1151,7 @@ class NetworkAnalysis:
|
|
|
1151
1151
|
if not all(results.geometry.isna()):
|
|
1152
1152
|
if dissolve:
|
|
1153
1153
|
results = results.dissolve(by=["origin", self.rules.weight]).loc[
|
|
1154
|
-
:, [
|
|
1154
|
+
:, [results.geometry.name]
|
|
1155
1155
|
]
|
|
1156
1156
|
else:
|
|
1157
1157
|
results = results.dissolve(
|
|
@@ -1166,7 +1166,7 @@ class NetworkAnalysis:
|
|
|
1166
1166
|
].rename(columns={"temp_idx": "origin"})[["origin"]]
|
|
1167
1167
|
|
|
1168
1168
|
if len(missing):
|
|
1169
|
-
missing[
|
|
1169
|
+
missing[results.geometry.name] = pd.NA
|
|
1170
1170
|
results = pd.concat([results, missing], ignore_index=True)
|
|
1171
1171
|
|
|
1172
1172
|
results["origin"] = results["origin"].map(self.origins.idx_dict)
|
|
@@ -1329,7 +1329,7 @@ class NetworkAnalysis:
|
|
|
1329
1329
|
df["cost_std"] = results[self.rules.weight].std()
|
|
1330
1330
|
|
|
1331
1331
|
if fun == "service_area":
|
|
1332
|
-
df["percent_missing"] = results[
|
|
1332
|
+
df["percent_missing"] = results[results.geometry.name].isna().mean() * 100
|
|
1333
1333
|
else:
|
|
1334
1334
|
df["destinations_count"] = len(self.destinations.gdf)
|
|
1335
1335
|
|
|
@@ -1456,7 +1456,7 @@ class NetworkAnalysis:
|
|
|
1456
1456
|
else:
|
|
1457
1457
|
points = self.origins.gdf
|
|
1458
1458
|
|
|
1459
|
-
points = points.drop_duplicates(
|
|
1459
|
+
points = points.drop_duplicates(points.geometry.name)
|
|
1460
1460
|
|
|
1461
1461
|
self.network.gdf["meters_"] = self.network.gdf.length
|
|
1462
1462
|
|
|
@@ -1573,7 +1573,7 @@ class NetworkAnalysis:
|
|
|
1573
1573
|
This method is best stored in the NetworkAnalysis class,
|
|
1574
1574
|
since the point classes are instantiated each time an analysis is run.
|
|
1575
1575
|
"""
|
|
1576
|
-
if self.wkts[what]
|
|
1576
|
+
if not np.array_equal(self.wkts[what], points.geometry.to_wkt().values):
|
|
1577
1577
|
return True
|
|
1578
1578
|
|
|
1579
1579
|
if not all(x in self.graph.vs["name"] for x in list(points.temp_idx.values)):
|
|
@@ -1590,17 +1590,15 @@ class NetworkAnalysis:
|
|
|
1590
1590
|
"""
|
|
1591
1591
|
self.wkts = {}
|
|
1592
1592
|
|
|
1593
|
-
self.wkts["network"] =
|
|
1593
|
+
self.wkts["network"] = self.network.gdf.geometry.to_wkt().values
|
|
1594
1594
|
|
|
1595
1595
|
if not hasattr(self, "origins"):
|
|
1596
1596
|
return
|
|
1597
1597
|
|
|
1598
|
-
self.wkts["origins"] =
|
|
1598
|
+
self.wkts["origins"] = self.origins.gdf.geometry.to_wkt().values
|
|
1599
1599
|
|
|
1600
1600
|
if self.destinations is not None:
|
|
1601
|
-
self.wkts["destinations"] =
|
|
1602
|
-
geom.wkt for geom in self.destinations.gdf.geometry
|
|
1603
|
-
]
|
|
1601
|
+
self.wkts["destinations"] = self.destinations.gdf.geometry.to_wkt().values
|
|
1604
1602
|
|
|
1605
1603
|
@staticmethod
|
|
1606
1604
|
def _sort_breaks(breaks: str | list | tuple | int | float) -> list[float | int]:
|
sgis/parallel/parallel.py
CHANGED
|
@@ -32,7 +32,6 @@ try:
|
|
|
32
32
|
from ..io.dapla_functions import read_geopandas
|
|
33
33
|
from ..io.dapla_functions import write_geopandas
|
|
34
34
|
|
|
35
|
-
# from ..io.write_municipality_data import write_municipality_data
|
|
36
35
|
except ImportError:
|
|
37
36
|
pass
|
|
38
37
|
|
|
@@ -40,11 +39,8 @@ except ImportError:
|
|
|
40
39
|
try:
|
|
41
40
|
from dapla import read_pandas
|
|
42
41
|
from dapla import write_pandas
|
|
43
|
-
from dapla.gcs import GCSFileSystem
|
|
44
42
|
except ImportError:
|
|
45
|
-
|
|
46
|
-
class GCSFileSystem:
|
|
47
|
-
"""Placeholder."""
|
|
43
|
+
pass
|
|
48
44
|
|
|
49
45
|
|
|
50
46
|
class Parallel:
|
|
@@ -806,7 +802,11 @@ def _write_one_muni(
|
|
|
806
802
|
|
|
807
803
|
if not len(gdf_muni):
|
|
808
804
|
if write_empty:
|
|
809
|
-
|
|
805
|
+
try:
|
|
806
|
+
geom_col = gdf.geometry.name
|
|
807
|
+
except AttributeError:
|
|
808
|
+
geom_col = "geometry"
|
|
809
|
+
gdf_muni = gdf_muni.drop(columns=geom_col, errors="ignore")
|
|
810
810
|
gdf_muni["geometry"] = None
|
|
811
811
|
write_pandas(gdf_muni, out)
|
|
812
812
|
return
|
|
@@ -834,7 +834,11 @@ def _write_one_muni_with_neighbors(
|
|
|
834
834
|
|
|
835
835
|
if not len(gdf_neighbor):
|
|
836
836
|
if write_empty:
|
|
837
|
-
|
|
837
|
+
try:
|
|
838
|
+
geom_col = gdf.geometry.name
|
|
839
|
+
except AttributeError:
|
|
840
|
+
geom_col = "geometry"
|
|
841
|
+
gdf_neighbor = gdf_neighbor.drop(columns=geom_col, errors="ignore")
|
|
838
842
|
gdf_neighbor["geometry"] = None
|
|
839
843
|
write_pandas(gdf_neighbor, out)
|
|
840
844
|
return
|
|
@@ -880,7 +884,9 @@ def _fix_missing_muni_numbers(
|
|
|
880
884
|
)
|
|
881
885
|
|
|
882
886
|
try:
|
|
883
|
-
municipalities = municipalities[
|
|
887
|
+
municipalities = municipalities[
|
|
888
|
+
[muni_number_col, municipalities.geometry.name]
|
|
889
|
+
].to_crs(gdf.crs)
|
|
884
890
|
except Exception as e:
|
|
885
891
|
raise e.__class__(e, to_print) from e
|
|
886
892
|
|
|
@@ -967,10 +973,21 @@ def parallel_overlay(
|
|
|
967
973
|
|
|
968
974
|
|
|
969
975
|
def _clean_intersection(
|
|
970
|
-
df1: GeoDataFrame, df2: GeoDataFrame, to_print: str =
|
|
976
|
+
df1: GeoDataFrame, df2: GeoDataFrame, to_print: str | None = None
|
|
971
977
|
) -> GeoDataFrame:
|
|
972
978
|
print(to_print, "- intersection chunk len:", len(df1))
|
|
973
|
-
|
|
979
|
+
cols_to_keep = df1.columns.union(df2.columns.difference({df2.geometry.name}))
|
|
980
|
+
df1["_range_idx"] = range(len(df1))
|
|
981
|
+
joined = df1.sjoin(df2, predicate="within", how="left")
|
|
982
|
+
within = joined.loc[joined["_range_idx"].notna(), cols_to_keep]
|
|
983
|
+
not_within = joined.loc[joined["_range_idx"].isna(), df1.columns]
|
|
984
|
+
return pd.concat(
|
|
985
|
+
[
|
|
986
|
+
within,
|
|
987
|
+
clean_overlay(not_within, df2, how="intersection"),
|
|
988
|
+
],
|
|
989
|
+
ignore_index=True,
|
|
990
|
+
)
|
|
974
991
|
|
|
975
992
|
|
|
976
993
|
def chunkwise(
|