geoai-py 0.2.3__py2.py3-none-any.whl → 0.3.0__py2.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.
@@ -588,6 +588,8 @@ def view_vector(
588
588
 
589
589
  def view_vector_interactive(
590
590
  vector_data,
591
+ layer_name="Vector Layer",
592
+ tiles_args=None,
591
593
  **kwargs,
592
594
  ):
593
595
  """
@@ -599,6 +601,9 @@ def view_vector_interactive(
599
601
 
600
602
  Args:
601
603
  vector_data (geopandas.GeoDataFrame): The vector dataset to visualize.
604
+ layer_name (str, optional): The name of the layer. Defaults to "Vector Layer".
605
+ tiles_args (dict, optional): Additional arguments for the localtileserver client.
606
+ get_folium_tile_layer function. Defaults to None.
602
607
  **kwargs: Additional keyword arguments to pass to GeoDataFrame.explore() function.
603
608
  See https://geopandas.org/en/stable/docs/reference/api/geopandas.GeoDataFrame.explore.html
604
609
 
@@ -613,6 +618,59 @@ def view_vector_interactive(
613
618
  >>> roads = gpd.read_file("roads.shp")
614
619
  >>> view_vector_interactive(roads, figsize=(12, 8))
615
620
  """
621
+ import folium
622
+ import folium.plugins as plugins
623
+ from localtileserver import get_folium_tile_layer, TileClient
624
+ from leafmap import cog_tile
625
+
626
+ google_tiles = {
627
+ "Roadmap": {
628
+ "url": "https://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}",
629
+ "attribution": "Google",
630
+ "name": "Google Maps",
631
+ },
632
+ "Satellite": {
633
+ "url": "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}",
634
+ "attribution": "Google",
635
+ "name": "Google Satellite",
636
+ },
637
+ "Terrain": {
638
+ "url": "https://mt1.google.com/vt/lyrs=p&x={x}&y={y}&z={z}",
639
+ "attribution": "Google",
640
+ "name": "Google Terrain",
641
+ },
642
+ "Hybrid": {
643
+ "url": "https://mt1.google.com/vt/lyrs=y&x={x}&y={y}&z={z}",
644
+ "attribution": "Google",
645
+ "name": "Google Hybrid",
646
+ },
647
+ }
648
+
649
+ basemap_layer_name = None
650
+ raster_layer = None
651
+
652
+ if "tiles" in kwargs and isinstance(kwargs["tiles"], str):
653
+ if kwargs["tiles"].title() in google_tiles:
654
+ basemap_layer_name = google_tiles[kwargs["tiles"].title()]["name"]
655
+ kwargs["tiles"] = google_tiles[kwargs["tiles"].title()]["url"]
656
+ kwargs["attr"] = "Google"
657
+ elif kwargs["tiles"].lower().endswith(".tif"):
658
+ if tiles_args is None:
659
+ tiles_args = {}
660
+ if kwargs["tiles"].lower().startswith("http"):
661
+ basemap_layer_name = "Remote Raster"
662
+ kwargs["tiles"] = cog_tile(kwargs["tiles"], **tiles_args)
663
+ kwargs["attr"] = "TiTiler"
664
+ else:
665
+ basemap_layer_name = "Local Raster"
666
+ client = TileClient(kwargs["tiles"])
667
+ raster_layer = get_folium_tile_layer(client, **tiles_args)
668
+ kwargs["tiles"] = raster_layer.tiles
669
+ kwargs["attr"] = "localtileserver"
670
+
671
+ if "max_zoom" not in kwargs:
672
+ kwargs["max_zoom"] = 30
673
+
616
674
  if isinstance(vector_data, str):
617
675
  vector_data = gpd.read_file(vector_data)
618
676
 
@@ -620,4 +678,364 @@ def view_vector_interactive(
620
678
  if not isinstance(vector_data, gpd.GeoDataFrame):
621
679
  raise TypeError("Input data must be a GeoDataFrame")
622
680
 
623
- return vector_data.explore(**kwargs)
681
+ layer_control = kwargs.pop("layer_control", True)
682
+ fullscreen_control = kwargs.pop("fullscreen_control", True)
683
+
684
+ m = vector_data.explore(**kwargs)
685
+
686
+ # Change the layer name
687
+ for layer in m._children.values():
688
+ if isinstance(layer, folium.GeoJson):
689
+ layer.layer_name = layer_name
690
+ if isinstance(layer, folium.TileLayer) and basemap_layer_name:
691
+ layer.layer_name = basemap_layer_name
692
+
693
+ if layer_control:
694
+ m.add_child(folium.LayerControl())
695
+
696
+ if fullscreen_control:
697
+ plugins.Fullscreen().add_to(m)
698
+
699
+ return m
700
+
701
+
702
+ def regularization(
703
+ building_polygons,
704
+ angle_tolerance=10,
705
+ simplify_tolerance=0.5,
706
+ orthogonalize=True,
707
+ preserve_topology=True,
708
+ ):
709
+ """
710
+ Regularizes building footprint polygons with multiple techniques beyond minimum
711
+ rotated rectangles.
712
+
713
+ Args:
714
+ building_polygons: GeoDataFrame or list of shapely Polygons containing building footprints
715
+ angle_tolerance: Degrees within which angles will be regularized to 90/180 degrees
716
+ simplify_tolerance: Distance tolerance for Douglas-Peucker simplification
717
+ orthogonalize: Whether to enforce orthogonal angles in the final polygons
718
+ preserve_topology: Whether to preserve topology during simplification
719
+
720
+ Returns:
721
+ GeoDataFrame or list of shapely Polygons with regularized building footprints
722
+ """
723
+ from shapely.geometry import Polygon, shape
724
+ from shapely.affinity import rotate, translate
725
+ from shapely import wkt
726
+
727
+ regularized_buildings = []
728
+
729
+ # Check if we're dealing with a GeoDataFrame
730
+ if isinstance(building_polygons, gpd.GeoDataFrame):
731
+ geom_objects = building_polygons.geometry
732
+ else:
733
+ geom_objects = building_polygons
734
+
735
+ for building in geom_objects:
736
+ # Handle potential string representations of geometries
737
+ if isinstance(building, str):
738
+ try:
739
+ # Try to parse as WKT
740
+ building = wkt.loads(building)
741
+ except Exception:
742
+ print(f"Failed to parse geometry string: {building[:30]}...")
743
+ continue
744
+
745
+ # Ensure we have a valid geometry
746
+ if not hasattr(building, "simplify"):
747
+ print(f"Invalid geometry type: {type(building)}")
748
+ continue
749
+
750
+ # Step 1: Simplify to remove noise and small vertices
751
+ simplified = building.simplify(
752
+ simplify_tolerance, preserve_topology=preserve_topology
753
+ )
754
+
755
+ if orthogonalize:
756
+ # Make sure we have a valid polygon with an exterior
757
+ if not hasattr(simplified, "exterior") or simplified.exterior is None:
758
+ print(f"Simplified geometry has no exterior: {simplified}")
759
+ regularized_buildings.append(building) # Use original instead
760
+ continue
761
+
762
+ # Step 2: Get the dominant angle to rotate building
763
+ coords = np.array(simplified.exterior.coords)
764
+
765
+ # Make sure we have enough coordinates for angle calculation
766
+ if len(coords) < 3:
767
+ print(f"Not enough coordinates for angle calculation: {len(coords)}")
768
+ regularized_buildings.append(building) # Use original instead
769
+ continue
770
+
771
+ segments = np.diff(coords, axis=0)
772
+ angles = np.arctan2(segments[:, 1], segments[:, 0]) * 180 / np.pi
773
+
774
+ # Find most common angle classes (0, 90, 180, 270 degrees)
775
+ binned_angles = np.round(angles / 90) * 90
776
+ dominant_angle = np.bincount(binned_angles.astype(int) % 180).argmax()
777
+
778
+ # Step 3: Rotate to align with axes, regularize, then rotate back
779
+ rotated = rotate(simplified, -dominant_angle, origin="centroid")
780
+
781
+ # Step 4: Rectify coordinates to enforce right angles
782
+ ext_coords = np.array(rotated.exterior.coords)
783
+ rect_coords = []
784
+
785
+ # Regularize each vertex to create orthogonal corners
786
+ for i in range(len(ext_coords) - 1):
787
+ rect_coords.append(ext_coords[i])
788
+
789
+ # Check if we need to add a right-angle vertex
790
+ angle = (
791
+ np.arctan2(
792
+ ext_coords[(i + 1) % (len(ext_coords) - 1), 1]
793
+ - ext_coords[i, 1],
794
+ ext_coords[(i + 1) % (len(ext_coords) - 1), 0]
795
+ - ext_coords[i, 0],
796
+ )
797
+ * 180
798
+ / np.pi
799
+ )
800
+
801
+ if abs(angle % 90) > angle_tolerance and abs(angle % 90) < (
802
+ 90 - angle_tolerance
803
+ ):
804
+ # Add intermediate point to create right angle
805
+ rect_coords.append(
806
+ [
807
+ ext_coords[(i + 1) % (len(ext_coords) - 1), 0],
808
+ ext_coords[i, 1],
809
+ ]
810
+ )
811
+
812
+ # Close the polygon by adding the first point again
813
+ rect_coords.append(rect_coords[0])
814
+
815
+ # Create regularized polygon and rotate back
816
+ regularized = Polygon(rect_coords)
817
+ final_building = rotate(regularized, dominant_angle, origin="centroid")
818
+ else:
819
+ final_building = simplified
820
+
821
+ regularized_buildings.append(final_building)
822
+
823
+ # If input was a GeoDataFrame, return a GeoDataFrame
824
+ if isinstance(building_polygons, gpd.GeoDataFrame):
825
+ return gpd.GeoDataFrame(
826
+ geometry=regularized_buildings, crs=building_polygons.crs
827
+ )
828
+ else:
829
+ return regularized_buildings
830
+
831
+
832
+ def hybrid_regularization(building_polygons):
833
+ """
834
+ A comprehensive hybrid approach to building footprint regularization.
835
+
836
+ Applies different strategies based on building characteristics.
837
+
838
+ Args:
839
+ building_polygons: GeoDataFrame or list of shapely Polygons containing building footprints
840
+
841
+ Returns:
842
+ GeoDataFrame or list of shapely Polygons with regularized building footprints
843
+ """
844
+ from shapely.geometry import Polygon
845
+ from shapely.affinity import rotate
846
+
847
+ # Use minimum_rotated_rectangle instead of oriented_envelope
848
+ try:
849
+ from shapely.minimum_rotated_rectangle import minimum_rotated_rectangle
850
+ except ImportError:
851
+ # For older Shapely versions
852
+ def minimum_rotated_rectangle(geom):
853
+ """Calculate the minimum rotated rectangle for a geometry"""
854
+ # For older Shapely versions, implement a simple version
855
+ return geom.minimum_rotated_rectangle
856
+
857
+ # Determine input type for correct return
858
+ is_gdf = isinstance(building_polygons, gpd.GeoDataFrame)
859
+
860
+ # Extract geometries if GeoDataFrame
861
+ if is_gdf:
862
+ geom_objects = building_polygons.geometry
863
+ else:
864
+ geom_objects = building_polygons
865
+
866
+ results = []
867
+
868
+ for building in geom_objects:
869
+ # 1. Analyze building characteristics
870
+ if not hasattr(building, "exterior") or building.is_empty:
871
+ results.append(building)
872
+ continue
873
+
874
+ # Calculate shape complexity metrics
875
+ complexity = building.length / (4 * np.sqrt(building.area))
876
+
877
+ # Calculate dominant angle
878
+ coords = np.array(building.exterior.coords)[:-1]
879
+ segments = np.diff(np.vstack([coords, coords[0]]), axis=0)
880
+ segment_lengths = np.sqrt(segments[:, 0] ** 2 + segments[:, 1] ** 2)
881
+ segment_angles = np.arctan2(segments[:, 1], segments[:, 0]) * 180 / np.pi
882
+
883
+ # Weight angles by segment length
884
+ hist, bins = np.histogram(
885
+ segment_angles % 180, bins=36, range=(0, 180), weights=segment_lengths
886
+ )
887
+ bin_centers = (bins[:-1] + bins[1:]) / 2
888
+ dominant_angle = bin_centers[np.argmax(hist)]
889
+
890
+ # Check if building is close to orthogonal
891
+ is_orthogonal = min(dominant_angle % 45, 45 - (dominant_angle % 45)) < 5
892
+
893
+ # 2. Apply appropriate regularization strategy
894
+ if complexity > 1.5:
895
+ # Complex buildings: use minimum rotated rectangle
896
+ result = minimum_rotated_rectangle(building)
897
+ elif is_orthogonal:
898
+ # Near-orthogonal buildings: orthogonalize in place
899
+ rotated = rotate(building, -dominant_angle, origin="centroid")
900
+
901
+ # Create orthogonal hull in rotated space
902
+ bounds = rotated.bounds
903
+ ortho_hull = Polygon(
904
+ [
905
+ (bounds[0], bounds[1]),
906
+ (bounds[2], bounds[1]),
907
+ (bounds[2], bounds[3]),
908
+ (bounds[0], bounds[3]),
909
+ ]
910
+ )
911
+
912
+ result = rotate(ortho_hull, dominant_angle, origin="centroid")
913
+ else:
914
+ # Diagonal buildings: use custom approach for diagonal buildings
915
+ # Rotate to align with axes
916
+ rotated = rotate(building, -dominant_angle, origin="centroid")
917
+
918
+ # Simplify in rotated space
919
+ simplified = rotated.simplify(0.3, preserve_topology=True)
920
+
921
+ # Get the bounds in rotated space
922
+ bounds = simplified.bounds
923
+ min_x, min_y, max_x, max_y = bounds
924
+
925
+ # Create a rectangular hull in rotated space
926
+ rect_poly = Polygon(
927
+ [(min_x, min_y), (max_x, min_y), (max_x, max_y), (min_x, max_y)]
928
+ )
929
+
930
+ # Rotate back to original orientation
931
+ result = rotate(rect_poly, dominant_angle, origin="centroid")
932
+
933
+ results.append(result)
934
+
935
+ # Return in same format as input
936
+ if is_gdf:
937
+ return gpd.GeoDataFrame(geometry=results, crs=building_polygons.crs)
938
+ else:
939
+ return results
940
+
941
+
942
+ def adaptive_regularization(
943
+ building_polygons, simplify_tolerance=0.5, area_threshold=0.9, preserve_shape=True
944
+ ):
945
+ """
946
+ Adaptively regularizes building footprints based on their characteristics.
947
+
948
+ This approach determines the best regularization method for each building.
949
+
950
+ Args:
951
+ building_polygons: GeoDataFrame or list of shapely Polygons
952
+ simplify_tolerance: Distance tolerance for simplification
953
+ area_threshold: Minimum acceptable area ratio
954
+ preserve_shape: Whether to preserve overall shape for complex buildings
955
+
956
+ Returns:
957
+ GeoDataFrame or list of shapely Polygons with regularized building footprints
958
+ """
959
+ from shapely.geometry import Polygon
960
+ from shapely.affinity import rotate
961
+
962
+ # Analyze the overall dataset to set appropriate parameters
963
+ if is_gdf := isinstance(building_polygons, gpd.GeoDataFrame):
964
+ geom_objects = building_polygons.geometry
965
+ else:
966
+ geom_objects = building_polygons
967
+
968
+ results = []
969
+
970
+ for building in geom_objects:
971
+ # Skip invalid geometries
972
+ if not hasattr(building, "exterior") or building.is_empty:
973
+ results.append(building)
974
+ continue
975
+
976
+ # Measure building complexity
977
+ complexity = building.length / (4 * np.sqrt(building.area))
978
+
979
+ # Determine if the building has a clear principal direction
980
+ coords = np.array(building.exterior.coords)[:-1]
981
+ segments = np.diff(np.vstack([coords, coords[0]]), axis=0)
982
+ segment_lengths = np.sqrt(segments[:, 0] ** 2 + segments[:, 1] ** 2)
983
+ angles = np.arctan2(segments[:, 1], segments[:, 0]) * 180 / np.pi
984
+
985
+ # Normalize angles to 0-180 range and get histogram
986
+ norm_angles = angles % 180
987
+ hist, bins = np.histogram(
988
+ norm_angles, bins=18, range=(0, 180), weights=segment_lengths
989
+ )
990
+
991
+ # Calculate direction clarity (ratio of longest direction to total)
992
+ direction_clarity = np.max(hist) / np.sum(hist) if np.sum(hist) > 0 else 0
993
+
994
+ # Choose regularization method based on building characteristics
995
+ if complexity < 1.2 and direction_clarity > 0.5:
996
+ # Simple building with clear direction: use rotated rectangle
997
+ bin_max = np.argmax(hist)
998
+ bin_centers = (bins[:-1] + bins[1:]) / 2
999
+ dominant_angle = bin_centers[bin_max]
1000
+
1001
+ # Rotate to align with coordinate system
1002
+ rotated = rotate(building, -dominant_angle, origin="centroid")
1003
+
1004
+ # Create bounding box in rotated space
1005
+ bounds = rotated.bounds
1006
+ rect = Polygon(
1007
+ [
1008
+ (bounds[0], bounds[1]),
1009
+ (bounds[2], bounds[1]),
1010
+ (bounds[2], bounds[3]),
1011
+ (bounds[0], bounds[3]),
1012
+ ]
1013
+ )
1014
+
1015
+ # Rotate back
1016
+ result = rotate(rect, dominant_angle, origin="centroid")
1017
+
1018
+ # Quality check
1019
+ if (
1020
+ result.area / building.area < area_threshold
1021
+ or result.area / building.area > (1.0 / area_threshold)
1022
+ ):
1023
+ # Too much area change, use simplified original
1024
+ result = building.simplify(simplify_tolerance, preserve_topology=True)
1025
+
1026
+ else:
1027
+ # Complex building or no clear direction: preserve shape
1028
+ if preserve_shape:
1029
+ # Simplify with topology preservation
1030
+ result = building.simplify(simplify_tolerance, preserve_topology=True)
1031
+ else:
1032
+ # Fall back to convex hull for very complex shapes
1033
+ result = building.convex_hull
1034
+
1035
+ results.append(result)
1036
+
1037
+ # Return in same format as input
1038
+ if is_gdf:
1039
+ return gpd.GeoDataFrame(geometry=results, crs=building_polygons.crs)
1040
+ else:
1041
+ return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: geoai-py
3
- Version: 0.2.3
3
+ Version: 0.3.0
4
4
  Summary: A Python package for using Artificial Intelligence (AI) with geospatial data
5
5
  Author-email: Qiusheng Wu <giswqs@gmail.com>
6
6
  License: MIT License
@@ -0,0 +1,13 @@
1
+ geoai/__init__.py,sha256=rJod2PDa1AiRHE8ugVp0Bfiky7ZWBhqbh2kZ45WiggA,923
2
+ geoai/download.py,sha256=4GiDmLrp2wKslgfm507WeZrwOdYcMekgQXxWGbl5cBw,13094
3
+ geoai/extract.py,sha256=9oLbrSg_aHcimpYxfk0jLOIHQWVULRsdiAGUsPLC-qk,71708
4
+ geoai/geoai.py,sha256=wNwKIqwOT10tU4uiWTcNp5Gd598rRFMANIfJsGdOWKM,90
5
+ geoai/preprocess.py,sha256=teV-W7ykXnoru0Y_d0V9ANdO6jMyETeGbqr1_8H-Yh0,118523
6
+ geoai/segmentation.py,sha256=Vcymnhwl_xikt4v9x8CYJq_vId9R1gB7-YzLfwg-F9M,11372
7
+ geoai/utils.py,sha256=3vXFDdFqZeg4kgeNt6-Hp28RfNoQcDOH7BjrlJ6L0UE,37521
8
+ geoai_py-0.3.0.dist-info/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
9
+ geoai_py-0.3.0.dist-info/METADATA,sha256=L62RHKj0Yqno8LDYVrL50YyMfO1ybRYs2NI15WHiJMQ,5754
10
+ geoai_py-0.3.0.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
11
+ geoai_py-0.3.0.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
12
+ geoai_py-0.3.0.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
13
+ geoai_py-0.3.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
1
- geoai/__init__.py,sha256=TyhISsNnMOVPLx0wyM79k-Dgfak4s7QhfKPEIh0rBg8,923
2
- geoai/common.py,sha256=stnGB-OiTdpb9hTSaaVn5jKAGweFNM760NFKP6suiv0,21735
3
- geoai/download.py,sha256=4GiDmLrp2wKslgfm507WeZrwOdYcMekgQXxWGbl5cBw,13094
4
- geoai/extract.py,sha256=wjo5KyaUMsci3oclnOGFqwPe1VIV4vlkFcbJlPWDOzI,31572
5
- geoai/geoai.py,sha256=r9mFviDJrs7xvbK8kN3kIzZp-yswqTAleUejQvrZD4U,91
6
- geoai/preprocess.py,sha256=2O3gaGN5imIpmTdudQdTbvCtrBqdmOb0AGNEz81MW2M,109762
7
- geoai/segmentation.py,sha256=Vcymnhwl_xikt4v9x8CYJq_vId9R1gB7-YzLfwg-F9M,11372
8
- geoai_py-0.2.3.dist-info/LICENSE,sha256=vN2L5U7cZ6ZkOHFmc8WiGlsogWsZc5dllMeNxnKVOZg,1070
9
- geoai_py-0.2.3.dist-info/METADATA,sha256=T7lSXz-PE_AUSStKbnE5HUJi7-q5w04VIyiI7TZ4Xrw,5754
10
- geoai_py-0.2.3.dist-info/WHEEL,sha256=rF4EZyR2XVS6irmOHQIJx2SUqXLZKRMUrjsg8UwN-XQ,109
11
- geoai_py-0.2.3.dist-info/entry_points.txt,sha256=uGp3Az3HURIsRHP9v-ys0hIbUuBBNUfXv6VbYHIXeg4,41
12
- geoai_py-0.2.3.dist-info/top_level.txt,sha256=1YkCUWu-ii-0qIex7kbwAvfei-gos9ycyDyUCJPNWHY,6
13
- geoai_py-0.2.3.dist-info/RECORD,,