ssb-sgis 1.0.5__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.
@@ -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 to_lines(*gdfs: GeoDataFrame, copy: bool = True) -> GeoDataFrame:
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
- if not all(isinstance(gdf, (GeoSeries, GeoDataFrame)) for gdf in gdfs):
560
- raise TypeError("gdf must be GeoDataFrame or GeoSeries")
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 any(gdf.geom_type.isin(["Point", "MultiPoint"]).any() for gdf in gdfs):
563
- raise ValueError(
564
- f"Cannot convert points to lines. {[gdf.geom_type.value_counts() for gdf in gdfs]}"
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
- def _shapely_geometry_to_lines(geom):
568
- """Get all lines from the exterior and interiors of a Polygon."""
569
- # if lines (points are not allowed in this function)
570
- if geom.area == 0:
571
- return geom
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
- n_interior_rings = get_num_interior_rings(part)
580
- if not (n_interior_rings):
581
- continue
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
- interior_rings = [
584
- LineString(get_interior_ring(part, n)) for n in range(n_interior_rings)
585
- ]
643
+ if not len(relevant_lines):
644
+ if splitted_col:
645
+ return lines.assign(**{splitted_col: 0})
646
+ return lines
586
647
 
587
- lines += interior_rings
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
- return union_all(lines)
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
- lines = []
592
- for gdf in gdfs:
593
- if copy:
594
- gdf = gdf.copy()
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
- mapped = gdf.geometry.map(_shapely_geometry_to_lines)
597
- try:
598
- gdf.geometry = mapped
599
- except AttributeError:
600
- # geoseries
601
- gdf.loc[:] = mapped
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
- gdf = to_single_geom_type(gdf, "line")
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
- lines.append(gdf)
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
- if len(lines) == 1:
608
- return lines[0]
695
+ # now, we can replace the source/target coordinate with the coordinates of
696
+ # the snapped points.
609
697
 
610
- if len(lines[0]) and len(lines[1]):
611
- unioned = lines[0].overlay(lines[1], how="union", keep_geom_type=True)
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
- unioned = pd.concat([lines[0], lines[1]], ignore_index=True)
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
- if len(lines) > 2:
616
- for line_gdf in lines[2:]:
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
- return make_all_singlepart(unioned, ignore_index=True)
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
- df.name = geom_col
892
- original_index = df.index
893
- df = df.reset_index()
894
- df.index = original_index
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
- # to wide format: each row will be one group to be merged to one geometry
922
- try:
923
- geoms_wide: pd.DataFrame = df[geom_col].unstack(level=0)
924
- except Exception as e:
925
- bb = [*by, geom_col]
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 gdf.geom_type.isin(
34
- ["Polygon", "Point", "LineString", "LinearRing"]
35
- ).all():
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.geom_type == "GeometryCollection"
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 = gdf.geom_type.isin(["Polygon", "MultiPolygon"])
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 = gdf.geom_type.isin(["LineString", "MultiLineString", "LinearRing"])
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 = gdf.geom_type.isin(["Point", "MultiPoint"])
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 not isinstance(gdf, (GeoDataFrame, GeoSeries)):
172
- if isinstance(gdf, Geometry):
173
- if gdf.geom_type in polys:
174
- return "polygon"
175
- if gdf.geom_type in lines:
176
- return "line"
177
- if gdf.geom_type in points:
178
- return "point"
179
- return "mixed"
180
- raise TypeError(f"'gdf' should be GeoDataFrame or GeoSeries, got {type(gdf)}")
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 (gdf.geom_type.isin(lines)).all():
182
+ if np.all(np.isin(_get_geom_types(gdf), lines)):
185
183
  return "line"
186
- if (gdf.geom_type.isin(points)).all():
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
- if not isinstance(gdf, (GeoDataFrame, GeoSeries)):
217
- raise TypeError(f"'gdf' should be GeoDataFrame or GeoSeries, got {type(gdf)}")
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
- return False
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"))