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