ssb-sgis 1.0.4__py3-none-any.whl → 1.0.6__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 +5 -5
- sgis/debug_config.py +1 -0
- sgis/geopandas_tools/buffer_dissolve_explode.py +3 -40
- sgis/geopandas_tools/conversion.py +37 -9
- sgis/geopandas_tools/general.py +330 -106
- sgis/geopandas_tools/geometry_types.py +38 -33
- sgis/geopandas_tools/overlay.py +5 -1
- sgis/io/dapla_functions.py +33 -17
- sgis/maps/explore.py +16 -5
- sgis/maps/map.py +3 -0
- sgis/maps/maps.py +0 -1
- sgis/networkanalysis/closing_network_holes.py +100 -22
- sgis/networkanalysis/cutting_lines.py +4 -147
- sgis/networkanalysis/finding_isolated_networks.py +6 -0
- sgis/networkanalysis/nodes.py +4 -110
- sgis/parallel/parallel.py +267 -182
- sgis/raster/image_collection.py +789 -836
- sgis/raster/indices.py +0 -90
- sgis/raster/regex.py +146 -0
- sgis/raster/sentinel_config.py +9 -0
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/METADATA +1 -1
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/RECORD +24 -26
- sgis/raster/cube.py +0 -1274
- sgis/raster/cubebase.py +0 -25
- sgis/raster/raster.py +0 -1475
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/LICENSE +0 -0
- {ssb_sgis-1.0.4.dist-info → ssb_sgis-1.0.6.dist-info}/WHEEL +0 -0
sgis/geopandas_tools/general.py
CHANGED
|
@@ -19,9 +19,6 @@ from numpy.typing import NDArray
|
|
|
19
19
|
from shapely import Geometry
|
|
20
20
|
from shapely import extract_unique_points
|
|
21
21
|
from shapely import get_coordinates
|
|
22
|
-
from shapely import get_exterior_ring
|
|
23
|
-
from shapely import get_interior_ring
|
|
24
|
-
from shapely import get_num_interior_rings
|
|
25
22
|
from shapely import get_parts
|
|
26
23
|
from shapely import linestrings
|
|
27
24
|
from shapely import make_valid
|
|
@@ -30,7 +27,6 @@ from shapely import union_all
|
|
|
30
27
|
from shapely.geometry import LineString
|
|
31
28
|
from shapely.geometry import MultiPoint
|
|
32
29
|
from shapely.geometry import Point
|
|
33
|
-
from shapely.geometry import Polygon
|
|
34
30
|
|
|
35
31
|
from .conversion import coordinate_array
|
|
36
32
|
from .conversion import to_bbox
|
|
@@ -39,6 +35,8 @@ from .conversion import to_geoseries
|
|
|
39
35
|
from .geometry_types import get_geom_type
|
|
40
36
|
from .geometry_types import make_all_singlepart
|
|
41
37
|
from .geometry_types import to_single_geom_type
|
|
38
|
+
from .neighbors import get_k_nearest_neighbors
|
|
39
|
+
from .sfilter import sfilter_split
|
|
42
40
|
|
|
43
41
|
|
|
44
42
|
def split_geom_types(gdf: GeoDataFrame | GeoSeries) -> tuple[GeoDataFrame | GeoSeries]:
|
|
@@ -88,6 +86,7 @@ def get_common_crs(
|
|
|
88
86
|
# hash values for same crs. Therefore, trying to
|
|
89
87
|
actually_different = set()
|
|
90
88
|
for x in truthy_crs:
|
|
89
|
+
x = pyproj.CRS(x)
|
|
91
90
|
if x.to_string() in {j.to_string() for j in actually_different}:
|
|
92
91
|
continue
|
|
93
92
|
actually_different.add(x)
|
|
@@ -503,7 +502,42 @@ def random_points_in_polygons(gdf: GeoDataFrame, n: int, seed=None) -> GeoDataFr
|
|
|
503
502
|
)
|
|
504
503
|
|
|
505
504
|
|
|
506
|
-
def
|
|
505
|
+
def polygons_to_lines(
|
|
506
|
+
gdf: GeoDataFrame | GeoSeries, copy: bool = True
|
|
507
|
+
) -> GeoDataFrame | GeoSeries:
|
|
508
|
+
if not len(gdf):
|
|
509
|
+
return gdf
|
|
510
|
+
if not (gdf.geom_type == "Polygon").all():
|
|
511
|
+
raise ValueError("geometries must be singlepart polygons")
|
|
512
|
+
if copy:
|
|
513
|
+
gdf = gdf.copy()
|
|
514
|
+
geoms = gdf.geometry.values
|
|
515
|
+
exterior_coords, exterior_indices = shapely.get_coordinates(
|
|
516
|
+
shapely.get_exterior_ring(geoms), return_index=True
|
|
517
|
+
)
|
|
518
|
+
exteriors = shapely.linestrings(exterior_coords, indices=exterior_indices)
|
|
519
|
+
max_rings: int = np.max(shapely.get_num_interior_rings(geoms))
|
|
520
|
+
|
|
521
|
+
interiors = [
|
|
522
|
+
[LineString(shapely.get_interior_ring(geom, j)) for j in range(max_rings)]
|
|
523
|
+
for i, geom in enumerate(geoms)
|
|
524
|
+
]
|
|
525
|
+
|
|
526
|
+
lines = shapely.union_all(
|
|
527
|
+
np.array(
|
|
528
|
+
[[ext, *int_] for ext, int_ in zip(exteriors, interiors, strict=True)]
|
|
529
|
+
),
|
|
530
|
+
axis=1,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
gdf.geometry.loc[:] = lines
|
|
534
|
+
|
|
535
|
+
return gdf
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def to_lines(
|
|
539
|
+
*gdfs: GeoDataFrame, copy: bool = True, split: bool = True
|
|
540
|
+
) -> GeoDataFrame:
|
|
507
541
|
"""Makes lines out of one or more GeoDataFrames and splits them at intersections.
|
|
508
542
|
|
|
509
543
|
The GeoDataFrames' geometries are converted to LineStrings, then unioned together
|
|
@@ -513,6 +547,8 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
|
|
|
513
547
|
Args:
|
|
514
548
|
*gdfs: one or more GeoDataFrames.
|
|
515
549
|
copy: whether to take a copy of the incoming GeoDataFrames. Defaults to True.
|
|
550
|
+
split: If True (default), lines will be split at intersections if more than
|
|
551
|
+
one GeoDataFrame is passed as gdfs. Otherwise, a simple concat.
|
|
516
552
|
|
|
517
553
|
Returns:
|
|
518
554
|
A GeoDataFrame with singlepart line geometries and columns of all input
|
|
@@ -556,70 +592,295 @@ def to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
|
|
|
556
592
|
>>> lines["l"] = lines.length
|
|
557
593
|
>>> sg.qtm(lines, "l")
|
|
558
594
|
"""
|
|
559
|
-
|
|
560
|
-
|
|
595
|
+
gdf = (
|
|
596
|
+
pd.concat(df.assign(**{"_df_idx": i}) for i, df in enumerate(gdfs))
|
|
597
|
+
.pipe(make_all_singlepart, ignore_index=True)
|
|
598
|
+
.pipe(clean_geoms)
|
|
599
|
+
)
|
|
600
|
+
geom_col = gdf.geometry.name
|
|
561
601
|
|
|
562
|
-
if
|
|
563
|
-
|
|
564
|
-
|
|
602
|
+
if not len(gdf):
|
|
603
|
+
return gdf.drop(columns="_df_idx")
|
|
604
|
+
|
|
605
|
+
geoms = gdf.geometry
|
|
606
|
+
|
|
607
|
+
if (geoms.geom_type == "Polygon").all():
|
|
608
|
+
geoms = polygons_to_lines(geoms, copy=copy)
|
|
609
|
+
elif (geoms.geom_type != "LineString").any():
|
|
610
|
+
raise ValueError("Point geometries not allowed in 'to_lines'.")
|
|
611
|
+
|
|
612
|
+
gdf.geometry.loc[:] = geoms
|
|
613
|
+
|
|
614
|
+
if not split:
|
|
615
|
+
return gdf
|
|
616
|
+
|
|
617
|
+
out = []
|
|
618
|
+
for i in gdf["_df_idx"].unique():
|
|
619
|
+
these = gdf[gdf["_df_idx"] == i]
|
|
620
|
+
others = gdf.loc[gdf["_df_idx"] != i, [geom_col]]
|
|
621
|
+
intersection_points = these.overlay(others, keep_geom_type=False).explode(
|
|
622
|
+
ignore_index=True
|
|
565
623
|
)
|
|
624
|
+
points = intersection_points[intersection_points.geom_type == "Point"]
|
|
625
|
+
lines = intersection_points[intersection_points.geom_type == "LineString"]
|
|
626
|
+
splitted = _split_lines_by_points_along_line(these, points, splitted_col=None)
|
|
627
|
+
out.append(splitted)
|
|
628
|
+
out.append(lines)
|
|
566
629
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
630
|
+
return (
|
|
631
|
+
pd.concat(out, ignore_index=True)
|
|
632
|
+
.pipe(make_all_singlepart, ignore_index=True)
|
|
633
|
+
.drop(columns="_df_idx")
|
|
634
|
+
)
|
|
572
635
|
|
|
573
|
-
singlepart = get_parts(geom)
|
|
574
|
-
lines = []
|
|
575
|
-
for part in singlepart:
|
|
576
|
-
exterior_ring = get_exterior_ring(part)
|
|
577
|
-
lines.append(exterior_ring)
|
|
578
636
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
637
|
+
def _split_lines_by_points_along_line(lines, points, splitted_col: str | None = None):
|
|
638
|
+
precision = 1e-5
|
|
639
|
+
# find the lines that were snapped to (or are very close because of float rounding)
|
|
640
|
+
points_buff = points.buffer(precision, resolution=16).to_frame("geometry")
|
|
641
|
+
relevant_lines, the_other_lines = sfilter_split(lines, points_buff)
|
|
582
642
|
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
643
|
+
if not len(relevant_lines):
|
|
644
|
+
if splitted_col:
|
|
645
|
+
return lines.assign(**{splitted_col: 0})
|
|
646
|
+
return lines
|
|
586
647
|
|
|
587
|
-
|
|
648
|
+
# need consistent coordinate dimensions later
|
|
649
|
+
# (doing it down here to not overwrite the original data)
|
|
650
|
+
relevant_lines.geometry = shapely.force_2d(relevant_lines.geometry)
|
|
651
|
+
points.geometry = shapely.force_2d(points.geometry)
|
|
588
652
|
|
|
589
|
-
|
|
653
|
+
# split the lines with buffer + difference, since shaply.split usually doesn't work
|
|
654
|
+
# relevant_lines["_idx"] = range(len(relevant_lines))
|
|
655
|
+
splitted = relevant_lines.overlay(points_buff, how="difference").explode(
|
|
656
|
+
ignore_index=True
|
|
657
|
+
)
|
|
590
658
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
659
|
+
# linearrings (maybe coded as linestrings) that were not split,
|
|
660
|
+
# do not have edges and must be added in the end
|
|
661
|
+
boundaries = splitted.geometry.boundary
|
|
662
|
+
circles = splitted[boundaries.is_empty]
|
|
663
|
+
splitted = splitted[~boundaries.is_empty]
|
|
595
664
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
665
|
+
if not len(splitted):
|
|
666
|
+
return pd.concat([the_other_lines, circles], ignore_index=True)
|
|
667
|
+
|
|
668
|
+
# the endpoints of the new lines are now sligtly off. Using get_k_nearest_neighbors
|
|
669
|
+
# to get the exact snapped point coordinates, . This will map the sligtly
|
|
670
|
+
# wrong line endpoints with the point the line was split by.
|
|
671
|
+
|
|
672
|
+
points["point_coords"] = [(geom.x, geom.y) for geom in points.geometry]
|
|
673
|
+
|
|
674
|
+
# get line endpoints as columns (source_coords and target_coords)
|
|
675
|
+
splitted = make_edge_coords_cols(splitted)
|
|
676
|
+
|
|
677
|
+
splitted_source = to_gdf(splitted["source_coords"], crs=lines.crs)
|
|
678
|
+
splitted_target = to_gdf(splitted["target_coords"], crs=lines.crs)
|
|
679
|
+
|
|
680
|
+
def get_nearest(splitted: GeoDataFrame, points: GeoDataFrame) -> pd.DataFrame:
|
|
681
|
+
"""Find the nearest snapped point for each source and target of the lines."""
|
|
682
|
+
return get_k_nearest_neighbors(splitted, points, k=1).loc[
|
|
683
|
+
lambda x: x["distance"] <= precision * 2
|
|
684
|
+
]
|
|
602
685
|
|
|
603
|
-
|
|
686
|
+
# points = points.set_index("point_coords")
|
|
687
|
+
points.index = points.geometry
|
|
688
|
+
dists_source = get_nearest(splitted_source, points)
|
|
689
|
+
dists_target = get_nearest(splitted_target, points)
|
|
604
690
|
|
|
605
|
-
|
|
691
|
+
# neighbor_index: point coordinates as tuple
|
|
692
|
+
pointmapper_source: pd.Series = dists_source["neighbor_index"]
|
|
693
|
+
pointmapper_target: pd.Series = dists_target["neighbor_index"]
|
|
606
694
|
|
|
607
|
-
|
|
608
|
-
|
|
695
|
+
# now, we can replace the source/target coordinate with the coordinates of
|
|
696
|
+
# the snapped points.
|
|
609
697
|
|
|
610
|
-
|
|
611
|
-
|
|
698
|
+
splitted = _change_line_endpoint(
|
|
699
|
+
splitted,
|
|
700
|
+
indices=dists_source.index,
|
|
701
|
+
pointmapper=pointmapper_source,
|
|
702
|
+
change_what="first",
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
# same for the lines where the target was split, but change the last coordinate
|
|
706
|
+
splitted = _change_line_endpoint(
|
|
707
|
+
splitted,
|
|
708
|
+
indices=dists_target.index,
|
|
709
|
+
pointmapper=pointmapper_target,
|
|
710
|
+
change_what="last",
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
if splitted_col:
|
|
714
|
+
splitted[splitted_col] = 1
|
|
715
|
+
|
|
716
|
+
return pd.concat([the_other_lines, splitted, circles], ignore_index=True).drop(
|
|
717
|
+
["source_coords", "target_coords"], axis=1
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
def _change_line_endpoint(
|
|
722
|
+
gdf: GeoDataFrame,
|
|
723
|
+
indices: pd.Index,
|
|
724
|
+
pointmapper: pd.Series,
|
|
725
|
+
change_what: str | int,
|
|
726
|
+
) -> GeoDataFrame:
|
|
727
|
+
"""Modify the endpoints of selected lines in a GeoDataFrame based on an index mapping.
|
|
728
|
+
|
|
729
|
+
This function updates the geometry of specified line features within a GeoDataFrame,
|
|
730
|
+
changing either the first or last point of each line to new coordinates provided by a mapping.
|
|
731
|
+
It is typically used in scenarios where line endpoints need to be adjusted to new locations,
|
|
732
|
+
such as in network adjustments or data corrections.
|
|
733
|
+
|
|
734
|
+
Args:
|
|
735
|
+
gdf: A GeoDataFrame containing line geometries.
|
|
736
|
+
indices: An Index object identifying the rows in the GeoDataFrame whose endpoints will be changed.
|
|
737
|
+
pointmapper: A Series mapping from the index of lines to new point geometries.
|
|
738
|
+
change_what: Specifies which endpoint of the line to change. Accepts 'first' or 0 for the
|
|
739
|
+
starting point, and 'last' or -1 for the ending point.
|
|
740
|
+
|
|
741
|
+
Returns:
|
|
742
|
+
A GeoDataFrame with the specified line endpoints updated according to the pointmapper.
|
|
743
|
+
|
|
744
|
+
Raises:
|
|
745
|
+
ValueError: If `change_what` is not one of the accepted values ('first', 'last', 0, -1).
|
|
746
|
+
"""
|
|
747
|
+
assert gdf.index.is_unique
|
|
748
|
+
|
|
749
|
+
if change_what == "first" or change_what == 0:
|
|
750
|
+
keep = "first"
|
|
751
|
+
elif change_what == "last" or change_what == -1:
|
|
752
|
+
keep = "last"
|
|
612
753
|
else:
|
|
613
|
-
|
|
754
|
+
raise ValueError(
|
|
755
|
+
f"change_what should be 'first' or 'last' or 0 or -1. Got {change_what}"
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
is_relevant = gdf.index.isin(indices)
|
|
759
|
+
relevant_lines = gdf.loc[is_relevant]
|
|
760
|
+
|
|
761
|
+
relevant_lines.geometry = extract_unique_points(relevant_lines.geometry)
|
|
762
|
+
relevant_lines = relevant_lines.explode(index_parts=False)
|
|
763
|
+
|
|
764
|
+
relevant_lines.loc[lambda x: ~x.index.duplicated(keep=keep), "geometry"] = (
|
|
765
|
+
relevant_lines.loc[lambda x: ~x.index.duplicated(keep=keep)]
|
|
766
|
+
.index.map(pointmapper)
|
|
767
|
+
.values
|
|
768
|
+
)
|
|
769
|
+
|
|
770
|
+
is_line = relevant_lines.groupby(level=0).size() > 1
|
|
771
|
+
relevant_lines_mapped = (
|
|
772
|
+
relevant_lines.loc[is_line].groupby(level=0)["geometry"].agg(LineString)
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
gdf.loc[relevant_lines_mapped.index, "geometry"] = relevant_lines_mapped
|
|
776
|
+
|
|
777
|
+
return gdf
|
|
778
|
+
|
|
614
779
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
if len(line_gdf):
|
|
618
|
-
unioned = unioned.overlay(line_gdf, how="union", keep_geom_type=True)
|
|
619
|
-
else:
|
|
620
|
-
unioned = pd.concat([unioned, line_gdf], ignore_index=True)
|
|
780
|
+
def make_edge_coords_cols(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
781
|
+
"""Get the wkt of the first and last points of lines as columns.
|
|
621
782
|
|
|
622
|
-
|
|
783
|
+
It takes a GeoDataFrame of LineStrings and returns a GeoDataFrame with two new
|
|
784
|
+
columns, source_coords and target_coords, which are the x and y coordinates of the
|
|
785
|
+
first and last points of the LineStrings in a tuple. The lines all have to be
|
|
786
|
+
|
|
787
|
+
Args:
|
|
788
|
+
gdf (GeoDataFrame): the GeoDataFrame with the lines
|
|
789
|
+
|
|
790
|
+
Returns:
|
|
791
|
+
A GeoDataFrame with new columns 'source_coords' and 'target_coords'
|
|
792
|
+
"""
|
|
793
|
+
try:
|
|
794
|
+
gdf, endpoints = _prepare_make_edge_cols_simple(gdf)
|
|
795
|
+
except ValueError:
|
|
796
|
+
gdf, endpoints = _prepare_make_edge_cols(gdf)
|
|
797
|
+
|
|
798
|
+
coords = [(geom.x, geom.y) for geom in endpoints.geometry]
|
|
799
|
+
gdf["source_coords"], gdf["target_coords"] = (
|
|
800
|
+
coords[0::2],
|
|
801
|
+
coords[1::2],
|
|
802
|
+
)
|
|
803
|
+
|
|
804
|
+
return gdf
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def make_edge_wkt_cols(gdf: GeoDataFrame) -> GeoDataFrame:
|
|
808
|
+
"""Get coordinate tuples of the first and last points of lines as columns.
|
|
809
|
+
|
|
810
|
+
It takes a GeoDataFrame of LineStrings and returns a GeoDataFrame with two new
|
|
811
|
+
columns, source_wkt and target_wkt, which are the WKT representations of the first
|
|
812
|
+
and last points of the LineStrings
|
|
813
|
+
|
|
814
|
+
Args:
|
|
815
|
+
gdf (GeoDataFrame): the GeoDataFrame with the lines
|
|
816
|
+
|
|
817
|
+
Returns:
|
|
818
|
+
A GeoDataFrame with new columns 'source_wkt' and 'target_wkt'
|
|
819
|
+
"""
|
|
820
|
+
try:
|
|
821
|
+
gdf, endpoints = _prepare_make_edge_cols_simple(gdf)
|
|
822
|
+
except ValueError:
|
|
823
|
+
gdf, endpoints = _prepare_make_edge_cols(gdf)
|
|
824
|
+
|
|
825
|
+
wkt_geom = [
|
|
826
|
+
f"POINT ({x} {y})" for x, y in zip(endpoints.x, endpoints.y, strict=True)
|
|
827
|
+
]
|
|
828
|
+
gdf["source_wkt"], gdf["target_wkt"] = (
|
|
829
|
+
wkt_geom[0::2],
|
|
830
|
+
wkt_geom[1::2],
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
return gdf
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def _prepare_make_edge_cols(lines: GeoDataFrame) -> tuple[GeoDataFrame, GeoDataFrame]:
|
|
837
|
+
|
|
838
|
+
lines = lines.loc[lines.geom_type != "LinearRing"]
|
|
839
|
+
|
|
840
|
+
if not (lines.geom_type == "LineString").all():
|
|
841
|
+
multilinestring_error_message = (
|
|
842
|
+
"MultiLineStrings have more than two endpoints. "
|
|
843
|
+
"Try shapely.line_merge and/or explode() to get LineStrings. "
|
|
844
|
+
"Or use the Network class methods, where the lines are prepared correctly."
|
|
845
|
+
)
|
|
846
|
+
if (lines.geom_type == "MultiLinestring").any():
|
|
847
|
+
raise ValueError(multilinestring_error_message)
|
|
848
|
+
else:
|
|
849
|
+
raise ValueError(
|
|
850
|
+
"You have mixed geometries. Only lines are accepted. "
|
|
851
|
+
"Try using: to_single_geom_type(gdf, 'lines')."
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
geom_col = lines._geometry_column_name
|
|
855
|
+
|
|
856
|
+
# some LineStrings are in fact rings and must be removed manually
|
|
857
|
+
lines, _ = split_out_circles(lines)
|
|
858
|
+
|
|
859
|
+
endpoints = lines[geom_col].boundary.explode(ignore_index=True)
|
|
860
|
+
|
|
861
|
+
if len(lines) and len(endpoints) / len(lines) != 2:
|
|
862
|
+
raise ValueError(
|
|
863
|
+
"The lines should have only two endpoints each. "
|
|
864
|
+
"Try splitting multilinestrings with explode.",
|
|
865
|
+
lines[geom_col],
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
return lines, endpoints
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
def _prepare_make_edge_cols_simple(
|
|
872
|
+
lines: GeoDataFrame,
|
|
873
|
+
) -> tuple[GeoDataFrame, GeoDataFrame]:
|
|
874
|
+
"""Faster version of _prepare_make_edge_cols."""
|
|
875
|
+
endpoints = lines[lines._geometry_column_name].boundary.explode(ignore_index=True)
|
|
876
|
+
|
|
877
|
+
if len(lines) and len(endpoints) / len(lines) != 2:
|
|
878
|
+
raise ValueError(
|
|
879
|
+
"The lines should have only two endpoints each. "
|
|
880
|
+
"Try splitting multilinestrings with explode."
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
return lines, endpoints
|
|
623
884
|
|
|
624
885
|
|
|
625
886
|
def clean_clip(
|
|
@@ -676,6 +937,14 @@ def clean_clip(
|
|
|
676
937
|
return gdf
|
|
677
938
|
|
|
678
939
|
|
|
940
|
+
def split_out_circles(
|
|
941
|
+
lines: GeoDataFrame | GeoSeries,
|
|
942
|
+
) -> tuple[GeoDataFrame | GeoSeries, GeoDataFrame | GeoSeries]:
|
|
943
|
+
boundaries = lines.geometry.boundary
|
|
944
|
+
is_circle = (~boundaries.is_empty).values
|
|
945
|
+
return lines.iloc[is_circle], lines.iloc[~is_circle]
|
|
946
|
+
|
|
947
|
+
|
|
679
948
|
def extend_lines(arr1, arr2, distance) -> NDArray[LineString]:
|
|
680
949
|
if len(arr1) != len(arr2):
|
|
681
950
|
raise ValueError
|
|
@@ -871,9 +1140,6 @@ def _grouped_unary_union(
|
|
|
871
1140
|
|
|
872
1141
|
Experimental. Messy code.
|
|
873
1142
|
"""
|
|
874
|
-
df = df.copy()
|
|
875
|
-
df_orig = df.copy()
|
|
876
|
-
|
|
877
1143
|
try:
|
|
878
1144
|
geom_col = df._geometry_column_name
|
|
879
1145
|
except AttributeError:
|
|
@@ -884,60 +1150,18 @@ def _grouped_unary_union(
|
|
|
884
1150
|
except AttributeError:
|
|
885
1151
|
geom_col = "geometry"
|
|
886
1152
|
|
|
887
|
-
if not len(df):
|
|
888
|
-
return GeoSeries(name=geom_col)
|
|
889
|
-
|
|
890
1153
|
if isinstance(df, pd.Series):
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
if isinstance(by, str):
|
|
897
|
-
by = [by]
|
|
898
|
-
elif by is None and level is None:
|
|
899
|
-
raise TypeError("You have to supply one of 'by' and 'level'")
|
|
900
|
-
elif by is None:
|
|
901
|
-
by = df.index.get_level_values(level)
|
|
902
|
-
|
|
903
|
-
cumcount = df.groupby(by, dropna=dropna).cumcount().values
|
|
904
|
-
|
|
905
|
-
def get_col_or_index(df, col: str) -> pd.Series | pd.Index:
|
|
906
|
-
try:
|
|
907
|
-
return df[col]
|
|
908
|
-
except KeyError:
|
|
909
|
-
for i, name in enumerate(df.index.names):
|
|
910
|
-
if name == col:
|
|
911
|
-
return df.index.get_level_values(i)
|
|
912
|
-
raise KeyError(col)
|
|
913
|
-
|
|
914
|
-
try:
|
|
915
|
-
df.index = pd.MultiIndex.from_arrays(
|
|
916
|
-
[cumcount, *[get_col_or_index(df, col) for col in by]]
|
|
1154
|
+
return GeoSeries(
|
|
1155
|
+
df.groupby(level=level, as_index=as_index, **kwargs).agg(
|
|
1156
|
+
lambda x: _unary_union_for_notna(x, grid_size=grid_size)
|
|
1157
|
+
)
|
|
917
1158
|
)
|
|
918
|
-
except KeyError:
|
|
919
|
-
df.index = pd.MultiIndex.from_arrays([cumcount, by])
|
|
920
1159
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
raise e.__class__(e, f"by={by}", df_orig[bb], df[geom_col]) from e
|
|
927
|
-
geometries_2d: NDArray[Polygon | None] = geoms_wide.values
|
|
928
|
-
try:
|
|
929
|
-
geometries_2d = make_valid(geometries_2d)
|
|
930
|
-
except TypeError:
|
|
931
|
-
# make_valid doesn't like nan, so converting to None
|
|
932
|
-
# np.isnan doesn't accept geometry type, so using isinstance
|
|
933
|
-
np_isinstance = np.vectorize(isinstance)
|
|
934
|
-
geometries_2d[np_isinstance(geometries_2d, Geometry) == False] = None
|
|
935
|
-
|
|
936
|
-
unioned = make_valid(union_all(geometries_2d, axis=1, **kwargs))
|
|
937
|
-
|
|
938
|
-
geoms = GeoSeries(unioned, name=geom_col, index=geoms_wide.index)
|
|
939
|
-
|
|
940
|
-
return geoms if as_index else geoms.reset_index()
|
|
1160
|
+
return GeoSeries(
|
|
1161
|
+
df.groupby(by, level=level, as_index=as_index, **kwargs)[geom_col].agg(
|
|
1162
|
+
lambda x: _unary_union_for_notna(x, grid_size=grid_size)
|
|
1163
|
+
)
|
|
1164
|
+
)
|
|
941
1165
|
|
|
942
1166
|
|
|
943
1167
|
def _parallel_unary_union(
|
|
@@ -25,14 +25,12 @@ def make_all_singlepart(
|
|
|
25
25
|
A GeoDataFrame of singlepart geometries.
|
|
26
26
|
"""
|
|
27
27
|
# only explode if nessecary
|
|
28
|
-
if
|
|
29
|
-
index_parts or ignore_index
|
|
30
|
-
): # and not gdf.index.equals(pd.Index(range(len(gdf)))):
|
|
28
|
+
if index_parts or ignore_index:
|
|
31
29
|
gdf = gdf.explode(index_parts=index_parts, ignore_index=ignore_index)
|
|
32
30
|
|
|
33
|
-
while not
|
|
34
|
-
["Polygon", "Point", "LineString", "LinearRing"]
|
|
35
|
-
)
|
|
31
|
+
while not np.all(
|
|
32
|
+
np.isin(_get_geom_types(gdf), ["Polygon", "Point", "LineString", "LinearRing"])
|
|
33
|
+
):
|
|
36
34
|
gdf = gdf.explode(index_parts=index_parts, ignore_index=ignore_index)
|
|
37
35
|
|
|
38
36
|
return gdf
|
|
@@ -114,22 +112,24 @@ def to_single_geom_type(
|
|
|
114
112
|
raise TypeError(f"'gdf' should be GeoDataFrame or GeoSeries, got {type(gdf)}")
|
|
115
113
|
|
|
116
114
|
# explode collections to single-typed geometries
|
|
117
|
-
collections = gdf
|
|
115
|
+
collections = _get_geom_types(gdf) == "GeometryCollection"
|
|
118
116
|
if collections.any():
|
|
119
117
|
collections = make_all_singlepart(gdf[collections], ignore_index=ignore_index)
|
|
120
118
|
|
|
121
119
|
gdf = pd.concat([gdf, collections], ignore_index=ignore_index)
|
|
122
120
|
|
|
123
121
|
if "poly" in geom_type:
|
|
124
|
-
is_polygon =
|
|
122
|
+
is_polygon = np.isin(_get_geom_types(gdf), ["Polygon", "MultiPolygon"])
|
|
125
123
|
if not is_polygon.all():
|
|
126
124
|
gdf = gdf.loc[is_polygon]
|
|
127
125
|
elif "line" in geom_type:
|
|
128
|
-
is_line =
|
|
126
|
+
is_line = np.isin(
|
|
127
|
+
_get_geom_types(gdf), ["LineString", "MultiLineString", "LinearRing"]
|
|
128
|
+
)
|
|
129
129
|
if not is_line.all():
|
|
130
130
|
gdf = gdf.loc[is_line]
|
|
131
131
|
else:
|
|
132
|
-
is_point =
|
|
132
|
+
is_point = np.isin(_get_geom_types(gdf), ["Point", "MultiPoint"])
|
|
133
133
|
if not is_point.all():
|
|
134
134
|
gdf = gdf.loc[is_point]
|
|
135
135
|
|
|
@@ -168,22 +168,20 @@ def get_geom_type(gdf: GeoDataFrame | GeoSeries) -> str:
|
|
|
168
168
|
polys = ["Polygon", "MultiPolygon", None]
|
|
169
169
|
lines = ["LineString", "MultiLineString", "LinearRing", None]
|
|
170
170
|
points = ["Point", "MultiPoint", None]
|
|
171
|
-
if
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (gdf.geom_type.isin(polys)).all():
|
|
171
|
+
if isinstance(gdf, Geometry):
|
|
172
|
+
if gdf.geom_type in polys:
|
|
173
|
+
return "polygon"
|
|
174
|
+
if gdf.geom_type in lines:
|
|
175
|
+
return "line"
|
|
176
|
+
if gdf.geom_type in points:
|
|
177
|
+
return "point"
|
|
178
|
+
return "mixed"
|
|
179
|
+
|
|
180
|
+
if np.all(np.isin(_get_geom_types(gdf), polys)):
|
|
183
181
|
return "polygon"
|
|
184
|
-
if (
|
|
182
|
+
if np.all(np.isin(_get_geom_types(gdf), lines)):
|
|
185
183
|
return "line"
|
|
186
|
-
if (
|
|
184
|
+
if np.all(np.isin(_get_geom_types(gdf), points)):
|
|
187
185
|
return "point"
|
|
188
186
|
return "mixed"
|
|
189
187
|
|
|
@@ -213,14 +211,21 @@ def is_single_geom_type(gdf: GeoDataFrame | GeoSeries) -> bool:
|
|
|
213
211
|
>>> is_single_geom_type(gdf)
|
|
214
212
|
True
|
|
215
213
|
"""
|
|
216
|
-
|
|
217
|
-
|
|
214
|
+
return (
|
|
215
|
+
np.all(np.isin(_get_geom_types(gdf), ["Polygon", "MultiPolygon"]))
|
|
216
|
+
or np.all(
|
|
217
|
+
np.isin(
|
|
218
|
+
_get_geom_types(gdf), ["LineString", "MultiLineString", "LinearRing"]
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
or np.all(np.isin(_get_geom_types(gdf), ["Point", "MultiPoint"]))
|
|
222
|
+
)
|
|
218
223
|
|
|
219
|
-
if all(gdf.geom_type.isin(["Polygon", "MultiPolygon"])):
|
|
220
|
-
return True
|
|
221
|
-
if all(gdf.geom_type.isin(["LineString", "MultiLineString", "LinearRing"])):
|
|
222
|
-
return True
|
|
223
|
-
if all(gdf.geom_type.isin(["Point", "MultiPoint"])):
|
|
224
|
-
return True
|
|
225
224
|
|
|
226
|
-
|
|
225
|
+
def _get_geom_types(
|
|
226
|
+
gdf: GeoDataFrame | GeoSeries | GeometryArray | np.ndarray,
|
|
227
|
+
) -> np.ndarray:
|
|
228
|
+
try:
|
|
229
|
+
return GeometryArray(gdf.geometry.values).geom_type
|
|
230
|
+
except AttributeError:
|
|
231
|
+
return getattr(GeometryArray(gdf, "geom_type"))
|
sgis/geopandas_tools/overlay.py
CHANGED
|
@@ -11,7 +11,6 @@ version of the solution from GH 2792.
|
|
|
11
11
|
import functools
|
|
12
12
|
from collections.abc import Callable
|
|
13
13
|
|
|
14
|
-
import dask.array as da
|
|
15
14
|
import geopandas as gpd
|
|
16
15
|
import joblib
|
|
17
16
|
import numpy as np
|
|
@@ -28,6 +27,11 @@ from shapely import make_valid
|
|
|
28
27
|
from shapely import unary_union
|
|
29
28
|
from shapely.errors import GEOSException
|
|
30
29
|
|
|
30
|
+
try:
|
|
31
|
+
import dask.array as da
|
|
32
|
+
except ImportError:
|
|
33
|
+
pass
|
|
34
|
+
|
|
31
35
|
from .general import _determine_geom_type_args
|
|
32
36
|
from .general import clean_geoms
|
|
33
37
|
from .geometry_types import get_geom_type
|