From fd996b953c575bd69d9e66362985473c34f22dba Mon Sep 17 00:00:00 2001 From: jnclelland Date: Thu, 14 Aug 2025 16:31:35 -0600 Subject: [PATCH 1/6] Update to smart_repair with some minor algorithmic changes to improve robustness, and updated Pandas syntax to remove FutureWarnings Update to smart_repair with some minor algorithmic changes to improve robustness, and updated Pandas syntax to remove FutureWarnings --- maup/intersections.py | 6 +- maup/repair.py | 145 +-- maup/smart_repair.py | 2042 ++++++++++++++--------------------------- 3 files changed, 716 insertions(+), 1477 deletions(-) diff --git a/maup/intersections.py b/maup/intersections.py index af3953e..894e745 100644 --- a/maup/intersections.py +++ b/maup/intersections.py @@ -9,7 +9,7 @@ @require_same_crs def intersections(sources, targets, output_type="geoseries", area_cutoff=None): """Computes all of the nonempty intersections between two sets of geometries. - By default, the returned :meth:`~geopandas.GeoSeries` will have a MultiIndex, where the + By default, the returned `~geopandas.GeoSeries` will have a MultiIndex, where the geometry at index *(i, j)* is the intersection of ``sources[i]`` and ``targets[j]`` (if it is not empty). If output_type == "geodataframe", the return type is a range-indexed GeoDataFrame @@ -20,8 +20,8 @@ def intersections(sources, targets, output_type="geoseries", area_cutoff=None): :param targets: geometries :type targets: :class:`~geopandas.GeoSeries` or :class:`~geopandas.GeoDataFrame` :rtype: :class:`~geopandas.GeoSeries` - :param area_cutoff: (optional) if provided, only return intersections with area - greater than ``area_cutoff`` + :param area_cutoff: (optional) if provided, only return intersections with + area greater than ``area_cutoff`` :type area_cutoff: Number or None """ diff --git a/maup/repair.py b/maup/repair.py index 187362b..74595b9 100644 --- a/maup/repair.py +++ b/maup/repair.py @@ -4,14 +4,8 @@ import pandas from geopandas import GeoSeries -from shapely.geometry import ( - Polygon, - MultiPolygon, - LineString, - MultiLineString, - GeometryCollection, -) -from shapely import union_all +from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString, GeometryCollection +from shapely.ops import unary_union from .adjacencies import adjacencies from .assign import assign_to_max @@ -63,9 +57,8 @@ def trim_valid(value): """ if isinstance(value, GeometryCollection): # List comprehension excluding non-Polygons - value = [ - item for item in value.geoms if isinstance(item, (Polygon, MultiPolygon)) - ] + value = [item for item in value.geoms + if isinstance(item, (Polygon, MultiPolygon))] # Re-aggregegating multiple Polygons into single MultiPolygon object. value = value[0] if len(value) == 1 else MultiPolygon(value) return value @@ -77,11 +70,9 @@ def holes_of_union(geometries): if not all( isinstance(geometry, (Polygon, MultiPolygon)) for geometry in geometries ): - raise TypeError( - f"Must be a Polygon or MultiPolygon (got types {set([x.geom_type for x in geometries])})!" - ) + raise TypeError(f"Must be a Polygon or MultiPolygon (got types {set([x.geom_type for x in geometries])})!") - union = union_all(geometries) + union = unary_union(geometries) series = holes(union) series.crs = geometries.crs return series @@ -120,10 +111,7 @@ def close_gaps(geometries, relative_threshold=0.1, force_polygons=False): geometries = get_geometries(geometries) gaps = holes_of_union(geometries) return absorb_by_shared_perimeter( - gaps, - geometries, - relative_threshold=relative_threshold, - force_polygons=force_polygons, + gaps, geometries, relative_threshold=relative_threshold, force_polygons=force_polygons ) @@ -163,15 +151,10 @@ def resolve_overlaps(geometries, relative_threshold=0.1, force_polygons=False): to_remove = GeoSeries( pandas.concat([overlaps.droplevel(1), overlaps.droplevel(0)]), crs=overlaps.crs ) - with_overlaps_removed = geometries.apply( - lambda x: x.difference(union_all(to_remove)) - ) + with_overlaps_removed = geometries.apply(lambda x: x.difference(unary_union(to_remove))) return absorb_by_shared_perimeter( - overlaps, - with_overlaps_removed, - relative_threshold=None, - force_polygons=force_polygons, + overlaps, with_overlaps_removed, relative_threshold=None, force_polygons=force_polygons ) @@ -189,9 +172,7 @@ def quick_repair(geometries, relative_threshold=0.1, force_polygons=False): For a more careful repair that takes adjacencies and higher-order overlaps between geometries into account, consider using smart_repair instead. """ - return autorepair( - geometries, relative_threshold=relative_threshold, force_polygons=force_polygons - ) + return autorepair(geometries, relative_threshold=relative_threshold, force_polygons=force_polygons) def autorepair(geometries, relative_threshold=0.1, force_polygons=False): @@ -213,28 +194,16 @@ def autorepair(geometries, relative_threshold=0.1, force_polygons=False): if force_polygons: geometries = make_valid_polygons(remove_repeated_vertices(geometries)) - geometries = make_valid_polygons( - resolve_overlaps( - geometries, - relative_threshold=relative_threshold, - force_polygons=force_polygons, - ) - ) - geometries = make_valid_polygons( - close_gaps( - geometries, - relative_threshold=relative_threshold, - force_polygons=force_polygons, - ) - ) + geometries = make_valid_polygons(resolve_overlaps(geometries, + relative_threshold=relative_threshold, + force_polygons=force_polygons)) + geometries = make_valid_polygons(close_gaps(geometries, + relative_threshold=relative_threshold, + force_polygons=force_polygons)) else: geometries = remove_repeated_vertices(geometries).make_valid() - geometries = resolve_overlaps( - geometries, relative_threshold=relative_threshold - ).make_valid() - geometries = close_gaps( - geometries, relative_threshold=relative_threshold - ).make_valid() + geometries = resolve_overlaps(geometries, relative_threshold=relative_threshold).make_valid() + geometries = close_gaps(geometries, relative_threshold=relative_threshold).make_valid() return geometries @@ -244,9 +213,7 @@ def remove_repeated_vertices(geometries): Removes repeated vertices. Vertices are considered to be repeated if they appear consecutively, excluding the start and end points. """ - return geometries.geometry.apply( - lambda x: apply_func_to_polygon_parts(x, dedup_vertices) - ) + return geometries.geometry.apply(lambda x: apply_func_to_polygon_parts(x, dedup_vertices)) def snap_to_grid(geometries, n=-7): @@ -263,19 +230,16 @@ def crop_to(source, target): """ Crops the source geometries to the target geometries. """ - target_union = union_all(get_geometries(target)) - cropped_geometries = get_geometries(source).apply( - lambda x: x.intersection(target_union) - ) + target_union = unary_union(get_geometries(target)) + cropped_geometries = get_geometries(source).apply(lambda x: x.intersection(target_union)) if (cropped_geometries.area == 0).any(): - warnings.warn( - "Some cropped geometries have zero area, likely due to\n" - + "large differences in the union of the geometries in your\n" - + "source and target shapefiles. This may become an issue\n" - + "when maupping.\n", - AreaCroppingWarning, - ) + warnings.warn("Some cropped geometries have zero area, likely due to\n" + + "large differences in the union of the geometries in your\n" + + "source and target shapefiles. This may become an issue\n" + + "when maupping.\n", + AreaCroppingWarning + ) return cropped_geometries @@ -291,18 +255,13 @@ def expand_to(source, target, force_polygons=False): else: geometries = get_geometries(source).make_valid() - source_union = union_all(geometries) + source_union = unary_union(geometries) leftover_geometries = get_geometries(target).apply(lambda x: x - source_union) - leftover_geometries = leftover_geometries[~leftover_geometries.is_empty].explode( - index_parts=False - ) + leftover_geometries = leftover_geometries[~leftover_geometries.is_empty].explode(index_parts=False) geometries = absorb_by_shared_perimeter( - leftover_geometries, - get_geometries(source), - relative_threshold=None, - force_polygons=force_polygons, + leftover_geometries, get_geometries(source), relative_threshold=None, force_polygons=force_polygons ) return geometries @@ -322,14 +281,14 @@ def doctor(source, target=None, silent=False, accept_holes=False): False. (Default is accept_holes = False.) """ shapefiles = [source] - source_union = union_all(get_geometries(source)) + source_union = unary_union(get_geometries(source)) health_check = True if target is not None: shapefiles.append(target) - target_union = union_all(get_geometries(target)) + target_union = unary_union(get_geometries(target)) sym_area = target_union.symmetric_difference(source_union).area if sym_area != 0: @@ -338,9 +297,7 @@ def doctor(source, target=None, silent=False, accept_holes=False): health_check = False for shp in shapefiles: - if not shp.geometry.apply( - lambda x: isinstance(x, (Polygon, MultiPolygon)) - ).all(): + if not shp.geometry.apply(lambda x: isinstance(x, (Polygon, MultiPolygon))).all(): if silent is False: print("Some rows do not have geometries.") health_check = False @@ -389,9 +346,7 @@ def apply_func_to_polygon_parts(shape, func): elif isinstance(shape, MultiPolygon): return MultiPolygon([func(poly) for poly in shape.geoms]) else: - raise TypeError( - f"Can only apply {func} to a Polygon or MultiPolygon (got {shape} with type {type(shape)})!" - ) + raise TypeError(f"Can only apply {func} to a Polygon or MultiPolygon (got {shape} with type {type(shape)})!") def dedup_vertices(polygon): @@ -426,31 +381,16 @@ def dedup_vertices(polygon): def snap_polygon_to_grid(polygon, n=-7): if len(polygon.interiors) == 0: - return Polygon( - [(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords] - ) + return Polygon([(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords]) else: - return Polygon( - [(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords], - holes=[ - [(round(x, -n), round(y, -n)) for x, y in interior_ring.coords] - for interior_ring in polygon.interiors - ], - ) + return Polygon([(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords], holes=[[(round(x, -n), round(y, -n)) for x, y in interior_ring.coords] for interior_ring in polygon.interiors]) def snap_multilinestring_to_grid(multilinestring, n=-7): if multilinestring.geom_type == "LineString": - return LineString( - [(round(x, -n), round(y, -n)) for x, y in multilinestring.coords] - ) + return LineString([(round(x, -n), round(y, -n)) for x, y in multilinestring.coords]) elif multilinestring.geom_type == "MultiLineString": - return MultiLineString( - [ - LineString([(round(x, -n), round(y, -n)) for x, y in linestring.coords]) - for linestring in multilinestring.geoms - ] - ) + return MultiLineString([LineString([(round(x, -n), round(y, -n)) for x, y in linestring.coords]) for linestring in multilinestring.geoms]) def split_by_level(series, multiindex): @@ -461,9 +401,7 @@ def split_by_level(series, multiindex): @require_same_crs -def absorb_by_shared_perimeter( - sources, targets, relative_threshold=None, force_polygons=False -): +def absorb_by_shared_perimeter(sources, targets, relative_threshold=None, force_polygons=False): if len(sources) == 0: return targets @@ -484,8 +422,7 @@ def absorb_by_shared_perimeter( assignment = assignment[under_threshold] sources_to_absorb = GeoSeries( - sources.groupby(assignment).apply(union_all), - crs=sources.crs, + sources.groupby(assignment).apply(unary_union), crs=sources.crs, ) # Note that the following line produces a warning message when sources_to_absorb @@ -497,7 +434,7 @@ def absorb_by_shared_perimeter( # This difference in indices is expected since not all target geometries may have sources # to absorb, so it would be nice to remove this warning. - result = targets.union(sources_to_absorb, align=True) + result = targets.union(sources_to_absorb) # The .union call only returns the targets who had a corresponding # source to absorb. Now we fill in all of the unchanged targets. diff --git a/maup/smart_repair.py b/maup/smart_repair.py index 1d6d4bf..8a51a55 100644 --- a/maup/smart_repair.py +++ b/maup/smart_repair.py @@ -7,17 +7,10 @@ import shapely from geopandas import GeoSeries, GeoDataFrame -from shapely import make_valid, extract_unique_points, union_all +from shapely import make_valid, extract_unique_points from shapely.strtree import STRtree -from shapely.ops import polygonize, linemerge, nearest_points -from shapely.geometry import ( - Polygon, - MultiPolygon, - Point, - MultiPoint, - LineString, - MultiLineString, -) +from shapely.ops import unary_union, polygonize, linemerge, nearest_points +from shapely.geometry import Polygon, MultiPolygon, Point, MultiPoint, LineString, MultiLineString from shapely.geometry.polygon import orient from tqdm import tqdm, TqdmWarning @@ -26,9 +19,9 @@ from .indexed_geometries import get_geometries from .intersections import intersections from .progress_bar import progress -from .repair import doctor, snap_to_grid +from .repair import doctor, snap_to_grid, snap_multilinestring_to_grid -warnings.filterwarnings("ignore", "GeoSeries.isna", UserWarning) +warnings.filterwarnings('ignore', 'GeoSeries.isna', UserWarning) warnings.filterwarnings("ignore", category=TqdmWarning) pandas.options.mode.chained_assignment = None @@ -38,7 +31,7 @@ Some of these functions are based on the functions in Mary Barker's check_shapefile_connectivity.py script in @gerrymandr/Preprocessing. -Updated functions for maup 2.0.0 were written by Jeanne Clelland. +Updated functions for maup 2.x were written by Jeanne Clelland. """ ######### @@ -46,54 +39,46 @@ ######### -def smart_repair( - geometries_df, - snapped=True, - snap_precision=10, - fill_gaps=True, - fill_gaps_threshold=0.1, - disconnection_threshold=0.0001, - nest_within_regions=None, - min_rook_length=None, -): +def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, + fill_gaps_threshold=0.1, disconnection_threshold=0.0001, + nest_within_regions=None, min_rook_length=None): """ Repairs topology issues (overlaps, gaps, invalid polygons) in a geopandas GeoDataFrame or GeoSeries, with an emphasis on preserving intended adjacency relations between geometries as closely as possible. - Specifically, the algorithm: - - 1. Applies shapely.make_valid to all polygon geometries. - 2. If snapped = True (default), snaps all polygon vertices to a grid of size no - more than 10^(-snap_precision) times the max of width/height of the entire - extent of the input. HIGHLY RECOMMENDED to avoid topological exceptions due to - rounding errors. Default value for snap_precision is 10; if topological - exceptions still occur, try reducing snap_precision (which must be integer- - valued) to 9 or 8. - 3. Resolves all overlaps. - 4. If fill_gaps = True (default), closes all simply connected gaps with area - less than fill_gaps_threshold times the largest area of all geometries adjoining - the gap. Default threshold is 10%; if fill_gaps_threshold = None then all - simply connected gaps will be filled. - 5. If nest_within_regions is a secondary GeoDataFrame/GeoSeries of region boundaries - (e.g., counties in a state) then all of the above will be performed so that - repaired geometries nest cleanly into the region boundaries; each repaired - geometry will be contained in the region with which the original geometry has the - largest area of intersection. Default value is None. - 6. If min_rook_length is given a numerical value, replaces all rook adjacencies - with length below this value with queen adjacencies. Note that this is an - absolute value and not a relative value, so make sure that the value provided - is in the correct units with respect to the input's CRS. - Default value is None. - 7. Sometimes the repair process creates tiny fragments that are disconnected from - the district that they are assigned to. A final cleanup step assigns any such - fragments to a neighboring geometry if their area is less than - disconnection_threshold times the area of the largest connected component of - their assigned geometry. Default threshold is 0.01%, and this seems to work - well in practice. + Specifically, the algorithm + (1) Applies shapely.make_valid to all polygon geometries. + (2) If snapped = True (default), snaps all polygon vertices to a grid of size no + more than 10^(-snap_precision) times the max of width/height of the entire + extent of the input. HIGHLY RECOMMENDED to avoid topological exceptions due to + rounding errors. Default value for snap_precision is 9; if topological + exceptions still occur, try reducing snap_precision (which must be integer- + valued) to 8 or 7. + (3) Resolves all overlaps. + (4) If fill_gaps = True (default), closes all simply connected gaps with area + less than fill_gaps_threshold times the largest area of all geometries adjoining + the gap. Default threshold is 10%; if fill_gaps_threshold = None then all + simply connected gaps will be filled. + (5) If nest_within_regions is a secondary GeoDataFrame/GeoSeries of region boundaries + (e.g., counties in a state) then all of the above will be performed so that + repaired geometries nest cleanly into the region boundaries; each repaired + geometrywill be contained in the region with which the original geometry has the + largest area of intersection. Default value is None. + (6) If min_rook_length is given a numerical value, replaces all rook adjacencies + with length below this value with queen adjacencies. Note that this is an + absolute value and not a relative value, so make sure that the value provided + is in the correct units with respect to the input's CRS. + Default value is None. + (7) Sometimes the repair process creates tiny fragments that are disconnected from + the district that they are assigned to. A final cleanup step assigns any such + fragments to a neighboring geometry if their area is less than + disconnection_threshold times the area of the largest connected component of + their assigned geometry. Default threshold is 0.01%, and this seems to work + well in practice. """ - # Keep a copy of the original input for comparisons later! + # Keep a copy of the original input for comparisons later. if isinstance(geometries_df, GeoSeries): orig_input_type = "geoseries" geometries_df = GeoDataFrame(geometry=geometries_df) @@ -103,26 +88,21 @@ def smart_repair( geometries_df = geometries_df.copy() geometries0_df = geometries_df.copy() else: - raise TypeError( - "Input geometries must be in the form of a geopandas GeoSeries or GeoDataFrame." - ) + raise TypeError("Input geometries must be in the form of a geopandas GeoSeries or GeoDataFrame.") # Ensure that geometries are 2-D and not 3-D: for i in geometries_df.index: - geometries_df.at[i, "geometry"] = shapely.wkb.loads( - shapely.wkb.dumps(geometries_df["geometry"][i], output_dimension=2) - ) + geometries_df.loc[i, "geometry"] = shapely.wkb.loads( + shapely.wkb.dumps(geometries_df.loc[i, "geometry"], output_dimension=2)) # Ensure that crs is not geographic: if geometries_df.crs is not None: if geometries_df.crs.is_geographic: - raise Exception( - "Input geometries must be in a projected, non-geographic CRS. To project a GeoDataFrame 'gdf' to UTM, use 'gdf = gdf.to_crs(gdf.estimate_utm_crs())' " - ) + raise Exception("Input geometries must be in a projected, non-geographic CRS. To project a GeoDataFrame 'gdf' to UTM, use 'gdf = gdf.to_crs(gdf.estimate_utm_crs())' ") # If nest_within_regions is not None, require it to have the same CRS as the main shapefile # and set regions_df equal to a GeoDataFrame version. - # nest_within_regions is None, set regions_df equal to None so we can use it as a parameter later. + # If nest_within_regions is None, set regions_df equal to None so we can use it as a parameter later. if nest_within_regions is None: regions_df = None else: @@ -131,90 +111,61 @@ def smart_repair( elif isinstance(nest_within_regions, GeoDataFrame): regions_df = nest_within_regions.copy() else: - raise TypeError( - "nest_within_regions must be a geopandas GeoSeries or GeoDataFrame." - ) + raise TypeError("nest_within_regions must be a geopandas GeoSeries or GeoDataFrame.") if nest_within_regions.crs != geometries_df.crs: - raise Exception( - "nest_within_regions must be in the same CRS as the geometries being repaired." - ) + raise Exception("nest_within_regions must be in the same CRS as the geometries being repaired.") if doctor(nest_within_regions, silent=True, accept_holes=True) is False: - raise Exception( - "nest_within_regions must be topologically clean---i.e., all geometries must be valid and there must be no overlaps between geometries. Generally the best source for region shapefiles is the U.S. Census Burueau." - ) + raise Exception("nest_within_regions must be topologically clean---i.e., all geometries must be valid and there must be no overlaps between geometries. Generally the best source for region shapefiles is the U.S. Census Burueau.") # Before doing anything else, make sure all polygons are valid, convert any empty # geometries to empty Polygons to avoid type errors, and remove any LineStrings and # MultiLineStrings. for i in geometries_df.index: - geometries_df.at[i, "geometry"] = make_valid(geometries_df["geometry"][i]) - if geometries_df["geometry"][i] is None: - geometries_df.at[i, "geometry"] = Polygon() - if geometries_df["geometry"][i].geom_type == "GeometryCollection": - geometries_df.at[i, "geometry"] = union_all( - [ - x - for x in geometries_df["geometry"][i].geoms - if x.geom_type in ("Polygon", "MultiPolygon") - ] - ) + geometries_df.loc[i, "geometry"] = make_valid(geometries_df.loc[i, "geometry"]) + if geometries_df.loc[i, "geometry"] is None: + geometries_df.loc[i, "geometry"] = Polygon() + if geometries_df.loc[i, "geometry"].geom_type == "GeometryCollection": + geometries_df.loc[i, "geometry"] = unary_union([x for x in geometries_df.loc[i, "geometry"].geoms if x.geom_type in ("Polygon", "MultiPolygon")]) # If snapped is True, snap all polygon vertices to a grid of size no more than - # 10^(-10) times the max of width/height of the entire extent of the input. - # (For instance, in Texas this would be less than 1/100th of an inch.) + # 10^(-snap_precision) times the max of width/height of the entire extent of the input. + # (For instance, in Texas this would be less than 1/10th of an inch.) # This avoids a rare "non-noded intersection" error due to a GEOS bug and leaves # several orders of magnitude for additional intersection operations before hitting # python's precision limit of about 10^(-15). + + # Do this is two steps: first snap the original vertices to a grid of size + # 10^(-snap_precision) times the max of width/height of the entire extent of the input. + # Then in the building blocks function snap the points of intersection to a grid of + # size 10^(-snap_precision-1) times the max of width/height of the entire extent of the input. if snapped: # These bounds are in the form (xmin, ymin, xmax, ymax) geometries_total_bounds = geometries_df.total_bounds - largest_bound = max( - geometries_total_bounds[2] - geometries_total_bounds[0], - geometries_total_bounds[3] - geometries_total_bounds[1], - ) + largest_bound = max(geometries_total_bounds[2] - geometries_total_bounds[0], geometries_total_bounds[3] - geometries_total_bounds[1]) snap_magnitude = int(math.log10(largest_bound)) - snap_precision - geometries_df["geometry"] = snap_to_grid( - geometries_df["geometry"], n=snap_magnitude - ) + geometries_df["geometry"] = snap_to_grid(geometries_df["geometry"], n=snap_magnitude) if nest_within_regions is not None: - regions_df["geometry"] = snap_to_grid( - regions_df["geometry"], n=snap_magnitude - ) + regions_df["geometry"] = snap_to_grid(regions_df["geometry"], n=snap_magnitude) # Snapping could possibly have created some invalid polygons, so do another round # of validity checks - and do a validity check for regions as well, if applicable. for i in geometries_df.index: - geometries_df.at[i, "geometry"] = make_valid(geometries_df["geometry"][i]) - if geometries_df["geometry"][i].geom_type == "GeometryCollection": - geometries_df.at[i, "geometry"] = union_all( - [ - x - for x in geometries_df["geometry"][i].geoms - if x.geom_type in ("Polygon", "MultiPolygon") - ] - ) + geometries_df.loc[i, "geometry"] = make_valid(geometries_df.loc[i, "geometry"]) + if geometries_df.loc[i, "geometry"].geom_type == "GeometryCollection": + geometries_df.loc[i, "geometry"] = unary_union([x for x in geometries_df.loc[i, "geometry"].geoms if x.geom_type in ("Polygon", "MultiPolygon")]) if nest_within_regions is not None: for i in regions_df.index: - regions_df.at[i, "geometry"] = make_valid(regions_df["geometry"][i]) - if regions_df["geometry"][i].geom_type == "GeometryCollection": - regions_df.at[i, "geometry"] = union_all( - [ - x - for x in regions_df["geometry"][i].geoms - if x.geom_type in ("Polygon", "MultiPolygon") - ] - ) - print( - "Snapping all geometries to a grid with precision 10^(", - snap_magnitude, - ") to avoid GEOS errors.", - ) + regions_df.loc[i, "geometry"] = make_valid(regions_df.loc[i, "geometry"]) + if regions_df.loc[i, "geometry"].geom_type == "GeometryCollection": + regions_df.loc[i, "geometry"] = unary_union([x for x in regions_df.loc[i, "geometry"].geoms if x.geom_type in ("Polygon", "MultiPolygon")]) + print("Snapping all geometries to a grid with precision 10^(", snap_magnitude, ") to avoid GEOS errors.") + + else: + snap_magnitude = None # Construct data about overlaps of all orders, plus holes. - overlap_tower, holes_df = building_blocks( - geometries_df, nest_within_regions=regions_df - ) + overlap_tower, holes_df = building_blocks(geometries_df, snap_magnitude=snap_magnitude, nest_within_regions=regions_df) # Use data from the overlap tower to rebuild geometries with no overlaps. # If nest_within_regions is not None, resolve overlaps and fill holes (if applicable) @@ -230,14 +181,11 @@ def smart_repair( # Also remove any non-simply connected holes since our algorithm breaks # down in that case, regardless of whether or not a relative area # threshold has been set. - holes_df, num_holes_dropped = drop_bad_holes( - reconstructed_df, holes_df, fill_gaps_threshold=fill_gaps_threshold - ) - if num_holes_dropped > 0: - print( - num_holes_dropped, - "gaps will remain unfilled, because they either are not simply connected or exceed the area threshold.", - ) + holes_df, num_holes_dropped_nsc, num_holes_dropped_aat = drop_bad_holes(reconstructed_df, holes_df, fill_gaps_threshold=fill_gaps_threshold) + if num_holes_dropped_aat > 0: + print(num_holes_dropped_aat, "gaps will remain unfilled, because they exceed the area threshold.") + if num_holes_dropped_nsc > 0: + print(num_holes_dropped_nsc, "gaps will remain unfilled, because they are not simply connected.") print("Filling gaps...") reconstructed_df = smart_close_gaps(reconstructed_df, holes_df) @@ -249,29 +197,17 @@ def smart_repair( print("Resolving overlaps...") reconstructed_df = geometries_df.copy() - geometries_to_regions_assignment = assign( - geometries_df.geometry, regions_df.geometry - ) + geometries_to_regions_assignment = assign(geometries_df.geometry, regions_df.geometry) for r_ind in nest_within_regions.index: - geometries_this_region_indices = [ - g_ind - for g_ind in geometries_df.index - if geometries_to_regions_assignment[g_ind] == r_ind - ] - geometries_this_region_df = geometries_df.loc[ - geometries_this_region_indices - ] + geometries_this_region_indices = [g_ind for g_ind in geometries_df.index if geometries_to_regions_assignment[g_ind] == r_ind] + geometries_this_region_df = geometries_df.loc[geometries_this_region_indices] overlap_tower_this_region = [] for i in range(len(overlap_tower)): - overlap_tower_this_region.append( - overlap_tower[i][overlap_tower[i]["region"] == r_ind] - ) + overlap_tower_this_region.append(overlap_tower[i][overlap_tower[i]["region"] == r_ind]) - reconstructed_this_region_df = reconstruct_from_overlap_tower( - geometries_this_region_df, overlap_tower_this_region, nested=True - ) + reconstructed_this_region_df = reconstruct_from_overlap_tower(geometries_this_region_df, overlap_tower_this_region, nested=True) if fill_gaps: holes_this_region_df = holes_df[holes_df["region"] == r_ind] @@ -279,26 +215,15 @@ def smart_repair( # Also remove any non-simply connected holes since our algorithm breaks # down in that case, regardless of whether or not a relative area # threshold has been set. - holes_this_region_df, num_holes_dropped_this_region = drop_bad_holes( - reconstructed_this_region_df, - holes_this_region_df, - fill_gaps_threshold=fill_gaps_threshold, - ) - if num_holes_dropped_this_region > 0: - print( - num_holes_dropped_this_region, - "gaps in region", - r_ind, - "will remain unfilled, because they either are not simply connected or exceed the area threshold.", - ) - - reconstructed_this_region_df = smart_close_gaps( - reconstructed_this_region_df, holes_this_region_df - ) - - reconstructed_df.loc[ - list(reconstructed_this_region_df.index), "geometry" - ] = reconstructed_this_region_df["geometry"] + holes_this_region_df, num_holes_dropped_this_region_nsc, num_holes_dropped_this_region_aat = drop_bad_holes(reconstructed_this_region_df, holes_this_region_df, fill_gaps_threshold=fill_gaps_threshold) + if num_holes_dropped_this_region_aat > 0: + print(num_holes_dropped_this_region_aat, "gaps in region", r_ind, "will remain unfilled, because they exceed the area threshold.") + if num_holes_dropped_this_region_nsc > 0: + print(num_holes_dropped_this_region_nsc, "gaps in region", r_ind, "will remain unfilled, because they are not simply connected.") + + reconstructed_this_region_df = smart_close_gaps(reconstructed_this_region_df, holes_this_region_df) + + reconstructed_df.loc[list(reconstructed_this_region_df.index), "geometry"] = reconstructed_this_region_df["geometry"] # Check for geometries that have become (more) disconnected, generally with an extra # component of negligible area. If any are found and the area is negligible, @@ -306,156 +231,76 @@ def smart_repair( # If the area is not negligible, leave it alone and report it so that the user # can decide what to do about it. - disconnected_df = reconstructed_df[ - reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon") - ] + disconnected_df = reconstructed_df[reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon")] # This will include geometries that were disconnected in the original; need to # filter by whether they got worse. + +# FIX: Allow for the possibility that reconnecting one geometry inadvertently reconnects +# another one at the same time/ + if len(disconnected_df) > 0: - disconnected_poly_indices = [] - for ind in disconnected_df.index: - if num_components(reconstructed_df["geometry"][ind]) > num_components( - geometries0_df["geometry"][ind] - ): - disconnected_poly_indices.append(ind) - - if len(disconnected_poly_indices) > 0: - # These are the ones (if any) that got worse. - geometries = get_geometries(reconstructed_df) - spatial_index = STRtree(geometries) - index_by_iloc = dict( - (i, list(geometries.index)[i]) for i in range(len(geometries.index)) - ) - - for g_ind in disconnected_poly_indices: - excess = num_components( - reconstructed_df["geometry"][g_ind] - ) - num_components(geometries0_df["geometry"][g_ind]) - component_num_list = list( - range(len(reconstructed_df["geometry"][g_ind].geoms)) - ) + geometries = get_geometries(reconstructed_df) + spatial_index = STRtree(geometries) + index_by_iloc = dict((i, list(geometries.index)[i]) for i in range(len(geometries.index))) + + for g_ind in disconnected_df.index: + if num_components(reconstructed_df.loc[g_ind, "geometry"]) > num_components(geometries0_df.loc[g_ind, "geometry"]): + excess = num_components(reconstructed_df.loc[g_ind, "geometry"]) - num_components(geometries0_df.loc[g_ind, "geometry"]) + component_num_list = list(range(len(reconstructed_df.loc[g_ind, "geometry"].geoms))) component_areas = [] - for c_ind in range(len(reconstructed_df["geometry"][g_ind].geoms)): - component_areas.append( - (c_ind, reconstructed_df["geometry"][g_ind].geoms[c_ind].area) - ) + for c_ind in range(len(reconstructed_df.loc[g_ind, "geometry"].geoms)): + component_areas.append((c_ind, reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind].area)) component_areas_sorted = sorted(component_areas, key=lambda tup: tup[1]) - big_area = max( - [ - reconstructed_df["geometry"][g_ind].area, - geometries0_df["geometry"][g_ind].area, - ] - ) + big_area = max([reconstructed_df.loc[g_ind, "geometry"].area, geometries0_df.loc[g_ind, "geometry"].area]) for i in range(excess): # Check whether the ith smallest component has small enough area, and if # so find a better polygon to add it to. c_ind = component_areas_sorted[i][0] - this_fragment = reconstructed_df["geometry"][g_ind].geoms[c_ind] - if ( - component_areas_sorted[i][1] - < disconnection_threshold * big_area - ): - possible_intersect_integer_indices = [ - *set( - numpy.ndarray.flatten( - spatial_index.query(this_fragment) - ) - ) - ] - possible_intersect_indices = [ - (index_by_iloc[k]) - for k in possible_intersect_integer_indices - ] + this_fragment = reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind] + if component_areas_sorted[i][1] < disconnection_threshold*big_area: + possible_intersect_integer_indices = [*set(numpy.ndarray.flatten(spatial_index.query(this_fragment)))] + possible_intersect_indices = [(index_by_iloc[k]) for k in possible_intersect_integer_indices] if nest_within_regions is not None: # Restrict to geometries in the same region as this geometry - possible_intersect_indices = [ - ind - for ind in possible_intersect_indices - if geometries_to_regions_assignment[ind] - == geometries_to_regions_assignment[g_ind] - ] + possible_intersect_indices = [ind for ind in possible_intersect_indices if geometries_to_regions_assignment[ind] == geometries_to_regions_assignment[g_ind]] shared_perimeters = [] for g_ind2 in possible_intersect_indices: - if ( - g_ind2 != g_ind - and not (this_fragment.boundary) - .intersection( - reconstructed_df["geometry"][g_ind2].boundary - ) - .is_empty - ): - shared_perimeters.append( - ( - g_ind2, - (this_fragment.boundary) - .intersection( - reconstructed_df["geometry"][ - g_ind2 - ].boundary - ) - .length, - ) - ) + if g_ind2 != g_ind and not (this_fragment.boundary).intersection(reconstructed_df.loc[g_ind2, "geometry"].boundary).is_empty: + shared_perimeters.append((g_ind2, (this_fragment.boundary).intersection(reconstructed_df.loc[g_ind2, "geometry"].boundary).length)) # If this is an isolated fragment and doesn't touch any other # geometries, leave it alone; otherwise, choose a geometry to # adjoin it to by largest shared perimeter. if len(shared_perimeters) > 0: - component_num_list.remove( - c_ind - ) # Tells us to take out this component later - max_shared_perim = sorted( - shared_perimeters, key=lambda tup: tup[1] - )[-1] + component_num_list.remove(c_ind) # Tells us to take out this component later + max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] - reconstructed_df.at[poly_to_add_to, "geometry"] = union_all( - [ - reconstructed_df["geometry"][poly_to_add_to], - this_fragment, - ] - ) + reconstructed_df.loc[poly_to_add_to, "geometry"] = unary_union( + [reconstructed_df.loc[poly_to_add_to, "geometry"], this_fragment]) if len(component_num_list) == 1: - reconstructed_df.at[g_ind, "geometry"] = reconstructed_df[ - "geometry" - ][g_ind].geoms[component_num_list[0]] + reconstructed_df.loc[g_ind, "geometry"] = reconstructed_df.loc[g_ind, "geometry"].geoms[component_num_list[0]] elif len(component_num_list) > 1: - reconstructed_df.at[g_ind, "geometry"] = MultiPolygon( - [ - reconstructed_df["geometry"][g_ind].geoms[c_ind] - for c_ind in component_num_list - ] - ) + reconstructed_df.loc[g_ind, "geometry"] = MultiPolygon( + [reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind] for c_ind in component_num_list]) else: - print( - "WARNING: A component of the geometry at index", - g_ind, - "was badly disconnected and redistributed to other geometries!", - ) + print("WARNING: A component of the geometry at index", g_ind, "was badly disconnected and redistributed to other geometries!") # We should usually now be back to the correct number of components everywhere, but # there may occasionally be exceptions, so check again and alert the user if not. - disconnected_df_2 = reconstructed_df[ - reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon") - ] + disconnected_df_2 = reconstructed_df[reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon")] if len(disconnected_df_2) > 0: for ind in disconnected_df_2.index: - if num_components(reconstructed_df["geometry"][ind]) > num_components( - geometries0_df["geometry"][ind] - ): - print( - "WARNING: A component of the geometry at index", - ind, - "may have been disconnected!", - ) + if num_components(reconstructed_df.loc[ind, "geometry"]) > num_components(geometries0_df.loc[ind, "geometry"]): + print("WARNING: A component of the geometry at index", ind, "may have been disconnected!") if min_rook_length is not None: # Find all inter-polygon boundaries shorter than min_rook_length and replace them @@ -473,12 +318,11 @@ def smart_repair( # SUPPORTING FUNCTIONS ######### - def num_components(geom): """Counts the number of connected components of a shapely object.""" if geom.is_empty: return 0 - elif geom.geom_type in ("Polygon", "Point", "LineString"): + elif geom.geom_type in ("Polygon", "Point", "LineString"): return 1 elif geom.geom_type in ("MultiPolygon", "MultiLineString", "GeometryCollection"): return len(geom.geoms) @@ -487,9 +331,13 @@ def num_components(geom): def segments(curve): """Extracts a list of the individual line segments from a LineString""" return list(map(LineString, zip(curve.coords[:-1], curve.coords[1:]))) + + +def contain_each_other(poly1, poly2): + return(poly1.contains(poly2) and poly2.contains(poly1)) -def building_blocks(geometries_df, nest_within_regions=None): +def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None): """ Partitions the extent of the input via all boundaries of all geometries (and regions, if nest_within_regions is a GeoDataFrame/GeoSeries of region @@ -503,28 +351,22 @@ def building_blocks(geometries_df, nest_within_regions=None): geometries_df = geometries_df.copy() if nest_within_regions is not None: if isinstance(nest_within_regions, GeoDataFrame) is False: - raise TypeError( - "nest_within_regions must be either None or a GeoDataFrame." - ) + raise TypeError("nest_within_regions must be either None or a GeoDataFrame.") else: regions_df = nest_within_regions.copy() # Make a list of all the boundaries of all the polygons. # This won't work properly with MultiPolygons, so explode first: boundaries = [] - geometries_exploded_df = geometries_df.explode(index_parts=False).reset_index( - drop=True - ) + geometries_exploded_df = geometries_df.explode(index_parts=False).reset_index(drop=True) for i in geometries_exploded_df.index: - boundaries.append(shapely.boundary(geometries_exploded_df["geometry"][i])) + boundaries.append(shapely.boundary(geometries_exploded_df.loc[i, "geometry"])) # Include region boundaries if applicable: if nest_within_regions is not None: - regions_exploded_df = regions_df.explode(index_parts=False).reset_index( - drop=True - ) + regions_exploded_df = regions_df.explode(index_parts=False).reset_index(drop=True) for i in regions_exploded_df.index: - boundaries.append(shapely.boundary(regions_exploded_df["geometry"][i])) + boundaries.append(shapely.boundary(regions_exploded_df.loc[i, "geometry"])) boundaries_exploded = [] for geom in boundaries: @@ -533,37 +375,39 @@ def building_blocks(geometries_df, nest_within_regions=None): elif geom.geom_type == "MultiLineString": boundaries_exploded += list(geom.geoms) boundaries_union = shapely.node(MultiLineString(boundaries_exploded)) - + + # Snap the noded boundaries to a grid of size snap_magnitude-1 and re-node: + if snap_magnitude is not None: + boundaries_2 = snap_multilinestring_to_grid(boundaries_union, n=snap_magnitude-1) + boundaries_2_exploded = [] + for geom in boundaries_2.geoms: + if geom.geom_type == "LineString": + boundaries_2_exploded.append(geom) + elif geom.geom_type == "MultiLineString": + boundaries_2_exploded += list(geom.geoms) + boundaries_union = shapely.node(MultiLineString(boundaries_2_exploded)) + # Create a geodataframe with all the pieces created by overlaps of all orders, # together with a set for each piece consisting of the polygons that created the overlap. - pieces_df = GeoDataFrame( - columns=["polygon indices"], - geometry=GeoSeries(list(polygonize(boundaries_union))), - crs=geometries_df.crs, - ) - - for i in pieces_df.index: - pieces_df.at[i, "polygon indices"] = set() + pieces_df = GeoDataFrame(columns=["polygon indices"], + geometry=GeoSeries(list(polygonize(boundaries_union))), + crs=geometries_df.crs) + pieces_df["polygon indices"] = [set() for x in range(len(pieces_df.index))] + # Add a column to indicate the region for each piece; if there are no regions the # entries will remain as None. pieces_df["region"] = None g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict( - (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) - ) + g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) # If region boundaries are included, also create an STRtree for the regions # and assign the main geometries to regions by largest area overlap. if nest_within_regions is not None: r_spatial_index = STRtree(regions_df["geometry"]) - r_index_by_iloc = dict( - (i, list(regions_df.index)[i]) for i in range(len(regions_df)) - ) - geometries_to_regions_assignment = assign( - geometries_df.geometry, regions_df.geometry - ) + r_index_by_iloc = dict((i, list(regions_df.index)[i]) for i in range(len(regions_df))) + geometries_to_regions_assignment = assign(geometries_df.geometry, regions_df.geometry) print("Identifying overlaps...") for i in progress(pieces_df.index, len(pieces_df.index)): @@ -571,56 +415,28 @@ def building_blocks(geometries_df, nest_within_regions=None): # Note that "None" is a possibility, and that each piece will belong to a unique # region because the regions GeoDataFrame/GeoSeries MUST be clean. if nest_within_regions is not None: - possible_region_integer_indices = [ - *set( - numpy.ndarray.flatten( - r_spatial_index.query(pieces_df["geometry"][i]) - ) - ) - ] - possible_region_indices = [ - r_index_by_iloc[k] for k in possible_region_integer_indices - ] + possible_region_integer_indices = [*set(numpy.ndarray.flatten(r_spatial_index.query(pieces_df.loc[i, "geometry"])))] + possible_region_indices = [r_index_by_iloc[k] for k in possible_region_integer_indices] for j in possible_region_indices: - if ( - pieces_df["geometry"][i] - .representative_point() - .intersects(regions_df["geometry"][j]) - ): - pieces_df.at[i, "region"] = j + if pieces_df.loc[i, "geometry"].representative_point().intersects(regions_df.loc[j, "geometry"]): + pieces_df.loc[i, "region"] = j # Now identify the set of geometries in the main geometry that each piece is # contained in. If region boundaries are included, then while determining which # geometries each piece is contained in, omit any geometries that are # assigned to a region other than the one the piece is contained in. - possible_geom_integer_indices = [ - *set(numpy.ndarray.flatten(g_spatial_index.query(pieces_df["geometry"][i]))) - ] - possible_geom_indices = [ - g_index_by_iloc[k] for k in possible_geom_integer_indices - ] + possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(pieces_df.loc[i, "geometry"])))] + possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for j in possible_geom_indices: if nest_within_regions is not None: - if ( - pieces_df["geometry"][i] - .representative_point() - .intersects(geometries_df["geometry"][j]) - ): - if geometries_to_regions_assignment[j] == pieces_df["region"][i]: - pieces_df.at[i, "polygon indices"] = pieces_df[ - "polygon indices" - ][i].union({j}) + if pieces_df.loc[i, "geometry"].representative_point().intersects(geometries_df.loc[j, "geometry"]): + if geometries_to_regions_assignment[j] == pieces_df.loc[i, "region"]: + pieces_df.at[i, "polygon indices"] = pieces_df.at[i, "polygon indices"].union({j}) else: - if ( - pieces_df["geometry"][i] - .representative_point() - .intersects(geometries_df["geometry"][j]) - ): - pieces_df.at[i, "polygon indices"] = pieces_df["polygon indices"][ - i - ].union({j}) + if pieces_df.loc[i, "geometry"].representative_point().intersects(geometries_df.loc[j, "geometry"]): + pieces_df.at[i, "polygon indices"] = pieces_df.at[i, "polygon indices"].union({j}) # Organize this info into separate GeoDataFrames for overlaps of all orders - including # order zero, which corresponds to gaps. @@ -639,39 +455,39 @@ def building_blocks(geometries_df, nest_within_regions=None): pieces_df = pieces_df[~pieces_df["region"].isna()].reset_index(drop=True) holes_df = holes_df[~holes_df["region"].isna()].reset_index(drop=True) - consolidated_holes_df = GeoDataFrame( - columns=["polygon indices", "geometry", "region", "overlap degree"], - geometry="geometry", - crs=holes_df.crs, - ) + consolidated_holes_df = GeoDataFrame(columns=["polygon indices", "geometry", "region", "overlap degree"], + geometry="geometry", crs=holes_df.crs) for r_ind in regions_df.index: this_region_holes_df = holes_df[holes_df["region"] == r_ind] - this_region_consolidated_holes = ( - GeoSeries([union_all(this_region_holes_df["geometry"])]) - .explode(index_parts=False) - .reset_index(drop=True) - ) - this_region_consolidated_holes_df = GeoDataFrame( - geometry=this_region_consolidated_holes, crs=holes_df.crs - ) + this_region_consolidated_holes = GeoSeries([unary_union(this_region_holes_df["geometry"])]).explode(index_parts=False).reset_index(drop=True) + this_region_consolidated_holes_df = GeoDataFrame(geometry=this_region_consolidated_holes, crs=holes_df.crs) this_region_consolidated_holes_df.insert(0, "polygon indices", None) - for i in this_region_consolidated_holes_df.index: - this_region_consolidated_holes_df.at[i, "polygon indices"] = set() + this_region_consolidated_holes_df["polygon indices"] = [set() for x in range(len(this_region_consolidated_holes_df.index))] this_region_consolidated_holes_df.insert(2, "region", r_ind) this_region_consolidated_holes_df.insert(2, "overlap degree", 0) - - consolidated_holes_df = pandas.concat( - [consolidated_holes_df, this_region_consolidated_holes_df] - ).reset_index(drop=True) + + consolidated_holes_df = pandas.concat([consolidated_holes_df, this_region_consolidated_holes_df]).reset_index(drop=True) holes_df = consolidated_holes_df + + else: + # Do the same thing we did for holes within each region to consolidate them: + all_consolidated_holes = GeoSeries([unary_union(holes_df["geometry"])]).explode(index_parts=False).reset_index(drop=True) + all_consolidated_holes_df = GeoDataFrame(geometry=all_consolidated_holes, crs=holes_df.crs) + + all_consolidated_holes_df.insert(0, "polygon indices", None) + all_consolidated_holes_df["polygon indices"] = [set() for x in range(len(all_consolidated_holes_df.index))] + all_consolidated_holes_df.insert(2, "region", None) + all_consolidated_holes_df.insert(2, "overlap degree", 0) + + holes_df = all_consolidated_holes_df # Here is a list of GeoDataFrames, one consisting of all overlaps of each order: overlap_tower = [] for i in range(max(pieces_df["overlap degree"])): - overlap_tower.append(pieces_df[pieces_df["overlap degree"] == i + 1]) + overlap_tower.append(pieces_df[pieces_df["overlap degree"] == i+1]) # Drop unnecessary "overlap degree" column and reindex each GeoDataFrame: for i in range(len(overlap_tower)): @@ -703,9 +519,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): for ind in overlap_tower[0].index: this_poly_ind = list(overlap_tower[0]["polygon indices"][ind])[0] this_piece = overlap_tower[0]["geometry"][ind] - geometries_df.at[this_poly_ind, "geometry"] = union_all( - [geometries_df["geometry"][this_poly_ind], this_piece] - ) + geometries_df.loc[this_poly_ind, "geometry"] = unary_union([geometries_df.loc[this_poly_ind, "geometry"], this_piece]) # We will need to know which geometries were disconnected by removing # overlaps, so add columns for numbers of components in the original and refined @@ -714,12 +528,8 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): geometries_df["num components refined"] = 0 for ind in geometries_df.index: - geometries_df.at[ind, "num components orig"] = num_components( - geometries0_df["geometry"][ind] - ) - geometries_df.at[ind, "num components refined"] = num_components( - geometries_df["geometry"][ind] - ) + geometries_df.loc[ind, "num components orig"] = num_components(geometries0_df.loc[ind, "geometry"]) + geometries_df.loc[ind, "num components refined"] = num_components(geometries_df.loc[ind, "geometry"]) # Now, start with the order 2 overlaps and gradually add overlaps at successively # higher orders until done. @@ -731,74 +541,43 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): # can disconnect more than one polygon, and only one of them gets to grab it back. # This will be addressed at the end of the reconstruction process. - geometries_disconnected_df = geometries_df[ - geometries_df["num components refined"] > geometries_df["num components orig"] - ] + geometries_disconnected_df = geometries_df[geometries_df["num components refined"] > geometries_df["num components orig"]] +# FIX: Keep a list of overlaps that don't find a home during this process, and try them again +# at the end. This is necessary because on very rare occasions, a lower-order overlap might +# only adjoin higher-order overlaps and not be able to find a home until other overlaps have +# been assigned. + + orphaned_overlaps = [] + for i in range(1, max_overlap_level): overlaps_df = overlap_tower[i] overlaps_df_unused_indices = overlaps_df.index.tolist() o_spatial_index = STRtree(overlaps_df["geometry"]) - o_index_by_iloc = dict( - (i, list(overlaps_df.index)[i]) for i in range(len(overlaps_df)) - ) + o_index_by_iloc = dict((i, list(overlaps_df.index)[i]) for i in range(len(overlaps_df))) for g_ind in geometries_disconnected_df.index: - possible_overlap_integer_indices = [ - *set( - numpy.ndarray.flatten( - o_spatial_index.query( - geometries_disconnected_df["geometry"][g_ind] - ) - ) - ) - ] - possible_overlap_indices_0 = [ - o_index_by_iloc[k] for k in possible_overlap_integer_indices - ] - possible_overlap_indices = list( - set(possible_overlap_indices_0) & set(overlaps_df_unused_indices) - ) + possible_overlap_integer_indices = [*set(numpy.ndarray.flatten(o_spatial_index.query(geometries_disconnected_df.loc[g_ind, "geometry"])))] + possible_overlap_indices_0 = [o_index_by_iloc[k] for k in possible_overlap_integer_indices] + possible_overlap_indices = list(set(possible_overlap_indices_0) & set(overlaps_df_unused_indices)) geom_finished = False for o_ind in possible_overlap_indices: # If the corresponding overlap intersects this geometry (and was # contained in it originally!), grab it. - if ( - (geom_finished is False) - and (g_ind in list(overlaps_df["polygon indices"][o_ind])) - and ( - not geometries_disconnected_df["geometry"][g_ind] - .intersection(overlaps_df["geometry"][o_ind]) - .is_empty - ) - ): - - if ( - geometries_disconnected_df["geometry"][g_ind].intersection( - overlaps_df["geometry"][o_ind] - ) - ).length > 0: - geometries_disconnected_df.at[g_ind, "geometry"] = union_all( - [ - geometries_disconnected_df["geometry"][g_ind], - overlaps_df["geometry"][o_ind], - ] - ) + if (geom_finished is False) and (g_ind in list(overlaps_df.loc[o_ind, "polygon indices"])) and (not geometries_disconnected_df.loc[g_ind, "geometry"].intersection(overlaps_df.loc[o_ind, "geometry"]).is_empty): + + if (geometries_disconnected_df.loc[g_ind, "geometry"].intersection(overlaps_df.loc[o_ind, "geometry"])).length > 0: + geometries_disconnected_df.loc[g_ind, "geometry"] = unary_union([ + geometries_disconnected_df.loc[g_ind, "geometry"], overlaps_df.loc[o_ind, "geometry"] + ]) overlaps_df_unused_indices.remove(o_ind) - if ( - num_components( - geometries_disconnected_df["geometry"][g_ind] - ) - == geometries_df["num components orig"][g_ind] - ): + if num_components(geometries_disconnected_df.loc[g_ind, "geometry"]) == geometries_df.loc[g_ind, "num components orig"]: geom_finished = True - geometries_df.at[g_ind, "geometry"] = geometries_disconnected_df[ - "geometry" - ][g_ind] + geometries_df.loc[g_ind, "geometry"] = geometries_disconnected_df.loc[g_ind, "geometry"] if geom_finished: geometries_disconnected_df = geometries_disconnected_df.drop(g_ind) @@ -806,54 +585,54 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): # That's all we can do for the disconnected geometries at this level. # Go on to filling in the rest of the overlaps by greatest perimeter. g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict( - (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) - ) + g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) if nested is False: - print("Assigning order", i + 1, "pieces...") + print("Assigning order", i+1, "pieces...") + for o_ind in overlaps_df_unused_indices: - this_overlap = overlaps_df["geometry"][o_ind] + this_overlap = overlaps_df.loc[o_ind, "geometry"] shared_perimeters = [] - possible_geom_integer_indices = [ - *set( - numpy.ndarray.flatten( - g_spatial_index.query(overlaps_df["geometry"][o_ind]) - ) - ) - ] - possible_geom_indices = [ - g_index_by_iloc[k] for k in possible_geom_integer_indices - ] + possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(this_overlap)))] + possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for g_ind in possible_geom_indices: - if (g_ind in list(overlaps_df["polygon indices"][o_ind])) and not ( - this_overlap.boundary - ).intersection(geometries_df["geometry"][g_ind].boundary).is_empty: - shared_perimeters.append( - ( - g_ind, - (this_overlap.boundary) - .intersection(geometries_df["geometry"][g_ind].boundary) - .length, - ) - ) + if (g_ind in list(overlaps_df.loc[o_ind, "polygon indices"])) and not (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).is_empty: + shared_perimeters.append((g_ind, (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).length)) if len(shared_perimeters) > 0: max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] - geometries_df.at[poly_to_add_to, "geometry"] = union_all( - [geometries_df["geometry"][poly_to_add_to], this_overlap] - ) + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap]) + else: - # It seems like this should never happen, but it still seems to on - # very rare occasions. + orphaned_overlaps.append((overlaps_df.loc[o_ind, "geometry"], overlaps_df.loc[o_ind, "polygon indices"])) + +# After completing the overlap tower, try again to assign any orphaned overlaps: + + if len(orphaned_overlaps) > 0: + for o_ind in range(len(orphaned_overlaps)): + this_overlap = orphaned_overlaps[o_ind][0] + this_overlap_polygon_indices = orphaned_overlaps[o_ind][1] + shared_perimeters = [] + possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(this_overlap)))] + possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] + + for g_ind in possible_geom_indices: + if (g_ind in list(this_overlap_polygon_indices)) and not (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).is_empty: + shared_perimeters.append((g_ind, (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).length)) + + if len(shared_perimeters) > 0: + max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] + poly_to_add_to = max_shared_perim[0] + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap]) + + else: + # It seems like this should REALLY never happen now, but I guess we'll see. if nested is False: - print( - "Couldn't find a polygon to glue a component in the intersection of geometries", - overlaps_df["polygon indices"][o_ind], - "to", - ) + print("Couldn't find a polygon to glue a component in the intersection of geometries", overlaps_df.loc[o_ind, "polygon indices"], "to") reconstructed_df = geometries_df del reconstructed_df["num components orig"] @@ -863,75 +642,65 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): def drop_bad_holes(reconstructed_df, holes_df, fill_gaps_threshold): - """Identify holes that won't be filled and drop them from holes_df""" + """ Identify holes that won't be filled and drop them from holes_df """ holes_df = holes_df.copy() if fill_gaps_threshold is not None: spatial_index = STRtree(reconstructed_df.geometry) - index_by_iloc = dict( - (i, list(reconstructed_df.index)[i]) - for i in range(len(reconstructed_df.index)) - ) - hole_indices_to_drop = [] + index_by_iloc = dict((i, list(reconstructed_df.index)[i]) for i in range(len(reconstructed_df.index))) + hole_indices_to_drop_nsc = [] + hole_indices_to_drop_aat = [] for h_ind in holes_df.index: - this_hole = holes_df["geometry"][h_ind] - if shapely.get_num_interior_rings(holes_df["geometry"][h_ind]) > 0: - hole_indices_to_drop.append(h_ind) - else: - possible_intersect_integer_indices = [ - *set(numpy.ndarray.flatten(spatial_index.query(this_hole))) - ] - possible_intersect_indices = [ - (index_by_iloc[k]) for k in possible_intersect_integer_indices - ] - actual_intersect_indices = [ - g_ind - for g_ind in possible_intersect_indices - if not this_hole.intersection( - reconstructed_df["geometry"][g_ind] - ).is_empty - ] - if len(actual_intersect_indices) > 0: - max_geom_area = max( - reconstructed_df["geometry"][g_ind].area - for g_ind in actual_intersect_indices - ) - hole_area_ratio = this_hole.area / max_geom_area - if hole_area_ratio > fill_gaps_threshold: - hole_indices_to_drop.append(h_ind) + this_hole = holes_df.loc[h_ind, "geometry"] + possible_intersect_integer_indices = [*set(numpy.ndarray.flatten(spatial_index.query(this_hole)))] + possible_intersect_indices = [(index_by_iloc[k]) for k in possible_intersect_integer_indices] + actual_intersect_indices = [g_ind for g_ind in possible_intersect_indices if not this_hole.intersection(reconstructed_df.loc[g_ind, "geometry"]).is_empty] + + drop_this_hole_for_area = False + if len(actual_intersect_indices) > 0: + max_geom_area = max(reconstructed_df.loc[g_ind, "geometry"].area for g_ind in actual_intersect_indices) + hole_area_ratio = this_hole.area/max_geom_area + if hole_area_ratio > fill_gaps_threshold: + hole_indices_to_drop_aat.append(h_ind) + drop_this_hole_for_area = True + + if shapely.get_num_interior_rings(holes_df.loc[h_ind, "geometry"]) > 0 and not drop_this_hole_for_area: + hole_indices_to_drop_nsc.append(h_ind) + else: - hole_indices_to_drop = [] + hole_indices_to_drop_nsc = [] + hole_indices_to_drop_aat = [] for h_ind in holes_df.index: - if shapely.get_num_interior_rings(holes_df["geometry"][h_ind]) > 0: - hole_indices_to_drop.append(h_ind) - + if shapely.get_num_interior_rings(holes_df.loc[h_ind, "geometry"]) > 0: + hole_indices_to_drop_nsc.append(h_ind) + + hole_indices_to_drop = hole_indices_to_drop_nsc + hole_indices_to_drop_aat if len(hole_indices_to_drop) > 0: holes_df = holes_df.drop(hole_indices_to_drop).reset_index(drop=True) - return holes_df, len(hole_indices_to_drop) + return holes_df, len(hole_indices_to_drop_nsc), len(hole_indices_to_drop_aat) def smart_close_gaps(geometries_df, holes_df): """ Fill simply connected gaps; general procedure is roughly as follows: - - 1. Fill in gaps that only intersect one non-exterior geometry in the - obvious way. - 2. For remaining gaps, partially fill by "convexifying" boundaries with each - non-exterior geometry. This will have the effect of completely filling - gaps that only intersect 2 geometries and no exterior boundaries. - 3. For any gap that intersects 4 or more geometries nontrivially (including - exterior boundaries), find the non-adjacent pair with the shortest distance - between them and try to connect the pair by adding a "triangle" to each of the - non-exterior geometries in the pair. (Keep trying until this succeeds for - some pair.) This reduces the gap to 1 or 2 smaller gaps, each intersecting - strictly fewer geometries than the original. Put the smaller gaps back in the - queue for the next round. - 4. For any gap that intersects exactly 3 geometries (including exterior boundaries) - nontrivially, fill by a process that gives a portion of the gap to each of - the non-exterior geometries that it intersects. + (1) Fill in gaps that only intersect one non-exterior geometry in the + obvious way. + (2) For remaining gaps, partially fill by "convexifying" boundaries with each + non-exterior geometry. This will have the effect of completely filling + gaps that only intersect 2 geometries and no exterior boundaries. + (3) For any gap that intersects 4 or more geometries nontrivially (including + exterior boundaries), find the non-adjacent pair with the shortest distance + between them and try to connect the pair by adding a "triangle" to each of the + non-exterior geometries in the pair. (Keep trying until this succeeds for + some pair.) This reduces the gap to 1 or 2 smaller gaps, each intersecting + strictly fewer geometries than the original. Put the smaller gaps back in the + queue for the next round. + (4) For any gap that intersects exactly 3 geometries (including exterior boundaries) + nontrivially, fill by a process that gives a portion of the gap to each of + the non-exterior geometries that it intersects. """ geometries_df = geometries_df.copy() holes_df = holes_df.copy() @@ -942,16 +711,11 @@ def smart_close_gaps(geometries_df, holes_df): # Now proceed with filling simplified gaps. if len(holes_df) > 0: holes_to_process = deque(list(holes_df["geometry"])) - this_region = list(holes_df["region"])[ - 0 - ] # All holes in this dataframe should be from the same region + this_region = list(holes_df["region"])[0] # All holes in this dataframe should be from the same region if this_region is None: pbar = tqdm(desc="Gaps to fill", total=len(holes_to_process)) else: - pbar = tqdm( - desc=f"Gaps to fill in region {this_region}", - total=len(holes_to_process), - ) + pbar = tqdm(desc=f"Gaps to fill in region {this_region}", total=len(holes_to_process)) else: holes_to_process = deque([]) pbar = tqdm(desc="Gaps to fill", total=len(holes_to_process)) @@ -969,12 +733,8 @@ def smart_close_gaps(geometries_df, holes_df): if len(set(this_hole_boundaries_df["target"]).difference({-1})) == 1: # Attach the gap to the unique non-exterior geometry that it intersects: - poly_to_add_to = list( - set(this_hole_boundaries_df["target"]).difference({-1}) - )[0] - geometries_df.at[poly_to_add_to, "geometry"] = union_all( - [geometries_df["geometry"][poly_to_add_to], this_hole] - ) + poly_to_add_to = list(set(this_hole_boundaries_df["target"]).difference({-1}))[0] + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union([geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) elif len(segments(this_hole.boundary)) == 3: # If the hole is a simple triangle if len(set(this_hole_boundaries_df["target"]).difference({-1})) == 3: @@ -983,49 +743,27 @@ def smart_close_gaps(geometries_df, holes_df): # the centroid, especially for long skinny triangles.) this_hole_incenter = incenter(this_hole) for thb_ind in this_hole_boundaries_df.index: - g_ind = this_hole_boundaries_df["target"][thb_ind] - this_segment = this_hole_boundaries_df["geometry"][thb_ind] - this_segment_poly_to_add = make_valid( - Polygon( - [ - this_segment.boundary.geoms[0], - this_segment.boundary.geoms[1], - this_hole_incenter, - ] - ) - ) - geometries_df.at[g_ind, "geometry"] = union_all( - [geometries_df["geometry"][g_ind], this_segment_poly_to_add] - ) + g_ind = this_hole_boundaries_df.loc[thb_ind, "target"] + this_segment = this_hole_boundaries_df.loc[thb_ind, "geometry"] + this_segment_poly_to_add = make_valid(Polygon([this_segment.boundary.geoms[0], this_segment.boundary.geoms[1], this_hole_incenter])) + geometries_df.loc[g_ind, "geometry"] = unary_union([geometries_df.loc[g_ind, "geometry"], this_segment_poly_to_add]) else: # There are either 2 sides intersecting a common geometry or 1 # side intersecting an exterior boundary. In this case join the entire # triangle to the geometry that it shares the largest perimeter with. - touching_geoms = list( - set(this_hole_boundaries_df["target"]).difference({-1}) - ) - perim_1 = this_hole.intersection( - geometries_df["geometry"][touching_geoms[0]] - ).length - perim_2 = this_hole.intersection( - geometries_df["geometry"][touching_geoms[1]] - ).length + touching_geoms = list(set(this_hole_boundaries_df["target"]).difference({-1})) + perim_1 = this_hole.intersection(geometries_df.loc[touching_geoms[0], "geometry"]).length + perim_2 = this_hole.intersection(geometries_df.loc[touching_geoms[1], "geometry"]).length if perim_1 > perim_2: poly_to_add_to = touching_geoms[0] else: poly_to_add_to = touching_geoms[1] - geometries_df.at[poly_to_add_to, "geometry"] = union_all( - [geometries_df["geometry"][poly_to_add_to], this_hole] - ) + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union([geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) else: - this_hole_df = GeoDataFrame( - geometry=GeoSeries([this_hole]), crs=holes_df.crs - ) - this_hole_boundaries_df = construct_hole_boundaries( - geometries_df, this_hole_df - ) + this_hole_df = GeoDataFrame(geometry=GeoSeries([this_hole]), crs=holes_df.crs) + this_hole_boundaries_df = construct_hole_boundaries(geometries_df, this_hole_df) # If this_hole falls into one of the simple cases above, put it back # in the queue. (Note that after convexification, @@ -1033,305 +771,129 @@ def smart_close_gaps(geometries_df, holes_df): # boundaries is exterior and didn't get convexified.) if len(this_hole_boundaries_df) == 3: # Put the gap boundaries and target geometries into oriented order: - this_hole_boundaries = [this_hole_boundaries_df["geometry"][0]] - target_geometries = [this_hole_boundaries_df["target"][0]] - - if ( - this_hole_boundaries_df["geometry"][1].coords[0] - == this_hole_boundaries_df["geometry"][0].coords[-1] - ): - this_hole_boundaries.append(this_hole_boundaries_df["geometry"][1]) - target_geometries.append(this_hole_boundaries_df["target"][1]) - this_hole_boundaries.append(this_hole_boundaries_df["geometry"][2]) - target_geometries.append(this_hole_boundaries_df["target"][2]) - elif ( - this_hole_boundaries_df["geometry"][2].coords[0] - == this_hole_boundaries_df["geometry"][0].coords[-1] - ): - this_hole_boundaries.append(this_hole_boundaries_df["geometry"][2]) - target_geometries.append(this_hole_boundaries_df["target"][2]) - this_hole_boundaries.append(this_hole_boundaries_df["geometry"][1]) - target_geometries.append(this_hole_boundaries_df["target"][1]) + this_hole_boundaries = [this_hole_boundaries_df.loc[0, "geometry"]] + target_geometries = [this_hole_boundaries_df.loc[0, "target"]] + + if this_hole_boundaries_df.loc[1, "geometry"].coords[0] == this_hole_boundaries_df.loc[0, "geometry"].coords[-1]: + this_hole_boundaries.append(this_hole_boundaries_df.loc[1, "geometry"]) + target_geometries.append(this_hole_boundaries_df.loc[1, "target"]) + this_hole_boundaries.append(this_hole_boundaries_df.loc[2, "geometry"]) + target_geometries.append(this_hole_boundaries_df.loc[2, "target"]) + elif this_hole_boundaries_df.loc[2, "geometry"].coords[0] == this_hole_boundaries_df.loc[0, "geometry"].coords[-1]: + this_hole_boundaries.append(this_hole_boundaries_df.loc[2, "geometry"]) + target_geometries.append(this_hole_boundaries_df.loc[2, "target"]) + this_hole_boundaries.append(this_hole_boundaries_df.loc[1, "geometry"]) + target_geometries.append(this_hole_boundaries_df.loc[1, "target"]) # If one of the boundaries is an exterior region boundary, find # the shortest path between the vertex that isn't one of its # endpoints and the nearest point in this boundary, and divide # the hole between the other two adjacent geometries along this path. - # Otherwise, for each of the three boundary endpoints, construct - # the angle bisector of the two adjacent line segments and extend - # this line beyond the extent of the hole. Intersections of these - # 3 line segments will determine the endpoints of the new boundaries. - if -1 in target_geometries: ext_boundary_position = target_geometries.index(-1) # Cyclically permute so that the exterior boundary is in the # 1st position: - this_hole_boundaries = ( - this_hole_boundaries[ext_boundary_position:] - + this_hole_boundaries[0:ext_boundary_position] - ) - target_geometries = ( - target_geometries[ext_boundary_position:] - + target_geometries[0:ext_boundary_position] - ) + this_hole_boundaries = this_hole_boundaries[ext_boundary_position:] + this_hole_boundaries[0:ext_boundary_position] + target_geometries = target_geometries[ext_boundary_position:] + target_geometries[0:ext_boundary_position] main_vertex = Point(this_hole_boundaries[2].coords[0]) - nearest_ext_boundary_point = nearest_points( - main_vertex, extract_unique_points(this_hole_boundaries[0]) - )[1] + nearest_ext_boundary_point = nearest_points(main_vertex, extract_unique_points(this_hole_boundaries[0]))[1] - ext_boundary_points = list( - extract_unique_points(this_hole_boundaries[0]).geoms - ) - nearest_point_position = ext_boundary_points.index( - nearest_ext_boundary_point - ) + ext_boundary_points = list(extract_unique_points(this_hole_boundaries[0]).geoms) + nearest_point_position = ext_boundary_points.index(nearest_ext_boundary_point) if nearest_point_position == 0: # Add the entire hole to target_geometries[1]. - geometries_df.at[target_geometries[1], "geometry"] = union_all( - [geometries_df["geometry"][target_geometries[1]], this_hole] - ) + geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], this_hole]) elif nearest_point_position == len(ext_boundary_points) - 1: # Add the entire hole to target_geometries[2]. - geometries_df.at[target_geometries[2], "geometry"] = union_all( - [geometries_df["geometry"][target_geometries[2]], this_hole] - ) + geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], this_hole]) else: this_hole_triangulation = triangulate_polygon(this_hole) - sp = LineString( - shortest_path_in_polygon( - this_hole, - main_vertex, - nearest_ext_boundary_point, - full_triangulation=this_hole_triangulation, - ) - ) - - poly1_to_add_boundary = union_all( - [ - this_hole_boundaries[1], - sp, - LineString( - ext_boundary_points[nearest_point_position:] - ), - ] - ) + sp = LineString(shortest_path_in_polygon(this_hole, main_vertex, nearest_ext_boundary_point, full_triangulation=this_hole_triangulation)) + + poly1_to_add_boundary = unary_union([this_hole_boundaries[1], sp, LineString(ext_boundary_points[nearest_point_position:])]) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.at[target_geometries[1], "geometry"] = union_all( - [ - geometries_df["geometry"][target_geometries[1]], - poly1_to_add, - ] - ) - - poly2_to_add_boundary = union_all( - [ - this_hole_boundaries[2], - sp, - LineString( - ext_boundary_points[0 : nearest_point_position + 1] - ), - ] - ) - poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.at[target_geometries[2], "geometry"] = union_all( - [ - geometries_df["geometry"][target_geometries[2]], - poly2_to_add, - ] - ) + geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], poly1_to_add]) + poly2_to_add_boundary = unary_union([this_hole_boundaries[2], sp, LineString(ext_boundary_points[0:nearest_point_position+1])]) + poly2_to_add = polygonize(poly2_to_add_boundary)[0] + geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], poly2_to_add]) + + # Otherwise, construct the incenter of the circumscribing triangle. + # If the incenter is in the interior of the hole, construct shortest paths + # from the incenter to each of the vertices and divide the hole accordingly. + # If not, identify the closest hole boundary to the incenter, and divide + # the hole between the OTHER two boundaries as if this boundary had no + # adjoining geometry. else: - max_line_length = this_hole.boundary.length / 2 - vertices = [] - bisectors = [] - - for i in range(3): - this_vertex = numpy.array(this_hole_boundaries[i].coords[0]) - vertices.append(Point(this_hole_boundaries[i].coords[0])) - this_vec_1_raw = ( - numpy.array(this_hole_boundaries[i].coords[1]) - this_vertex - ) - this_vec_2_raw = ( - numpy.array(this_hole_boundaries[i - 1].coords[-2]) - - this_vertex - ) - this_unit_vec_1 = this_vec_1_raw / math.sqrt( - this_vec_1_raw[0] ** 2 + this_vec_1_raw[1] ** 2 - ) - this_unit_vec_2 = this_vec_2_raw / math.sqrt( - this_vec_2_raw[0] ** 2 + this_vec_2_raw[1] ** 2 - ) - this_bisector_vec_raw = this_unit_vec_1 + this_unit_vec_2 - this_bisector_unit_vec = this_bisector_vec_raw / math.sqrt( - this_bisector_vec_raw[0] ** 2 - + this_bisector_vec_raw[1] ** 2 - ) - this_bisector = LineString( - [ - tuple(this_vertex), - tuple( - this_vertex - + max_line_length * this_bisector_unit_vec - ), - ] - ) - bisectors.append(this_bisector) - - # Points of intersection of the bisectors: - i_points = [ - bisectors[0].intersection(bisectors[1]), - bisectors[1].intersection(bisectors[2]), - bisectors[2].intersection(bisectors[0]), - ] - - # Note that these points could coincide - e.g., if the convexified - # hole is a triangle - and the rest of the construction would be very - # simple. - # Also - even though this is geometrically impossible(!), - # rounding errors can create a situation in which two - # of these points are equal but different from the 3rd. - # In this case, assume that the one that appears twice - # is actually the common value for all three. - - if i_points[0] == i_points[1] or i_points[0] == i_points[2]: - # Construct pieces to append to geometries and append them. - middle_point = i_points[0] - for i in range(3): - poly_to_add_boundary = union_all( - [ - this_hole_boundaries[i], - LineString( - [ - this_hole_boundaries[i].coords[-1], - middle_point, - this_hole_boundaries[i].coords[0], - ] - ), - ] - ) - poly_to_add = polygonize(poly_to_add_boundary)[0] - geometries_df["geometry"][target_geometries[i]] = union_all( - [ - geometries_df["geometry"][target_geometries[i]], - poly_to_add, - ] - ) - - elif i_points[1] == i_points[2]: - # Construct pieces to append to geometries and append them. - middle_point = i_points[1] + main_vertices = [this_hole_boundaries[i].boundary.geoms[0] for i in range(3)] + + this_hole_hull = Polygon(main_vertices) + this_hole_hull_incenter = incenter(this_hole_hull) + + if this_hole.contains(this_hole_hull_incenter): + this_hole_triangulation = triangulate_polygon(this_hole) + incenter_triangle = [poly for poly in this_hole_triangulation if poly.contains(this_hole_hull_incenter) or poly.boundary.contains(this_hole_hull_incenter)][0] + incenter_triangle_vertices = extract_unique_points(incenter_triangle.boundary).geoms + incenter_segments = [LineString([this_hole_hull_incenter, point]) for point in incenter_triangle_vertices] + this_hole_partition = polygonize(unary_union([this_hole.boundary] + incenter_segments)) + + paths_to_main_vertices = [] for i in range(3): - poly_to_add_boundary = union_all( - [ - this_hole_boundaries[i], - LineString( - [ - this_hole_boundaries[i].coords[-1], - middle_point, - this_hole_boundaries[i].coords[0], - ] - ), - ] - ) - poly_to_add = polygonize(poly_to_add_boundary)[0] - geometries_df["geometry"][target_geometries[i]] = union_all( - [ - geometries_df["geometry"][target_geometries[i]], - poly_to_add, - ] - ) - - else: - # In general, each bisector intersects the other two - # bisectors in distinct points. To accurately construct - # the path to the more distant one, we need to include - # the nearer one as an intermediate point. - # And we might as well go ahead and find the incenter of the - # triangle formed by the intersection points, and include it - # on the path to the more distant one so we can completely - # fill the hole without a separate step. - middle_point = incenter(Polygon(i_points)) - - # The first bisector contains the 1st and 3rd intersection points. - if vertices[0].distance(i_points[0]) > vertices[0].distance( - i_points[2] - ): - v0_to_i01_path = LineString( - [vertices[0], i_points[2], middle_point, i_points[0]] - ) - v0_to_i02_path = LineString([vertices[0], i_points[2]]) - else: - v0_to_i01_path = LineString([vertices[0], i_points[0]]) - v0_to_i02_path = LineString( - [vertices[0], i_points[0], middle_point, i_points[2]] - ) - - # The second bisector contains the 1st and 2nd intersection points. - if vertices[1].distance(i_points[0]) > vertices[1].distance( - i_points[1] - ): - v1_to_i01_path = LineString( - [vertices[1], i_points[1], middle_point, i_points[0]] - ) - v1_to_i12_path = LineString([vertices[1], i_points[1]]) - else: - v1_to_i01_path = LineString([vertices[1], i_points[0]]) - v1_to_i12_path = LineString( - [vertices[1], i_points[0], middle_point, i_points[1]] - ) - - # The third bisector contains the 2nd and 3rd intersection points. - if vertices[2].distance(i_points[1]) > vertices[2].distance( - i_points[2] - ): - v2_to_i12_path = LineString( - [vertices[2], i_points[2], middle_point, i_points[1]] - ) - v2_to_i02_path = LineString([vertices[2], i_points[2]]) - else: - v2_to_i12_path = LineString([vertices[2], i_points[1]]) - v2_to_i02_path = LineString( - [vertices[2], i_points[1], middle_point, i_points[2]] - ) - - # Construct and adjoin new polygon pieces one at a time. - poly0_to_add_boundary = union_all( - [this_hole_boundaries[0], v0_to_i01_path, v1_to_i01_path] - ) + sub_hole = [poly for poly in this_hole_partition if poly.boundary.contains(main_vertices[i])][0] + paths_to_main_vertices.append(LineString(shortest_path_in_polygon(sub_hole, this_hole_hull_incenter, main_vertices[i]))) + + poly0_to_add_boundary = unary_union([this_hole_boundaries[0], paths_to_main_vertices[0], paths_to_main_vertices[1]]) poly0_to_add = polygonize(poly0_to_add_boundary)[0] - geometries_df.at[target_geometries[0], "geometry"] = union_all( - [ - geometries_df["geometry"][target_geometries[0]], - poly0_to_add, - ] - ) - - poly1_to_add_boundary = union_all( - [this_hole_boundaries[1], v1_to_i12_path, v2_to_i12_path] - ) + geometries_df.loc[target_geometries[0], "geometry"] = unary_union([geometries_df.loc[target_geometries[0], "geometry"], poly0_to_add]) + + poly1_to_add_boundary = unary_union([this_hole_boundaries[1], paths_to_main_vertices[1], paths_to_main_vertices[2]]) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.at[target_geometries[1], "geometry"] = union_all( - [ - geometries_df["geometry"][target_geometries[1]], - poly1_to_add, - ] - ) - - poly2_to_add_boundary = union_all( - [this_hole_boundaries[2], v2_to_i02_path, v0_to_i02_path] - ) + geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], poly1_to_add]) + + poly2_to_add_boundary = unary_union([this_hole_boundaries[2], paths_to_main_vertices[2], paths_to_main_vertices[0]]) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.at[target_geometries[2], "geometry"] = union_all( - [ - geometries_df["geometry"][target_geometries[2]], - poly2_to_add, - ] - ) + geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], poly2_to_add]) + + else: + incenter_boundary_dists = [this_hole_boundaries[i].distance(this_hole_hull_incenter) for i in range(3)] + + min_dist_position = incenter_boundary_dists.index(min(incenter_boundary_dists)) + this_hole_boundaries = this_hole_boundaries[min_dist_position:] + this_hole_boundaries[0:min_dist_position] + target_geometries = target_geometries[min_dist_position:] + target_geometries[0:min_dist_position] + + main_vertex = Point(this_hole_boundaries[2].coords[0]) + opp_boundary_int_points = list(extract_unique_points(this_hole_boundaries[0]).geoms)[1:-1] + nearest_opp_boundary_int_point = nearest_points(main_vertex, MultiPoint(opp_boundary_int_points))[1] + + opp_boundary_points = list(extract_unique_points(this_hole_boundaries[0]).geoms) + nearest_point_position = opp_boundary_points.index(nearest_opp_boundary_int_point) + + #if nearest_point_position == 0: + # # Add the entire hole to target_geometries[1]. + # geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], this_hole]) + + #elif nearest_point_position == len(ext_boundary_points) - 1: + # # Add the entire hole to target_geometries[2]. + # geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], this_hole]) + + #else: + + this_hole_triangulation = triangulate_polygon(this_hole) + sp = LineString(shortest_path_in_polygon(this_hole, main_vertex, nearest_opp_boundary_int_point, full_triangulation=this_hole_triangulation)) + + poly1_to_add_boundary = unary_union([this_hole_boundaries[1], sp, LineString(opp_boundary_points[nearest_point_position:])]) + poly1_to_add = polygonize(poly1_to_add_boundary)[0] + geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], poly1_to_add]) + poly2_to_add_boundary = unary_union([this_hole_boundaries[2], sp, LineString(opp_boundary_points[0:nearest_point_position+1])]) + poly2_to_add = polygonize(poly2_to_add_boundary)[0] + geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], poly2_to_add]) + + else: # If len(this_hole_boundaries_df) >= 4 this_hole_triangulation = triangulate_polygon(this_hole) thb_distances = [] @@ -1339,28 +901,21 @@ def smart_close_gaps(geometries_df, holes_df): for i in this_hole_boundaries_df.index: for j in this_hole_boundaries_df.index: if j > i: - this_distance = this_hole_boundaries_df["geometry"][ - i - ].distance(this_hole_boundaries_df["geometry"][j]) + this_distance = this_hole_boundaries_df.loc[i, "geometry"].distance(this_hole_boundaries_df.loc[j, "geometry"]) if this_distance != 0: thb_distances.append((i, j, this_distance)) - thb_distance_data_sorted = deque( - sorted(thb_distances, key=lambda tup: tup[2]) - ) + thb_distance_data_sorted = deque(sorted(thb_distances, key=lambda tup: tup[2])) found_triangles = False while found_triangles is False and len(thb_distance_data_sorted) > 0: boundary_distance_data = thb_distance_data_sorted.popleft() - boundaries_to_connect = ( - boundary_distance_data[0], - boundary_distance_data[1], - ) + boundaries_to_connect = (boundary_distance_data[0], boundary_distance_data[1]) - nhb1 = this_hole_boundaries_df["geometry"][boundaries_to_connect[0]] - nhb2 = this_hole_boundaries_df["geometry"][boundaries_to_connect[1]] - geom1 = this_hole_boundaries_df["target"][boundaries_to_connect[0]] - geom2 = this_hole_boundaries_df["target"][boundaries_to_connect[1]] + nhb1 = this_hole_boundaries_df.loc[boundaries_to_connect[0], "geometry"] + nhb2 = this_hole_boundaries_df.loc[boundaries_to_connect[1], "geometry"] + geom1 = this_hole_boundaries_df.loc[boundaries_to_connect[0], "target"] + geom2 = this_hole_boundaries_df.loc[boundaries_to_connect[1], "target"] # Construct the shortest paths between # (1) initial points of both boundaries; @@ -1369,6 +924,9 @@ def smart_close_gaps(geometries_df, holes_df): # hole boundary segments, but generically---and provably for at # at leat one non-adjacent pair---at a single interior point of # the hole. + # IF THE POINT IS IN THE INTERIOR, REPLACE IT WITH THE NEAREST + # POINT ON THE BOUNDARY TO MINIMIZE ROUNDING ERRORS CREATED BY + # INTRODUCING NEW POINTS! # In the generic case, these paths together with the two # hole boundaries will form a pair of "triangles" that each share a # boundary of positive length with one of the two hole boundaries. @@ -1398,42 +956,23 @@ def smart_close_gaps(geometries_df, holes_df): geom_int = geom1 point1 = nhb_int.boundary.geoms[0] point2 = nhb_int.boundary.geoms[1] - nearest_ext_boundary_point = nearest_points( - nhb_int, extract_unique_points(nhb_ext) - )[1] - path1 = LineString( - shortest_path_in_polygon( - this_hole, - point1, - nearest_ext_boundary_point, - full_triangulation=this_hole_triangulation, - ) - ) - path2 = LineString( - shortest_path_in_polygon( - this_hole, - point2, - nearest_ext_boundary_point, - full_triangulation=this_hole_triangulation, - ) - ) - polys_to_add_boundary = shapely.node( - MultiLineString([nhb_int, path1, path2]) - ) + nearest_ext_boundary_point = nearest_points(nhb_int, extract_unique_points(nhb_ext))[1] + path1 = LineString(shortest_path_in_polygon(this_hole, point1, nearest_ext_boundary_point, full_triangulation=this_hole_triangulation)) + path2 = LineString(shortest_path_in_polygon(this_hole, point2, nearest_ext_boundary_point, full_triangulation=this_hole_triangulation)) + polys_to_add_boundary = shapely.node(MultiLineString([nhb_int, path1, path2])) polys_to_add = polygonize(polys_to_add_boundary) + + hole_partition_boundary = shapely.node(MultiLineString(list(this_hole_boundaries_df["geometry"]) + [path1, path2])) + hole_partition_polys = polygonize(hole_partition_boundary) + if len(polys_to_add) > 0: for poly_to_add in polys_to_add: if poly_to_add.area > 0: found_triangles = True - geometries_df.at[geom_int, "geometry"] = ( - union_all( - [ - geometries_df["geometry"][geom_int], - poly_to_add, - ] - ) - ) - this_hole = this_hole.difference(poly_to_add) + geometries_df.loc[geom_int, "geometry"] = unary_union([geometries_df.loc[geom_int, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + #this_hole = this_hole.difference(poly_to_add) else: # Start by constructing the shortest paths between the initial point @@ -1446,26 +985,9 @@ def smart_close_gaps(geometries_df, holes_df): point21 = nhb2.boundary.geoms[0] point22 = nhb2.boundary.geoms[1] - test_path1_vertices = shortest_path_in_polygon( - this_hole, - point11, - point22, - full_triangulation=this_hole_triangulation, - ) - test_path2_vertices = shortest_path_in_polygon( - this_hole, - point12, - point21, - full_triangulation=this_hole_triangulation, - ) - if ( - len( - set(test_path1_vertices).intersection( - set(test_path2_vertices) - ) - ) - == 0 - ): + test_path1_vertices = shortest_path_in_polygon(this_hole, point11, point22, full_triangulation=this_hole_triangulation) + test_path2_vertices = shortest_path_in_polygon(this_hole, point12, point21, full_triangulation=this_hole_triangulation) + if len(set(test_path1_vertices).intersection(set(test_path2_vertices))) == 0: # In this case we should be good to add triangles formed # by crossing paths between the initial and terminal # points between the two boundaries! @@ -1478,47 +1000,29 @@ def smart_close_gaps(geometries_df, holes_df): found_triangles = True if geom1 == geom2: - path1 = LineString( - shortest_path_in_polygon( - this_hole, - point11, - point22, - full_triangulation=this_hole_triangulation, - ) - ) - path2 = LineString( - shortest_path_in_polygon( - this_hole, - point12, - point21, - full_triangulation=this_hole_triangulation, - ) - ) + path1 = LineString(shortest_path_in_polygon(this_hole, point11, point22, full_triangulation=this_hole_triangulation)) + path2 = LineString(shortest_path_in_polygon(this_hole, point12, point21, full_triangulation=this_hole_triangulation)) else: - path1 = LineString( - shortest_path_in_polygon( - this_hole, - point11, - point21, - full_triangulation=this_hole_triangulation, - ) - ) - path2 = LineString( - shortest_path_in_polygon( - this_hole, - point12, - point22, - full_triangulation=this_hole_triangulation, - ) - ) - - polys_to_add_boundary = shapely.node( - MultiLineString([nhb1, nhb2, path1, path2]) - ) + path1 = LineString(shortest_path_in_polygon(this_hole, point11, point21, full_triangulation=this_hole_triangulation)) + path2 = LineString(shortest_path_in_polygon(this_hole, point12, point22, full_triangulation=this_hole_triangulation)) + + polys_to_add_boundary = shapely.node(MultiLineString([nhb1, nhb2, path1, path2])) polys_to_add = polygonize(polys_to_add_boundary) + + hole_partition_boundary = shapely.node(MultiLineString(list(this_hole_boundaries_df["geometry"]) + [path1, path2])) + hole_partition_polys = polygonize(hole_partition_boundary) # polys_to_add will consist of either 1 or 2 polygons, - # each sharing a positive-length boundary witha unique geometry. + # each sharing a positive-length boundary with exactly one of + # geom1, geom2. # Add each polygon to the geometry that it shares a boundary with. + +# FIX: In rare cases, taking the difference of this_hole and poly_to_add goes wrong due to +# some precision problem in GEOS. Avoid this by polygonizing the hole boundary along with +# the new boundaries and replacing the hole with the unary union of the pieces that are +# NOT the triangles we want to remove. + + + nhb1_segments = segments(nhb1) nhb2_segments = segments(nhb2) for poly_to_add in polys_to_add: @@ -1526,100 +1030,73 @@ def smart_close_gaps(geometries_df, holes_df): # Cover all bases with both possible orientations for # boundary segments, even though the proper orientation # SHOULD always be correct. - poly_segments_oriented = segments( - poly_to_add.boundary - ) - poly_segments_reverse = [ - shapely.reverse(segment) - for segment in poly_segments_oriented - ] - poly_segments_all = set( - poly_segments_oriented + poly_segments_reverse - ) - if ( - len( - set(nhb1_segments).intersection( - poly_segments_all - ) - ) - > 0 - ) and ( - len( - set(nhb2_segments).intersection( - poly_segments_all - ) - ) - == 0 - ): - geometries_df.at[geom1, "geometry"] = union_all( - [ - geometries_df["geometry"][geom1], - poly_to_add, - ] - ) - this_hole = this_hole.difference(poly_to_add) - - elif ( - len( - set(nhb1_segments).intersection( - poly_segments_all - ) - ) - == 0 - ) and ( - len( - set(nhb2_segments).intersection( - poly_segments_all - ) - ) - > 0 - ): - geometries_df.at[geom2, "geometry"] = union_all( - [ - geometries_df["geometry"][geom2], - poly_to_add, - ] - ) - this_hole = this_hole.difference(poly_to_add) + poly_segments_oriented = segments(poly_to_add.boundary) + poly_segments_reverse = [shapely.reverse(segment) for segment in poly_segments_oriented] + poly_segments_all = set(poly_segments_oriented + poly_segments_reverse) + if (len(set(nhb1_segments).intersection(poly_segments_all)) > 0) and (len(set(nhb2_segments).intersection(poly_segments_all)) == 0): + geometries_df.loc[geom1, "geometry"] = unary_union([geometries_df.loc[geom1, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + #this_hole = this_hole.difference(poly_to_add) + + elif (len(set(nhb1_segments).intersection(poly_segments_all)) == 0) and (len(set(nhb2_segments).intersection(poly_segments_all)) > 0): + geometries_df.loc[geom2, "geometry"] = unary_union([geometries_df.loc[geom2, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + #this_hole = this_hole.difference(poly_to_add) elif geom1 == geom2: - geometries_df.at[geom1, "geometry"] = union_all( - [ - geometries_df["geometry"][geom1], - poly_to_add, - ] - ) - this_hole = this_hole.difference(poly_to_add) - - else: - print( - "Internal triangle construction went weird!" - ) - print("Hole boundaries:") - for i in this_hole_boundaries_df.index: - print( - "Target:", - this_hole_boundaries_df["target"][i], - ) - print( - list( - this_hole_boundaries_df["geometry"][ - i - ].coords - ) - ) - print("poly_to_add boundaries:") - print(list(poly_to_add.boundary.coords)) + geometries_df.loc[geom1, "geometry"] = unary_union([geometries_df.loc[geom1, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + #this_hole = this_hole.difference(poly_to_add) + + # It's possible with this new construction that the boundary of + # poly_to_add could intersect both nhb1 and nhb2 nontrivially. + # In this case, join it to the one that it intersects with + # longer perimeter. + + elif (len(set(nhb1_segments).intersection(poly_segments_all)) > 0) and (len(set(nhb2_segments).intersection(poly_segments_all)) > 0): + print("It happened!") + perim1 = linemerge(list(set(nhb1_segments).intersection(poly_segments_all))).length + perim2 = linemerge(list(set(nhb2_segments).intersection(poly_segments_all))).length + if perim1 > perim2: + geometries_df.loc[geom1, "geometry"] = unary_union([geometries_df.loc[geom1, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + else: + geometries_df.loc[geom2, "geometry"] = unary_union([geometries_df.loc[geom2, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + + + + + #print("Internal triangle construction went weird!") + #print(len(set(nhb1_segments).intersection(poly_segments_all)), len(set(nhb2_segments).intersection(poly_segments_all))) + #print(geom1, geom2) + #print("Temp crossing point:", list(temp_crossing_pt.coords)) + #print("Crossing point:", list(crossing_pt.coords)) + #print("Paths:", [list(x.coords) for x in paths]) + #print("Hole boundaries:") + #for i in this_hole_boundaries_df.index: + # print("Target:", this_hole_boundaries_df.loc[i, "target"]) + # print(list(this_hole_boundaries_df.loc[i, "geometry"].coords)) + #print("poly_to_add boundaries:") + #print(list(poly_to_add.boundary.coords)) # Now put the new hole(s) created by removing triangles back in the queue: - if found_triangles and not this_hole.is_empty: - if this_hole.geom_type == "MultiPolygon": # 2 holes to add - holes_to_add = [orient(geom) for geom in this_hole.geoms] - elif this_hole.geom_type == "Polygon": # 1 hole to add - holes_to_add = [orient(this_hole)] + if found_triangles and len(hole_partition_polys) > 0: + holes_to_add = [orient(poly) for poly in hole_partition_polys] holes_to_process.extend(holes_to_add) pbar_increment -= len(holes_to_add) +# if found_triangles and not this_hole.is_empty: +# if this_hole.geom_type == "MultiPolygon": # 2 holes to add +# holes_to_add = [orient(geom) for geom in this_hole.geoms] +# elif this_hole.geom_type == "Polygon": # 1 hole to add +# holes_to_add = [orient(this_hole)] +# holes_to_process.extend(holes_to_add) +# pbar_increment -= len(holes_to_add) + elif found_triangles is False: # This is rare, but it does happen occasionally in the scenario where # there's a large external boundary that, if it weren't external, @@ -1631,21 +1108,13 @@ def smart_close_gaps(geometries_df, holes_df): # anyway!) shared_perimeters = [] for i in this_hole_boundaries_df.index: - if this_hole_boundaries_df["target"][i] != -1: - shared_perimeters.append( - ( - this_hole_boundaries_df["target"][i], - this_hole_boundaries_df["geometry"][i].length, - ) - ) + if this_hole_boundaries_df.loc[i, "target"] != -1: + shared_perimeters.append((this_hole_boundaries_df.loc[i, "target"], this_hole_boundaries_df.loc[i, "geometry"].length)) if len(shared_perimeters) > 0: - max_shared_perim = sorted( - shared_perimeters, key=lambda tup: tup[1] - )[-1] + max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] - geometries_df.at[poly_to_add_to, "geometry"] = union_all( - [geometries_df["geometry"][poly_to_add_to], this_hole] - ) + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + [geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) pbar.update(pbar_increment) @@ -1677,21 +1146,19 @@ def small_rook_to_queen(geometries_df, min_rook_length): # Get rid of point geometries, linemerge the MultiLineStrings, and then # explode into components. (Then get rid of points again.) for ind in small_adj_df.index: - if small_adj_df["geometry"][ind].geom_type == "GeometryCollection": - small_adj_list = list(small_adj_df["geometry"][ind].geoms) - small_adj_list_no_point = [ - x for x in small_adj_list if x.geom_type != "Point" - ] - small_adj_df.at[ind, "geometry"] = MultiLineString(small_adj_list_no_point) + if small_adj_df.loc[ind, "geometry"].geom_type == "GeometryCollection": + small_adj_list = list(small_adj_df.loc[ind, "geometry"].geoms) + small_adj_list_no_point = [x for x in small_adj_list if x.geom_type != "Point"] + small_adj_df.loc[ind, "geometry"] = MultiLineString(small_adj_list_no_point) - if small_adj_df["geometry"][ind].geom_type == "MultiLineString": - small_adj_df.at[ind, "geometry"] = linemerge(small_adj_df["geometry"][ind]) + if small_adj_df.loc[ind, "geometry"].geom_type == "MultiLineString": + small_adj_df.loc[ind, "geometry"] = linemerge(small_adj_df.loc[ind, "geometry"]) small_adj_df = small_adj_df.explode(index_parts=False).reset_index(drop=True) small_adj_df_indices_to_drop = [] for ind in small_adj_df.index: - if small_adj_df["geometry"][ind].geom_type == "Point": + if small_adj_df.loc[ind, "geometry"].geom_type == "Point": small_adj_df_indices_to_drop.append(ind) if len(small_adj_df_indices_to_drop) > 0: @@ -1701,11 +1168,9 @@ def small_rook_to_queen(geometries_df, min_rook_length): # We'll take their unary union later in case any of them overlap. disks_to_remove_list = [] for a_ind in small_adj_df.index: - this_adj = small_adj_df["geometry"][a_ind] + this_adj = small_adj_df.loc[a_ind, "geometry"] adj_diam = this_adj.length - fat_point_radius = ( - 0.6 * adj_diam - ) # slightly more than the radius from the midpoint to the endpoints + fat_point_radius = 0.6*adj_diam # slightly more than the radius from the midpoint to the endpoints endpoint1 = this_adj.coords[0] endpoint2 = this_adj.coords[-1] midpoint = LineString([endpoint1, endpoint2]).centroid @@ -1719,53 +1184,39 @@ def small_rook_to_queen(geometries_df, min_rook_length): polys_to_remove_list = disks_to_remove_list polys_to_remove_complete = False while polys_to_remove_complete is False: - all_polys_to_remove = union_all(polys_to_remove_list) - if ( - all_polys_to_remove.geom_type == "Polygon" - ): # if it's all one big polygon now + all_polys_to_remove = unary_union(polys_to_remove_list) + if all_polys_to_remove.geom_type == "Polygon": # if it's all one big polygon now merged_polys_to_remove_list = [all_polys_to_remove] else: merged_polys_to_remove_list = list(all_polys_to_remove.geoms) - convex_polys_to_remove_list = [ - shapely.convex_hull(x) for x in merged_polys_to_remove_list - ] + convex_polys_to_remove_list = [shapely.convex_hull(x) for x in merged_polys_to_remove_list] if len(convex_polys_to_remove_list) == 1: polys_to_remove_complete = True - elif union_all(convex_polys_to_remove_list).geom_type == "MultiPolygon": + elif unary_union(convex_polys_to_remove_list).geom_type == "MultiPolygon": # Note that if the unary union is a Polygon, then this next condition # below can't hold anyway and we want polys_to_remove_complete to remain # False. - if len(union_all(convex_polys_to_remove_list).geoms) == len( - convex_polys_to_remove_list - ): + if len(unary_union(convex_polys_to_remove_list).geoms) == len(convex_polys_to_remove_list): polys_to_remove_complete = True polys_to_remove_list = convex_polys_to_remove_list # Build an STRtree to use for finding intersecting geometries. g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict( - (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) - ) + g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) for a_ind in range(len(polys_to_remove_list)): poly_to_remove = polys_to_remove_list[a_ind] # Identify geometries that might intersect this polygon. - possible_geom_integer_indices = [ - *set(numpy.ndarray.flatten(g_spatial_index.query(poly_to_remove))) - ] - possible_geom_indices = [ - g_index_by_iloc[k] for k in possible_geom_integer_indices - ] + possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(poly_to_remove)))] + possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] # Use the boundaries of these geometries together with the boundary of the disk to # polygonize and divide geometries into pieces inside and outside the disk. - boundaries = [ - geometries_df["geometry"][i].boundary for i in possible_geom_indices - ] + boundaries = [geometries_df.loc[i, "geometry"].boundary for i in possible_geom_indices] boundaries.append(LineString(list(poly_to_remove.exterior.coords))) boundaries_exploded = [] @@ -1776,38 +1227,21 @@ def small_rook_to_queen(geometries_df, min_rook_length): boundaries_exploded += list(geom.geoms) boundaries_union = shapely.node(MultiLineString(boundaries_exploded)) - pieces_df = GeoDataFrame( - columns=["polygon indices"], - geometry=GeoSeries(list(polygonize(boundaries_union))), - crs=geometries_df.crs, - ) + pieces_df = GeoDataFrame(columns=["polygon indices"], + geometry=GeoSeries(list(polygonize(boundaries_union))), + crs=geometries_df.crs) # Associate the pieces to the main geometries. (Note that if there are # gaps, some pieces may be unassigned.) - for i in pieces_df.index: - pieces_df.at[i, "polygon indices"] = set() + pieces_df["polygon indices"] = [set() for x in range(len(pieces_df.index))] for i in pieces_df.index: - temp_possible_geom_integer_indices = [ - *set( - numpy.ndarray.flatten( - g_spatial_index.query(pieces_df["geometry"][i]) - ) - ) - ] - temp_possible_geom_indices = [ - g_index_by_iloc[k] for k in temp_possible_geom_integer_indices - ] + temp_possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(pieces_df.loc[i, "geometry"])))] + temp_possible_geom_indices = [g_index_by_iloc[k] for k in temp_possible_geom_integer_indices] for j in temp_possible_geom_indices: - if ( - pieces_df["geometry"][i] - .representative_point() - .intersects(geometries_df["geometry"][j]) - ): - pieces_df.at[i, "polygon indices"] = pieces_df[ - "polygon indices" - ][i].union({j}) + if pieces_df.loc[i, "geometry"].representative_point().intersects(geometries_df.loc[j, "geometry"]): + pieces_df.loc[i, "polygon indices"] = pieces_df.loc[i, "polygon indices"].union({j}) # Now rebuild the disk from the pieces that are inside the circle, and drop them from # pieces_df. Then we'll give the pieces outside the circle back to the geometries that they came from. @@ -1816,75 +1250,47 @@ def small_rook_to_queen(geometries_df, min_rook_length): pieces_df_indices_to_drop = [] for p_ind in pieces_df.index: - if ( - pieces_df["geometry"][p_ind] - .representative_point() - .intersects(poly_to_remove) - ): - poly_to_remove_refined = union_all( - [poly_to_remove_refined, pieces_df["geometry"][p_ind]] - ) + if pieces_df.loc[p_ind, "geometry"].representative_point().intersects(poly_to_remove): + poly_to_remove_refined = unary_union([poly_to_remove_refined, pieces_df.loc[p_ind, "geometry"]]) pieces_df_indices_to_drop.append(p_ind) if len(pieces_df_indices_to_drop) > 0: pieces_df = pieces_df.drop(pieces_df_indices_to_drop) for g_ind in possible_geom_indices: - geometries_df.at[g_ind, "geometry"] = Polygon() + geometries_df.loc[g_ind, "geometry"] = Polygon() for p_ind in pieces_df.index: - if ( - len(pieces_df["polygon indices"][p_ind]) == 1 - ): # Note that it won't be >1 if the file is clean! - this_poly_ind = list(pieces_df["polygon indices"][p_ind])[0] - this_piece = pieces_df["geometry"][p_ind] + if len(pieces_df.loc[p_ind, "polygon indices"]) == 1: # Note that it won't be >1 if the file is clean! + this_poly_ind = list(pieces_df.loc[p_ind, "polygon indices"])[0] + this_piece = pieces_df.loc[p_ind, "geometry"] if this_poly_ind in possible_geom_indices: # This check is needed because the geometries in possible_geom_incides can form a # non-simply-connected region, in which case the interior holes - which may consist # of multiple geometries each - may be assigned someplace they shouldn't be! - geometries_df.at[this_poly_ind, "geometry"] = union_all( - [geometries_df["geometry"][this_poly_ind], this_piece] - ) + geometries_df.loc[this_poly_ind, "geometry"] = unary_union([geometries_df.loc[this_poly_ind, "geometry"], this_piece]) # Find the boundary arcs between geometries and poly_to_remove_refined (and make sure each arc is a connected piece): possible_geoms = geometries_df.loc[possible_geom_indices] - poly_to_remove_boundaries_df = intersections( - GeoDataFrame( - geometry=GeoSeries([poly_to_remove_refined], crs=geometries_df.crs) - ), - possible_geoms, - output_type="geodataframe", - ) + poly_to_remove_boundaries_df = intersections(GeoDataFrame(geometry=GeoSeries([poly_to_remove_refined], crs=geometries_df.crs)), possible_geoms, output_type="geodataframe") + poly_to_remove_boundaries_df = poly_to_remove_boundaries_df[poly_to_remove_boundaries_df.length > 0] for b_ind in poly_to_remove_boundaries_df.index: - if ( - poly_to_remove_boundaries_df["geometry"][b_ind].geom_type - == "MultiLineString" - ): - poly_to_remove_boundaries_df.at[b_ind, "geometry"] = linemerge( - poly_to_remove_boundaries_df["geometry"][b_ind] - ) - - poly_to_remove_boundaries_df = poly_to_remove_boundaries_df.explode( - index_parts=False - ).reset_index(drop=True) + if poly_to_remove_boundaries_df.loc[b_ind, "geometry"].geom_type == "MultiLineString": + poly_to_remove_boundaries_df.loc[b_ind, "geometry"] = linemerge(poly_to_remove_boundaries_df.loc[b_ind, "geometry"]) + + poly_to_remove_boundaries_df = poly_to_remove_boundaries_df.explode(index_parts=False).reset_index(drop=True) poly_to_remove_centroid_coords = poly_to_remove_refined.centroid.coords[0] # For each boundary arc, create a "pie wedge" from the center of poly_to_remove_refined # subtending this arc. (Since the polygon is convex, these are guaranteed to piece # together nicely.) for b_ind in poly_to_remove_boundaries_df.index: - boundary_arc_coords = list( - poly_to_remove_boundaries_df["geometry"][b_ind].coords - ) - boundary_wedge_coords = boundary_arc_coords + [ - poly_to_remove_centroid_coords - ] + boundary_arc_coords = list(poly_to_remove_boundaries_df.loc[b_ind, "geometry"].coords) + boundary_wedge_coords = boundary_arc_coords + [poly_to_remove_centroid_coords] - g_ind = poly_to_remove_boundaries_df["target"][b_ind] + g_ind = poly_to_remove_boundaries_df.loc[b_ind, "target"] - geometries_df.at[g_ind, "geometry"] = union_all( - [geometries_df["geometry"][g_ind], Polygon(boundary_wedge_coords)] - ) + geometries_df.loc[g_ind, "geometry"] = unary_union([geometries_df.loc[g_ind, "geometry"], Polygon(boundary_wedge_coords)]) return geometries_df @@ -1900,45 +1306,33 @@ def construct_hole_boundaries(geometries_df, holes_df): # Be sure gaps are correctly oriented: for h_ind in holes_df.index: - holes_df.at[h_ind, "geometry"] = orient(holes_df.geometry[h_ind]) + holes_df.loc[h_ind, "geometry"] = orient(holes_df.loc[h_ind, "geometry"]) # Do this WITHOUT using geometric intersection operations, which seem to be prone to # inexplicable rounding errors (GEOS bugs?) # Start by constructing an STRtree to find geometries that may intersect gaps. g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict( - (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) - ) + g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) # Initialize the geodataframe for the gap boundaries - hole_boundaries_df = GeoDataFrame( - columns=["source", "target"], geometry=GeoSeries([]), crs=geometries_df.crs - ) + hole_boundaries_df = GeoDataFrame(columns=["source", "target"], geometry=GeoSeries([]), crs=geometries_df.crs) # For each gap and each geometry that it might possibly intersect, find all # common LineStrings in their boundaries (if any) and take their unary union to # construct the appropriate boundary between them. (Note that this requires paying # VERY careful attention to orientations!) for h_ind in holes_df.index: - this_hole = holes_df["geometry"][h_ind] + this_hole = holes_df.loc[h_ind, "geometry"] this_hole_segments = segments(this_hole.boundary) this_hole_segments_used = [] - possible_geom_integer_indices = [ - *set( - numpy.ndarray.flatten( - g_spatial_index.query(holes_df["geometry"][h_ind]) - ) - ) - ] - possible_geom_indices = [ - g_index_by_iloc[k] for k in possible_geom_integer_indices - ] + possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(holes_df.loc[h_ind, "geometry"])))] + possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for g_ind in possible_geom_indices: - this_geom = geometries_df["geometry"][g_ind] + this_geom = geometries_df.loc[g_ind, "geometry"] if this_geom.geom_type == "Polygon": this_geom_geoms = [orient(this_geom)] elif this_geom.geom_type == "MultiPolygon": @@ -1956,49 +1350,26 @@ def construct_hole_boundaries(geometries_df, holes_df): for component in this_geom_boundary_components: this_geom_segments = this_geom_segments.union(set(segments(component))) - this_hole_this_geom_segments = [ - segment - for segment in this_hole_segments - if ( - segment in this_geom_segments - or shapely.reverse(segment) in this_geom_segments - ) - ] + this_hole_this_geom_segments = [segment for segment in this_hole_segments if (segment in this_geom_segments or shapely.reverse(segment) in this_geom_segments)] if len(this_hole_this_geom_segments) > 0: this_hole_segments_used += this_hole_this_geom_segments - this_hole_boundary_df = GeoDataFrame( - geometry=GeoSeries([linemerge(this_hole_this_geom_segments)]), - crs=geometries_df.crs, - ) + this_hole_boundary_df = GeoDataFrame(geometry=GeoSeries([linemerge(this_hole_this_geom_segments)]), crs=geometries_df.crs) this_hole_boundary_df.insert(0, "source", h_ind) this_hole_boundary_df.insert(1, "target", g_ind) - hole_boundaries_df = pandas.concat( - [hole_boundaries_df, this_hole_boundary_df] - ).reset_index(drop=True) + hole_boundaries_df = pandas.concat([hole_boundaries_df, this_hole_boundary_df]).reset_index(drop=True) # Finally, check for any exterior boundary: if len(this_hole_segments) > len(this_hole_segments_used): - exterior_segments = [ - segment - for segment in this_hole_segments - if segment not in this_hole_segments_used - ] - this_hole_exterior_boundary_df = GeoDataFrame( - geometry=GeoSeries([linemerge(exterior_segments)]), - crs=geometries_df.crs, - ) + exterior_segments = [segment for segment in this_hole_segments if segment not in this_hole_segments_used] + this_hole_exterior_boundary_df = GeoDataFrame(geometry=GeoSeries([linemerge(exterior_segments)]), crs=geometries_df.crs) this_hole_exterior_boundary_df.insert(0, "source", h_ind) this_hole_exterior_boundary_df.insert(1, "target", -1) - hole_boundaries_df = pandas.concat( - [hole_boundaries_df, this_hole_exterior_boundary_df] - ).reset_index(drop=True) + hole_boundaries_df = pandas.concat([hole_boundaries_df, this_hole_exterior_boundary_df]).reset_index(drop=True) - hole_boundaries_df = hole_boundaries_df.explode(index_parts=False).reset_index( - drop=True - ) + hole_boundaries_df = hole_boundaries_df.explode(index_parts=False).reset_index(drop=True) return hole_boundaries_df @@ -2026,21 +1397,18 @@ def incenter(triangle): # The incenter will be a weighted average of the coordinates of the vertices, # with coefficients proportional to a,b,c. - alpha = a / (a + b + c) - beta = b / (a + b + c) - gamma = c / (a + b + c) + alpha = a/(a + b + c) + beta = b/(a + b + c) + gamma = c/(a + b + c) - x_i = alpha * x_a + beta * x_b + gamma * x_c - y_i = alpha * y_a + beta * y_b + gamma * y_c + x_i = alpha*x_a + beta*x_b + gamma*x_c + y_i = alpha*y_a + beta*y_b + gamma*y_c # Occasionally for very tiny triangles, rounding errors produce a point not # contained in the triangle. In this case, replace the computed point with # the nearest vertex of the triangle. if not triangle.contains(Point(x_i, y_i)): - point_to_return = nearest_points( - Point(x_i, y_i), - MultiPoint([Point(x_a, y_a), Point(x_b, y_b), Point(x_c, y_c)]), - )[1] + point_to_return = nearest_points(Point(x_i, y_i), MultiPoint([Point(x_a, y_a), Point(x_b, y_b), Point(x_c, y_c)]))[1] else: point_to_return = Point(x_i, y_i) @@ -2061,18 +1429,12 @@ def triangulate_polygon(polygon): # Find an ear to cut from the polygon and add it to the list of triangles. for i in range(len(poly_vertices)): - triangle_to_check = Polygon( - [poly_vertices[i - 1], poly_vertices[i], poly_vertices[i + 1]] - ) - if ( - poly.contains(triangle_to_check) - and LineString([poly_vertices[i - 1], poly_vertices[i + 1]]) - .intersection(poly.boundary) - .difference(MultiPoint([poly_vertices[i - 1], poly_vertices[i + 1]])) - .is_empty - ): + triangle_to_check = Polygon([poly_vertices[i-1], poly_vertices[i], poly_vertices[i+1]]) + if poly.contains(triangle_to_check) and MultiPoint([poly_vertices[i-1], poly_vertices[i+1]]).contains(LineString([poly_vertices[i-1], poly_vertices[i+1]]).intersection(poly.boundary)): + #if poly.contains(triangle_to_check) and LineString([poly_vertices[i-1], poly_vertices[i+1]]).intersection(poly.boundary).difference(MultiPoint([poly_vertices[i-1], poly_vertices[i+1]])).is_empty: triangles.append(triangle_to_check) - poly = poly.difference(triangle_to_check) + poly_vertices_reordered = poly_vertices[i:] + poly_vertices[0:i] + poly = Polygon(poly_vertices_reordered[1:]) break # Remaining polygon is now a triangle, so add it to the list. @@ -2092,21 +1454,19 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): the same polygon. """ if not (polygon.is_valid and polygon.geom_type == "Polygon"): - raise TypeError( - "shortest_path_in_polygon: Input polygon must be a valid Polygon." - ) + raise TypeError("shortest_path_in_polygon: Input polygon must be a valid Polygon.") if not extract_unique_points(polygon).contains(MultiPoint([start, end])): - raise TypeError( - "shortest_path_in_polygon: Start and end points must be vertices of the polygon." - ) + raise TypeError("shortest_path_in_polygon: Start and end points must be vertices of the polygon.") # First check for the easy case: If the line segment between the start and end points is # contained in the polygon, then that's the shortest path. (And the rest of the algorithm # won't work correctly because the simplified polygon will degenerate.) + + if MultiPoint([start, end]).contains(LineString([start, end]).intersection(polygon.boundary)) and polygon.contains(LineString([start, end])): + #if polygon.contains(LineString([start, end])) and set(LineString([start, end]).intersection(polygon.boundary).geoms) == {start, end}: + return [start, end] - if polygon.contains(LineString([start, end])) or polygon.boundary.contains( - LineString([start, end]) - ): + elif LineString([start, end]) in segments(polygon.boundary) or LineString([end, start]) in segments(polygon.boundary): return [start, end] else: @@ -2120,25 +1480,17 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): start_index = boundary_points.index(start) end_index = boundary_points.index(end) if start_index < end_index: - path_1 = LineString(boundary_points[start_index : end_index + 1]) - path_2 = LineString( - boundary_points[end_index:] + boundary_points[0 : start_index + 1] - ) + path_1 = LineString(boundary_points[start_index:end_index+1]) + path_2 = LineString(boundary_points[end_index:] + boundary_points[0:start_index+1]) else: - path_1 = LineString( - boundary_points[start_index:] + boundary_points[0 : end_index + 1] - ) - path_2 = LineString(boundary_points[end_index : start_index + 1]) - - if (extract_unique_points(path_1).geoms[0] == start) and ( - extract_unique_points(path_2).geoms[0] == end - ): + path_1 = LineString(boundary_points[start_index:] + boundary_points[0:end_index+1]) + path_2 = LineString(boundary_points[end_index:start_index+1]) + + if (extract_unique_points(path_1).geoms[0] == start) and (extract_unique_points(path_2).geoms[0] == end): right_path = path_1 left_path = shapely.reverse(path_2) - elif (extract_unique_points(path_2).geoms[0] == start) and ( - extract_unique_points(path_1).geoms[0] == end - ): + elif (extract_unique_points(path_2).geoms[0] == start) and (extract_unique_points(path_1).geoms[0] == end): right_path = path_2 left_path = shapely.reverse(path_1) @@ -2153,40 +1505,25 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): triangulation = [] for triangle in full_triangulation: - if ( - not triangle.boundary.intersection( - MultiPoint(right_path_points[1:-1]) - ).is_empty - and not triangle.boundary.intersection( - MultiPoint(left_path_points[1:-1]) - ).is_empty - ): + if not triangle.boundary.intersection(MultiPoint(right_path_points[1:-1])).is_empty and not triangle.boundary.intersection(MultiPoint(left_path_points[1:-1])).is_empty: triangulation.append(triangle) # Put the triangles for the sleeve in the correct order: - initial_triangle = [ - triangle - for triangle in triangulation - if start in extract_unique_points(triangle.boundary).geoms - ][0] + initial_triangle = [triangle for triangle in triangulation if start in extract_unique_points(triangle.boundary).geoms][0] ordered_triangulation = [initial_triangle] triangulation.remove(initial_triangle) while len(triangulation) > 0: leading_triangle = ordered_triangulation[-1] - next_triangle = [ - triangle - for triangle in triangulation - if leading_triangle.intersection(triangle).geom_type == "LineString" - ][0] + next_triangle = [triangle for triangle in triangulation if leading_triangle.intersection(triangle).geom_type == "LineString"][0] ordered_triangulation.append(next_triangle) triangulation.remove(next_triangle) # Regard the sleeve given by the union of these triangles as the "simplified" # polygon; the shortest path must be contained in this simplfied polygon. - polygon_simplified = union_all(ordered_triangulation) - + polygon_simplified = unary_union(ordered_triangulation) + # Now use the ordered triangulation to order the vertices of the simplified polygon, # as well as the left and right paths restricted to the simplified polygon. ordered_path_vertices = [start] @@ -2195,12 +1532,8 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): for triangle in ordered_triangulation: this_triangle_vertices = set(extract_unique_points(triangle.boundary).geoms) - this_triangle_new_vertices = this_triangle_vertices.difference( - set(ordered_path_vertices) - ) - ordered_path_vertices = ordered_path_vertices + list( - this_triangle_new_vertices - ) + this_triangle_new_vertices = this_triangle_vertices.difference(set(ordered_path_vertices)) + ordered_path_vertices = ordered_path_vertices + list(this_triangle_new_vertices) for vertex in this_triangle_new_vertices: if vertex in right_path_points: right_path_simplified_points.append(vertex) @@ -2211,10 +1544,7 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): found_shortest_path = [start] left_funnel = [left_path_simplified_points[0], left_path_simplified_points[1]] - right_funnel = [ - right_path_simplified_points[0], - right_path_simplified_points[1], - ] + right_funnel = [right_path_simplified_points[0], right_path_simplified_points[1]] # We've already used the first 3 points on this list, so take them out. ordered_path_vertices = ordered_path_vertices[3:] @@ -2240,51 +1570,50 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): # vertex on the *other* funnel; it's guaranteed to be reflex. Make it # the new apex, and add the other funnel up to this point to # found_shortest_path. - if polygon_simplified.contains( - LineString([apex, point]) - ) or polygon_simplified.boundary.contains(LineString([apex, point])): - this_funnel = [apex, point] + + if polygon_simplified.contains(LineString([apex, point])) or polygon_simplified.boundary.contains(LineString([apex, point])): + new_funnel_points = [apex] + for i in range(1, len(this_funnel)): + if LineString([apex, point]).contains(LineString([this_funnel[i], point])): + new_funnel_points.append(this_funnel[i]) + this_funnel = new_funnel_points + [point] else: for i in range(1, len(this_funnel)): - if polygon_simplified.contains( - LineString([this_funnel[i], point]) - ) or polygon_simplified.boundary.contains( - LineString([this_funnel[i], point]) - ): + if polygon_simplified.contains(LineString([this_funnel[i], point])) or polygon_simplified.boundary.contains(LineString([this_funnel[i], point])): first_seen = i break - - seg1 = list( - LineString( - [this_funnel[first_seen - 1], this_funnel[first_seen]] - ).coords - ) + + seg1 = list(LineString([this_funnel[first_seen-1], this_funnel[first_seen]]).coords) seg2 = list(LineString([this_funnel[first_seen], point]).coords) vec1 = (seg1[1][0] - seg1[0][0], seg1[1][1] - seg1[0][1]) vec2 = (seg2[1][0] - seg2[0][0], seg2[1][1] - seg2[0][1]) - cross_prod = vec1[0] * vec2[1] - vec1[1] * vec2[0] + cross_prod = vec1[0]*vec2[1] - vec1[1]*vec2[0] - if cross_prod * reflex_sign >= 0: + if cross_prod*reflex_sign >= 0: # If this vertex is reflex: - this_funnel = this_funnel[0 : first_seen + 1] + [point] + new_funnel_points = this_funnel[0:first_seen+1] + for i in range(first_seen+1, len(this_funnel)): + if LineString([this_funnel[first_seen], point]).contains(LineString([this_funnel[i], point])): + new_funnel_points.append(this_funnel[i]) + this_funnel = new_funnel_points + [point] + else: - first_seen = min( - i - for i in range(1, len(other_funnel)) - if polygon_simplified.contains( - LineString([other_funnel[i], point]) - ) - or polygon_simplified.boundary.contains( - LineString([other_funnel[i], point]) - ) - ) - found_shortest_path += other_funnel[1 : first_seen + 1] + first_seen = min(i for i in range(1, len(other_funnel)) if polygon_simplified.contains(LineString([other_funnel[i], point])) or polygon_simplified.boundary.contains(LineString([other_funnel[i], point]))) + found_shortest_path += other_funnel[1: first_seen+1] apex = other_funnel[first_seen] + #new_funnel_points = [apex] + other_funnel_start_index = first_seen + for i in range(first_seen+1, len(other_funnel)): + if LineString([apex, point]).contains(LineString([other_funnel[i], point])): + found_shortest_path.append(other_funnel[i]) + apex = other_funnel[i] + other_funnel_start_index = i + this_funnel = [apex, point] - other_funnel = other_funnel[first_seen:] + other_funnel = other_funnel[other_funnel_start_index:] # Reassign this_funnel and other_funnel to left_funnel and right_funnel: if point in left_path_simplified_points: @@ -2304,13 +1633,11 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): def convexify_hole_boundaries(geometries_df, holes_df): """ Partially fill gaps as follows: - - 1. Assign any gap that only adjoins 1 geometry to that geometry. - 2. For each gap that adjoins at least 2 geometries, "convexify" the geometries - surrounding the gap by replacing the gap's boundary with each geometry by the - shortest path within the gap between its endpoints and "filling in" the - geometry up to the new boundary. (Exterior boundaries, if any, are left alone.) - + (1) Assign any gap that only adjoins 1 geometry to that geometry. + (2) For each gap that adjoins at least 2 geometries, "convexify" the geometries + surrounding the gap by replacing the gap's boundary with each geometry by the + shortest path within the gap between its endpoints and "filling in" the + geometry up to the new boundary. (Exterior boundaries, if any, are left alone.) If there are only 2 non-exterior (and no exterior) geometries intersecting the gap, this will fill the gap completely; otherwise it will usually leave one or more smaller gaps remaining. The convexity of the geometry boundaries will simplify @@ -2319,22 +1646,15 @@ def convexify_hole_boundaries(geometries_df, holes_df): geometries_df = geometries_df.copy() holes_df = holes_df.copy() - completed_holes_df = GeoDataFrame( - columns=["region"], geometry=GeoSeries([]), crs=holes_df.crs - ) + completed_holes_df = GeoDataFrame(columns=["region"], geometry=GeoSeries([]), crs=holes_df.crs) if len(holes_df) > 0: holes_to_process = deque(list(holes_df["geometry"])) - this_region = list(holes_df["region"])[ - 0 - ] # All holes in this dataframe should be from the same region + this_region = list(holes_df["region"])[0] # All holes in this dataframe should be from the same region if this_region is None: pbar = tqdm(desc="Gaps to simplify", total=len(holes_to_process)) else: - pbar = tqdm( - desc=f"Gaps to simplify in region {this_region}", - total=len(holes_to_process), - ) + pbar = tqdm(desc=f"Gaps to simplify in region {this_region}", total=len(holes_to_process)) else: holes_to_process = deque([]) pbar = tqdm(desc="Gaps to simplify", total=len(holes_to_process)) @@ -2350,20 +1670,12 @@ def convexify_hole_boundaries(geometries_df, holes_df): # This is probably a small component of a region that isn't assigned to # any geometry in that region. Just leave it alone and let it be a hole. if this_region is not None: - print( - "Found a component of the region at index", - this_region, - "that does not intersect any geometry assigned to that region.", - ) + print("Found a component of the region at index", this_region, "that does not intersect any geometry assigned to that region.") elif len(set(this_hole_boundaries_df["target"]).difference({-1})) == 1: # Attach the hole to the unique non-exterior geometry that it intersects: - poly_to_add_to = list( - set(this_hole_boundaries_df["target"]).difference({-1}) - )[0] - geometries_df.at[poly_to_add_to, "geometry"] = union_all( - [geometries_df["geometry"][poly_to_add_to], this_hole] - ) + poly_to_add_to = list(set(this_hole_boundaries_df["target"]).difference({-1}))[0] + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union([geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) else: # Each remaining hole intersects at least 2 geometries nontrivially. @@ -2377,49 +1689,52 @@ def convexify_hole_boundaries(geometries_df, holes_df): # after convexifying. If they do, their UNION isn't guaranteed to be # convexified at the end, so we'll put that hole back in the queue for # another round of processing. - if len(set(this_hole_boundaries_df["target"])) == len( - this_hole_boundaries_df - ): + if len(set(this_hole_boundaries_df["target"])) == len(this_hole_boundaries_df): target_repetition = False else: target_repetition = True repeated_targets = [] for target in set(this_hole_boundaries_df["target"]): - boundaries_this_target = this_hole_boundaries_df[ - this_hole_boundaries_df["target"] == target - ] + boundaries_this_target = this_hole_boundaries_df[this_hole_boundaries_df["target"] == target] if len(boundaries_this_target) > 1: repeated_targets.append((target, len(boundaries_this_target))) new_hole_in_progress = this_hole + + if new_hole_in_progress.boundary.geom_type == "MultiLineString": + print(new_hole_in_progress.geom_type) + print([list(x.coords) for x in new_hole_in_progress.boundary.geoms]) + this_hole_triangulation = triangulate_polygon(new_hole_in_progress) for thb_ind in this_hole_boundaries_df.index: - thb = this_hole_boundaries_df["geometry"][thb_ind] - this_geom = this_hole_boundaries_df["target"][thb_ind] + thb = this_hole_boundaries_df.loc[thb_ind, "geometry"] + this_geom = this_hole_boundaries_df.loc[thb_ind, "target"] - if this_geom != -1: + if this_geom != -1 and not new_hole_in_progress.is_empty: start = list(extract_unique_points(thb).geoms)[0] end = list(extract_unique_points(thb).geoms)[-1] - sp = LineString( - shortest_path_in_polygon( - this_hole, - start, - end, - full_triangulation=this_hole_triangulation, - ) - ) - - piece_to_add_boundary = union_all([thb, sp]) - if piece_to_add_boundary.geom_type == "MultiLineString": - piece_to_add_boundary = linemerge(piece_to_add_boundary) - - piece_to_add = union_all(polygonize(piece_to_add_boundary)) - geometries_df.at[this_geom, "geometry"] = union_all( - [geometries_df["geometry"][this_geom], piece_to_add] - ) - new_hole_in_progress = new_hole_in_progress.difference(piece_to_add) + sp = LineString(shortest_path_in_polygon(this_hole, start, end, full_triangulation=this_hole_triangulation)) + + polys_to_add_boundary = shapely.node(MultiLineString([thb, sp])) + hole_partition_boundary = shapely.node(unary_union([new_hole_in_progress.boundary, sp])) + #piece_to_add_boundary = unary_union([thb, sp]) + #if piece_to_add_boundary.geom_type == "MultiLineString": + # piece_to_add_boundary = linemerge(piece_to_add_boundary) + + polys_to_add = polygonize(polys_to_add_boundary) + hole_partition_polys = polygonize(hole_partition_boundary) + + for poly_to_add in polys_to_add: + geometries_df.loc[this_geom, "geometry"] = unary_union([geometries_df.loc[this_geom, "geometry"], poly_to_add]) + hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + + new_hole_in_progress = unary_union(hole_partition_polys) + + #piece_to_add = unary_union(polygonize(piece_to_add_boundary)) + #geometries_df.loc[this_geom, "geometry"] = unary_union([geometries_df.loc[this_geom, "geometry"], piece_to_add]) + #new_hole_in_progress = new_hole_in_progress.difference(piece_to_add) if not new_hole_in_progress.is_empty: if new_hole_in_progress.geom_type == "Polygon": @@ -2429,9 +1744,7 @@ def convexify_hole_boundaries(geometries_df, holes_df): for new_hole in new_holes: new_hole = orient(new_hole) - new_hole_df = GeoDataFrame( - geometry=GeoSeries([new_hole]), crs=holes_df.crs - ) + new_hole_df = GeoDataFrame(geometry=GeoSeries([new_hole]), crs=holes_df.crs) new_hole_df.insert(0, "region", this_region) if target_repetition: @@ -2441,30 +1754,19 @@ def convexify_hole_boundaries(geometries_df, holes_df): # may have been concatenated after convexifying, resulting in a # non-convex boundary - so put the hole back in the queue for # another round of processing. - new_hole_boundaries_df = construct_hole_boundaries( - geometries_df, new_hole_df - ) + new_hole_boundaries_df = construct_hole_boundaries(geometries_df, new_hole_df) reprocess_hole = False for target in repeated_targets: - new_boundaries_this_target = new_hole_boundaries_df[ - new_hole_boundaries_df["target"] == target[0] - ] - if ( - len(new_boundaries_this_target) > 0 - and len(new_boundaries_this_target) < target[1] - ): + new_boundaries_this_target = new_hole_boundaries_df[new_hole_boundaries_df["target"] == target[0]] + if len(new_boundaries_this_target) > 0 and len(new_boundaries_this_target) < target[1]: reprocess_hole = True break if reprocess_hole: holes_to_process.append(new_hole) else: - completed_holes_df = pandas.concat( - [completed_holes_df, new_hole_df] - ).reset_index(drop=True) + completed_holes_df = pandas.concat([completed_holes_df, new_hole_df]).reset_index(drop=True) else: - completed_holes_df = pandas.concat( - [completed_holes_df, new_hole_df] - ).reset_index(drop=True) + completed_holes_df = pandas.concat([completed_holes_df, new_hole_df]).reset_index(drop=True) pbar.update(pbar_increment) From e3bf52fbf9c7a9d86697deb9c0871b6192a1cdad Mon Sep 17 00:00:00 2001 From: jnclelland Date: Fri, 15 Aug 2025 18:30:49 -0600 Subject: [PATCH 2/6] Minor changes to address Issues posted to github Added a warning when maup.assign leaves some source geometries unassigned, made some minor syntax changes suggested in github Issues. --- maup/assign.py | 8 +++++++- maup/indexed_geometries.py | 2 +- maup/smart_repair.py | 20 ++++++++++---------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/maup/assign.py b/maup/assign.py index 968fae8..539b52a 100644 --- a/maup/assign.py +++ b/maup/assign.py @@ -1,4 +1,5 @@ import pandas +import warnings from .indexed_geometries import IndexedGeometries from .intersections import intersections @@ -25,7 +26,12 @@ def assign(sources, targets): ) assignment.update(assignments_by_area) - # TODO: add a warning here if there are still unassigned source geometries. + # Warn here if there are still unassigned source geometries. + unassigned = sources[assignment.isna()] + if len(unassigned): # skip if done + warnings.warn("Warning: Some units in the source geometry were unassigned.") + + return assignment.astype(targets.index.dtype, errors="ignore") diff --git a/maup/indexed_geometries.py b/maup/indexed_geometries.py index 9f02abc..3ae4cb6 100644 --- a/maup/indexed_geometries.py +++ b/maup/indexed_geometries.py @@ -24,7 +24,7 @@ def query(self, geometry): # (2 x n) array instead of a (1 x n) array, so it's safest to flatten the query # output before proceeding. relevant_index_array = self.spatial_index.query(geometry) - relevant_indices = [*set(numpy.ndarray.flatten(relevant_index_array))] + relevant_indices = list(set(relevant_index_array.ravel())) relevant_geometries = self.geometries.iloc[relevant_indices] return relevant_geometries diff --git a/maup/smart_repair.py b/maup/smart_repair.py index 8a51a55..4b6afb5 100644 --- a/maup/smart_repair.py +++ b/maup/smart_repair.py @@ -263,7 +263,7 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, c_ind = component_areas_sorted[i][0] this_fragment = reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind] if component_areas_sorted[i][1] < disconnection_threshold*big_area: - possible_intersect_integer_indices = [*set(numpy.ndarray.flatten(spatial_index.query(this_fragment)))] + possible_intersect_integer_indices = list(set(spatial_index.query(this_fragment).ravel())) possible_intersect_indices = [(index_by_iloc[k]) for k in possible_intersect_integer_indices] if nest_within_regions is not None: @@ -415,7 +415,7 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None # Note that "None" is a possibility, and that each piece will belong to a unique # region because the regions GeoDataFrame/GeoSeries MUST be clean. if nest_within_regions is not None: - possible_region_integer_indices = [*set(numpy.ndarray.flatten(r_spatial_index.query(pieces_df.loc[i, "geometry"])))] + possible_region_integer_indices = list(set(r_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel())) possible_region_indices = [r_index_by_iloc[k] for k in possible_region_integer_indices] for j in possible_region_indices: @@ -426,7 +426,7 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None # contained in. If region boundaries are included, then while determining which # geometries each piece is contained in, omit any geometries that are # assigned to a region other than the one the piece is contained in. - possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(pieces_df.loc[i, "geometry"])))] + possible_geom_integer_indices = list(set(g_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel())) possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for j in possible_geom_indices: @@ -558,7 +558,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): o_index_by_iloc = dict((i, list(overlaps_df.index)[i]) for i in range(len(overlaps_df))) for g_ind in geometries_disconnected_df.index: - possible_overlap_integer_indices = [*set(numpy.ndarray.flatten(o_spatial_index.query(geometries_disconnected_df.loc[g_ind, "geometry"])))] + possible_overlap_integer_indices = list(set(o_spatial_index.query(geometries_disconnected_df.loc[g_ind, "geometry"]).ravel())) possible_overlap_indices_0 = [o_index_by_iloc[k] for k in possible_overlap_integer_indices] possible_overlap_indices = list(set(possible_overlap_indices_0) & set(overlaps_df_unused_indices)) @@ -593,7 +593,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): for o_ind in overlaps_df_unused_indices: this_overlap = overlaps_df.loc[o_ind, "geometry"] shared_perimeters = [] - possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(this_overlap)))] + possible_geom_integer_indices = list(set(g_spatial_index.query(this_overlap).ravel())) possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for g_ind in possible_geom_indices: @@ -616,7 +616,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): this_overlap = orphaned_overlaps[o_ind][0] this_overlap_polygon_indices = orphaned_overlaps[o_ind][1] shared_perimeters = [] - possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(this_overlap)))] + possible_geom_integer_indices = list(set(g_spatial_index.query(this_overlap).ravel())) possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for g_ind in possible_geom_indices: @@ -653,7 +653,7 @@ def drop_bad_holes(reconstructed_df, holes_df, fill_gaps_threshold): hole_indices_to_drop_aat = [] for h_ind in holes_df.index: this_hole = holes_df.loc[h_ind, "geometry"] - possible_intersect_integer_indices = [*set(numpy.ndarray.flatten(spatial_index.query(this_hole)))] + possible_intersect_integer_indices = list(set(spatial_index.query(this_hole).ravel())) possible_intersect_indices = [(index_by_iloc[k]) for k in possible_intersect_integer_indices] actual_intersect_indices = [g_ind for g_ind in possible_intersect_indices if not this_hole.intersection(reconstructed_df.loc[g_ind, "geometry"]).is_empty] @@ -1211,7 +1211,7 @@ def small_rook_to_queen(geometries_df, min_rook_length): poly_to_remove = polys_to_remove_list[a_ind] # Identify geometries that might intersect this polygon. - possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(poly_to_remove)))] + possible_geom_integer_indices = list(set(g_spatial_index.query(poly_to_remove).ravel())) possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] # Use the boundaries of these geometries together with the boundary of the disk to @@ -1236,7 +1236,7 @@ def small_rook_to_queen(geometries_df, min_rook_length): pieces_df["polygon indices"] = [set() for x in range(len(pieces_df.index))] for i in pieces_df.index: - temp_possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(pieces_df.loc[i, "geometry"])))] + temp_possible_geom_integer_indices = list(set(g_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel())) temp_possible_geom_indices = [g_index_by_iloc[k] for k in temp_possible_geom_integer_indices] for j in temp_possible_geom_indices: @@ -1327,7 +1327,7 @@ def construct_hole_boundaries(geometries_df, holes_df): this_hole_segments = segments(this_hole.boundary) this_hole_segments_used = [] - possible_geom_integer_indices = [*set(numpy.ndarray.flatten(g_spatial_index.query(holes_df.loc[h_ind, "geometry"])))] + possible_geom_integer_indices = list(set(g_spatial_index.query(holes_df.loc[h_ind, "geometry"]).ravel())) possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] for g_ind in possible_geom_indices: From a72c951be5a8383a1952acdee2d16f293eb3c528 Mon Sep 17 00:00:00 2001 From: peterrrock2 <27579114+peterrrock2@users.noreply.github.com> Date: Wed, 20 Aug 2025 07:42:06 -0600 Subject: [PATCH 3/6] Run black formatter --- docs/conf.py | 44 +- maup/__init__.py | 12 +- maup/adjacencies.py | 17 +- maup/assign.py | 13 +- maup/indexed_geometries.py | 4 +- maup/intersections.py | 4 +- maup/repair.py | 131 +- maup/smart_repair.py | 1922 +++++++++++++++++++++++------- poetry.lock | 111 +- pyproject.toml | 3 +- tests/test_indexed_geometries.py | 9 +- tests/test_intersections.py | 4 +- tests/test_smart_repair.py | 87 +- 13 files changed, 1807 insertions(+), 554 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index ec7fce6..19b6c53 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,24 +12,24 @@ # import os import sys -sys.path.insert(0, os.path.abspath('..')) +sys.path.insert(0, os.path.abspath("..")) # Add markdown parser source_suffix = { - '.rst': 'restructuredtext', - '.md': 'markdown', + ".rst": "restructuredtext", + ".md": "markdown", } # -- Project information ----------------------------------------------------- -project = 'maup' -copyright = '2023, MGGG' -author = 'Jeanne Clelland, Max Fan, Max Hully ' +project = "maup" +copyright = "2023, MGGG" +author = "Jeanne Clelland, Max Fan, Max Hully " # The full version, including alpha/beta/rc tags -release = '2.0.2' +release = "2.0.2" # -- General configuration --------------------------------------------------- @@ -38,31 +38,31 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', - 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', - 'sphinx_copybutton', + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx_copybutton", ] # apidoc -apidoc_module_dir = '../maup' -apidoc_output_dir = 'reference/api' -apidoc_excluded_paths = ['tests'] +apidoc_module_dir = "../maup" +apidoc_output_dir = "reference/api" +apidoc_excluded_paths = ["tests"] apidoc_separate_modules = True # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- @@ -92,7 +92,7 @@ # # html_sidebars = {} html_css_files = [ - 'css/custom.css', + "css/custom.css", ] diff --git a/maup/__init__.py b/maup/__init__.py index a09c902..db2e86c 100644 --- a/maup/__init__.py +++ b/maup/__init__.py @@ -3,7 +3,15 @@ from .assign import assign from .indexed_geometries import IndexedGeometries from .intersections import intersections, prorate -from .repair import close_gaps, resolve_overlaps, quick_repair, snap_to_grid, crop_to, expand_to, doctor +from .repair import ( + close_gaps, + resolve_overlaps, + quick_repair, + snap_to_grid, + crop_to, + expand_to, + doctor, +) from .smart_repair import smart_repair from .normalize import normalize from .progress_bar import progress @@ -32,5 +40,5 @@ "doctor", "smart_repair", "normalize", - "progress" + "progress", ] diff --git a/maup/adjacencies.py b/maup/adjacencies.py index 60ba85e..19397e3 100644 --- a/maup/adjacencies.py +++ b/maup/adjacencies.py @@ -29,7 +29,10 @@ def iter_adjacencies(geometries): def adjacencies( geometries, adjacency_type="rook", - output_type="geoseries", *, warn_for_overlaps=True, warn_for_islands=True + output_type="geoseries", + *, + warn_for_overlaps=True, + warn_for_islands=True ): """Returns adjacencies between geometries. The default return type is a @@ -56,7 +59,9 @@ def adjacencies( index, geoms = [[], []] if output_type == "geodataframe": - inters = GeoDataFrame({"neighbors" : index, "geometry" : geoms}, crs = geometries.crs) + inters = GeoDataFrame( + {"neighbors": index, "geometry": geoms}, crs=geometries.crs + ) else: inters = GeoSeries(geoms, index=index, crs=geometries.crs) @@ -75,9 +80,13 @@ def adjacencies( if warn_for_islands: if output_type == "geodataframe": - islands = set(geometries.index) - set(i for pair in inters["neighbors"] for i in pair) + islands = set(geometries.index) - set( + i for pair in inters["neighbors"] for i in pair + ) else: - islands = set(geometries.index) - set(i for pair in inters.index for i in pair) + islands = set(geometries.index) - set( + i for pair in inters.index for i in pair + ) if len(islands) > 0: warnings.warn( "Found islands.\n" "Indices of islands: {}".format(islands), diff --git a/maup/assign.py b/maup/assign.py index 539b52a..78d4942 100644 --- a/maup/assign.py +++ b/maup/assign.py @@ -12,26 +12,21 @@ def assign(sources, targets): target that covers it, or, if no target covers the entire source, the target that covers the most of its area. """ - assignment = pandas.Series( - assign_by_covering(sources, targets), - dtype="float" - ) + assignment = pandas.Series(assign_by_covering(sources, targets), dtype="float") assignment.name = None unassigned = sources[assignment.isna()] if len(unassigned): # skip if done assignments_by_area = pandas.Series( - assign_by_area(unassigned, targets), - dtype="float" + assign_by_area(unassigned, targets), dtype="float" ) assignment.update(assignments_by_area) # Warn here if there are still unassigned source geometries. unassigned = sources[assignment.isna()] if len(unassigned): # skip if done - warnings.warn("Warning: Some units in the source geometry were unassigned.") - - + warnings.warn("Warning: Some units in the source geometry were unassigned.") + return assignment.astype(targets.index.dtype, errors="ignore") diff --git a/maup/indexed_geometries.py b/maup/indexed_geometries.py index 3ae4cb6..fc340eb 100644 --- a/maup/indexed_geometries.py +++ b/maup/indexed_geometries.py @@ -63,7 +63,9 @@ def assign(self, targets): # covering units at the assign_by_area step ub maup.assign. groups_concat_index_list = list(groups_concat.index) seen = set() - bad_indices = list(set([x for x in groups_concat_index_list if x in seen or seen.add(x)])) + bad_indices = list( + set([x for x in groups_concat_index_list if x in seen or seen.add(x)]) + ) if len(bad_indices) > 0: groups_concat = groups_concat.drop(bad_indices) return groups_concat.reindex(self.index) diff --git a/maup/intersections.py b/maup/intersections.py index 894e745..0cca8ca 100644 --- a/maup/intersections.py +++ b/maup/intersections.py @@ -37,7 +37,9 @@ def intersections(sources, targets, output_type="geoseries", area_cutoff=None): ) ] - df = GeoDataFrame(records, columns=["source", "target", "geometry"], crs=sources.crs) + df = GeoDataFrame( + records, columns=["source", "target", "geometry"], crs=sources.crs + ) df = df.sort_values(by=["source", "target"]).reset_index(drop=True) geometries = df.set_index(["source", "target"]).geometry diff --git a/maup/repair.py b/maup/repair.py index 74595b9..5f76385 100644 --- a/maup/repair.py +++ b/maup/repair.py @@ -4,7 +4,13 @@ import pandas from geopandas import GeoSeries -from shapely.geometry import Polygon, MultiPolygon, LineString, MultiLineString, GeometryCollection +from shapely.geometry import ( + Polygon, + MultiPolygon, + LineString, + MultiLineString, + GeometryCollection, +) from shapely.ops import unary_union from .adjacencies import adjacencies @@ -57,8 +63,9 @@ def trim_valid(value): """ if isinstance(value, GeometryCollection): # List comprehension excluding non-Polygons - value = [item for item in value.geoms - if isinstance(item, (Polygon, MultiPolygon))] + value = [ + item for item in value.geoms if isinstance(item, (Polygon, MultiPolygon)) + ] # Re-aggregegating multiple Polygons into single MultiPolygon object. value = value[0] if len(value) == 1 else MultiPolygon(value) return value @@ -70,7 +77,9 @@ def holes_of_union(geometries): if not all( isinstance(geometry, (Polygon, MultiPolygon)) for geometry in geometries ): - raise TypeError(f"Must be a Polygon or MultiPolygon (got types {set([x.geom_type for x in geometries])})!") + raise TypeError( + f"Must be a Polygon or MultiPolygon (got types {set([x.geom_type for x in geometries])})!" + ) union = unary_union(geometries) series = holes(union) @@ -111,7 +120,10 @@ def close_gaps(geometries, relative_threshold=0.1, force_polygons=False): geometries = get_geometries(geometries) gaps = holes_of_union(geometries) return absorb_by_shared_perimeter( - gaps, geometries, relative_threshold=relative_threshold, force_polygons=force_polygons + gaps, + geometries, + relative_threshold=relative_threshold, + force_polygons=force_polygons, ) @@ -151,10 +163,15 @@ def resolve_overlaps(geometries, relative_threshold=0.1, force_polygons=False): to_remove = GeoSeries( pandas.concat([overlaps.droplevel(1), overlaps.droplevel(0)]), crs=overlaps.crs ) - with_overlaps_removed = geometries.apply(lambda x: x.difference(unary_union(to_remove))) + with_overlaps_removed = geometries.apply( + lambda x: x.difference(unary_union(to_remove)) + ) return absorb_by_shared_perimeter( - overlaps, with_overlaps_removed, relative_threshold=None, force_polygons=force_polygons + overlaps, + with_overlaps_removed, + relative_threshold=None, + force_polygons=force_polygons, ) @@ -172,7 +189,9 @@ def quick_repair(geometries, relative_threshold=0.1, force_polygons=False): For a more careful repair that takes adjacencies and higher-order overlaps between geometries into account, consider using smart_repair instead. """ - return autorepair(geometries, relative_threshold=relative_threshold, force_polygons=force_polygons) + return autorepair( + geometries, relative_threshold=relative_threshold, force_polygons=force_polygons + ) def autorepair(geometries, relative_threshold=0.1, force_polygons=False): @@ -194,16 +213,28 @@ def autorepair(geometries, relative_threshold=0.1, force_polygons=False): if force_polygons: geometries = make_valid_polygons(remove_repeated_vertices(geometries)) - geometries = make_valid_polygons(resolve_overlaps(geometries, - relative_threshold=relative_threshold, - force_polygons=force_polygons)) - geometries = make_valid_polygons(close_gaps(geometries, - relative_threshold=relative_threshold, - force_polygons=force_polygons)) + geometries = make_valid_polygons( + resolve_overlaps( + geometries, + relative_threshold=relative_threshold, + force_polygons=force_polygons, + ) + ) + geometries = make_valid_polygons( + close_gaps( + geometries, + relative_threshold=relative_threshold, + force_polygons=force_polygons, + ) + ) else: geometries = remove_repeated_vertices(geometries).make_valid() - geometries = resolve_overlaps(geometries, relative_threshold=relative_threshold).make_valid() - geometries = close_gaps(geometries, relative_threshold=relative_threshold).make_valid() + geometries = resolve_overlaps( + geometries, relative_threshold=relative_threshold + ).make_valid() + geometries = close_gaps( + geometries, relative_threshold=relative_threshold + ).make_valid() return geometries @@ -213,7 +244,9 @@ def remove_repeated_vertices(geometries): Removes repeated vertices. Vertices are considered to be repeated if they appear consecutively, excluding the start and end points. """ - return geometries.geometry.apply(lambda x: apply_func_to_polygon_parts(x, dedup_vertices)) + return geometries.geometry.apply( + lambda x: apply_func_to_polygon_parts(x, dedup_vertices) + ) def snap_to_grid(geometries, n=-7): @@ -231,15 +264,18 @@ def crop_to(source, target): Crops the source geometries to the target geometries. """ target_union = unary_union(get_geometries(target)) - cropped_geometries = get_geometries(source).apply(lambda x: x.intersection(target_union)) + cropped_geometries = get_geometries(source).apply( + lambda x: x.intersection(target_union) + ) if (cropped_geometries.area == 0).any(): - warnings.warn("Some cropped geometries have zero area, likely due to\n" + - "large differences in the union of the geometries in your\n" + - "source and target shapefiles. This may become an issue\n" + - "when maupping.\n", - AreaCroppingWarning - ) + warnings.warn( + "Some cropped geometries have zero area, likely due to\n" + + "large differences in the union of the geometries in your\n" + + "source and target shapefiles. This may become an issue\n" + + "when maupping.\n", + AreaCroppingWarning, + ) return cropped_geometries @@ -258,10 +294,15 @@ def expand_to(source, target, force_polygons=False): source_union = unary_union(geometries) leftover_geometries = get_geometries(target).apply(lambda x: x - source_union) - leftover_geometries = leftover_geometries[~leftover_geometries.is_empty].explode(index_parts=False) + leftover_geometries = leftover_geometries[~leftover_geometries.is_empty].explode( + index_parts=False + ) geometries = absorb_by_shared_perimeter( - leftover_geometries, get_geometries(source), relative_threshold=None, force_polygons=force_polygons + leftover_geometries, + get_geometries(source), + relative_threshold=None, + force_polygons=force_polygons, ) return geometries @@ -297,7 +338,9 @@ def doctor(source, target=None, silent=False, accept_holes=False): health_check = False for shp in shapefiles: - if not shp.geometry.apply(lambda x: isinstance(x, (Polygon, MultiPolygon))).all(): + if not shp.geometry.apply( + lambda x: isinstance(x, (Polygon, MultiPolygon)) + ).all(): if silent is False: print("Some rows do not have geometries.") health_check = False @@ -346,7 +389,9 @@ def apply_func_to_polygon_parts(shape, func): elif isinstance(shape, MultiPolygon): return MultiPolygon([func(poly) for poly in shape.geoms]) else: - raise TypeError(f"Can only apply {func} to a Polygon or MultiPolygon (got {shape} with type {type(shape)})!") + raise TypeError( + f"Can only apply {func} to a Polygon or MultiPolygon (got {shape} with type {type(shape)})!" + ) def dedup_vertices(polygon): @@ -381,16 +426,31 @@ def dedup_vertices(polygon): def snap_polygon_to_grid(polygon, n=-7): if len(polygon.interiors) == 0: - return Polygon([(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords]) + return Polygon( + [(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords] + ) else: - return Polygon([(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords], holes=[[(round(x, -n), round(y, -n)) for x, y in interior_ring.coords] for interior_ring in polygon.interiors]) + return Polygon( + [(round(x, -n), round(y, -n)) for x, y in polygon.exterior.coords], + holes=[ + [(round(x, -n), round(y, -n)) for x, y in interior_ring.coords] + for interior_ring in polygon.interiors + ], + ) def snap_multilinestring_to_grid(multilinestring, n=-7): if multilinestring.geom_type == "LineString": - return LineString([(round(x, -n), round(y, -n)) for x, y in multilinestring.coords]) + return LineString( + [(round(x, -n), round(y, -n)) for x, y in multilinestring.coords] + ) elif multilinestring.geom_type == "MultiLineString": - return MultiLineString([LineString([(round(x, -n), round(y, -n)) for x, y in linestring.coords]) for linestring in multilinestring.geoms]) + return MultiLineString( + [ + LineString([(round(x, -n), round(y, -n)) for x, y in linestring.coords]) + for linestring in multilinestring.geoms + ] + ) def split_by_level(series, multiindex): @@ -401,7 +461,9 @@ def split_by_level(series, multiindex): @require_same_crs -def absorb_by_shared_perimeter(sources, targets, relative_threshold=None, force_polygons=False): +def absorb_by_shared_perimeter( + sources, targets, relative_threshold=None, force_polygons=False +): if len(sources) == 0: return targets @@ -422,7 +484,8 @@ def absorb_by_shared_perimeter(sources, targets, relative_threshold=None, force_ assignment = assignment[under_threshold] sources_to_absorb = GeoSeries( - sources.groupby(assignment).apply(unary_union), crs=sources.crs, + sources.groupby(assignment).apply(unary_union), + crs=sources.crs, ) # Note that the following line produces a warning message when sources_to_absorb diff --git a/maup/smart_repair.py b/maup/smart_repair.py index 4b6afb5..936c043 100644 --- a/maup/smart_repair.py +++ b/maup/smart_repair.py @@ -10,7 +10,14 @@ from shapely import make_valid, extract_unique_points from shapely.strtree import STRtree from shapely.ops import unary_union, polygonize, linemerge, nearest_points -from shapely.geometry import Polygon, MultiPolygon, Point, MultiPoint, LineString, MultiLineString +from shapely.geometry import ( + Polygon, + MultiPolygon, + Point, + MultiPoint, + LineString, + MultiLineString, +) from shapely.geometry.polygon import orient from tqdm import tqdm, TqdmWarning @@ -21,7 +28,7 @@ from .progress_bar import progress from .repair import doctor, snap_to_grid, snap_multilinestring_to_grid -warnings.filterwarnings('ignore', 'GeoSeries.isna', UserWarning) +warnings.filterwarnings("ignore", "GeoSeries.isna", UserWarning) warnings.filterwarnings("ignore", category=TqdmWarning) pandas.options.mode.chained_assignment = None @@ -39,9 +46,16 @@ ######### -def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, - fill_gaps_threshold=0.1, disconnection_threshold=0.0001, - nest_within_regions=None, min_rook_length=None): +def smart_repair( + geometries_df, + snapped=True, + snap_precision=9, + fill_gaps=True, + fill_gaps_threshold=0.1, + disconnection_threshold=0.0001, + nest_within_regions=None, + min_rook_length=None, +): """ Repairs topology issues (overlaps, gaps, invalid polygons) in a geopandas GeoDataFrame or GeoSeries, with an emphasis on preserving intended adjacency @@ -88,17 +102,22 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, geometries_df = geometries_df.copy() geometries0_df = geometries_df.copy() else: - raise TypeError("Input geometries must be in the form of a geopandas GeoSeries or GeoDataFrame.") + raise TypeError( + "Input geometries must be in the form of a geopandas GeoSeries or GeoDataFrame." + ) # Ensure that geometries are 2-D and not 3-D: for i in geometries_df.index: geometries_df.loc[i, "geometry"] = shapely.wkb.loads( - shapely.wkb.dumps(geometries_df.loc[i, "geometry"], output_dimension=2)) + shapely.wkb.dumps(geometries_df.loc[i, "geometry"], output_dimension=2) + ) # Ensure that crs is not geographic: if geometries_df.crs is not None: if geometries_df.crs.is_geographic: - raise Exception("Input geometries must be in a projected, non-geographic CRS. To project a GeoDataFrame 'gdf' to UTM, use 'gdf = gdf.to_crs(gdf.estimate_utm_crs())' ") + raise Exception( + "Input geometries must be in a projected, non-geographic CRS. To project a GeoDataFrame 'gdf' to UTM, use 'gdf = gdf.to_crs(gdf.estimate_utm_crs())' " + ) # If nest_within_regions is not None, require it to have the same CRS as the main shapefile # and set regions_df equal to a GeoDataFrame version. @@ -111,12 +130,18 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, elif isinstance(nest_within_regions, GeoDataFrame): regions_df = nest_within_regions.copy() else: - raise TypeError("nest_within_regions must be a geopandas GeoSeries or GeoDataFrame.") + raise TypeError( + "nest_within_regions must be a geopandas GeoSeries or GeoDataFrame." + ) if nest_within_regions.crs != geometries_df.crs: - raise Exception("nest_within_regions must be in the same CRS as the geometries being repaired.") + raise Exception( + "nest_within_regions must be in the same CRS as the geometries being repaired." + ) if doctor(nest_within_regions, silent=True, accept_holes=True) is False: - raise Exception("nest_within_regions must be topologically clean---i.e., all geometries must be valid and there must be no overlaps between geometries. Generally the best source for region shapefiles is the U.S. Census Burueau.") + raise Exception( + "nest_within_regions must be topologically clean---i.e., all geometries must be valid and there must be no overlaps between geometries. Generally the best source for region shapefiles is the U.S. Census Burueau." + ) # Before doing anything else, make sure all polygons are valid, convert any empty # geometries to empty Polygons to avoid type errors, and remove any LineStrings and @@ -126,7 +151,13 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, if geometries_df.loc[i, "geometry"] is None: geometries_df.loc[i, "geometry"] = Polygon() if geometries_df.loc[i, "geometry"].geom_type == "GeometryCollection": - geometries_df.loc[i, "geometry"] = unary_union([x for x in geometries_df.loc[i, "geometry"].geoms if x.geom_type in ("Polygon", "MultiPolygon")]) + geometries_df.loc[i, "geometry"] = unary_union( + [ + x + for x in geometries_df.loc[i, "geometry"].geoms + if x.geom_type in ("Polygon", "MultiPolygon") + ] + ) # If snapped is True, snap all polygon vertices to a grid of size no more than # 10^(-snap_precision) times the max of width/height of the entire extent of the input. @@ -134,38 +165,67 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, # This avoids a rare "non-noded intersection" error due to a GEOS bug and leaves # several orders of magnitude for additional intersection operations before hitting # python's precision limit of about 10^(-15). - - # Do this is two steps: first snap the original vertices to a grid of size + + # Do this is two steps: first snap the original vertices to a grid of size # 10^(-snap_precision) times the max of width/height of the entire extent of the input. - # Then in the building blocks function snap the points of intersection to a grid of + # Then in the building blocks function snap the points of intersection to a grid of # size 10^(-snap_precision-1) times the max of width/height of the entire extent of the input. if snapped: # These bounds are in the form (xmin, ymin, xmax, ymax) geometries_total_bounds = geometries_df.total_bounds - largest_bound = max(geometries_total_bounds[2] - geometries_total_bounds[0], geometries_total_bounds[3] - geometries_total_bounds[1]) + largest_bound = max( + geometries_total_bounds[2] - geometries_total_bounds[0], + geometries_total_bounds[3] - geometries_total_bounds[1], + ) snap_magnitude = int(math.log10(largest_bound)) - snap_precision - geometries_df["geometry"] = snap_to_grid(geometries_df["geometry"], n=snap_magnitude) + geometries_df["geometry"] = snap_to_grid( + geometries_df["geometry"], n=snap_magnitude + ) if nest_within_regions is not None: - regions_df["geometry"] = snap_to_grid(regions_df["geometry"], n=snap_magnitude) + regions_df["geometry"] = snap_to_grid( + regions_df["geometry"], n=snap_magnitude + ) # Snapping could possibly have created some invalid polygons, so do another round # of validity checks - and do a validity check for regions as well, if applicable. for i in geometries_df.index: - geometries_df.loc[i, "geometry"] = make_valid(geometries_df.loc[i, "geometry"]) + geometries_df.loc[i, "geometry"] = make_valid( + geometries_df.loc[i, "geometry"] + ) if geometries_df.loc[i, "geometry"].geom_type == "GeometryCollection": - geometries_df.loc[i, "geometry"] = unary_union([x for x in geometries_df.loc[i, "geometry"].geoms if x.geom_type in ("Polygon", "MultiPolygon")]) + geometries_df.loc[i, "geometry"] = unary_union( + [ + x + for x in geometries_df.loc[i, "geometry"].geoms + if x.geom_type in ("Polygon", "MultiPolygon") + ] + ) if nest_within_regions is not None: for i in regions_df.index: - regions_df.loc[i, "geometry"] = make_valid(regions_df.loc[i, "geometry"]) + regions_df.loc[i, "geometry"] = make_valid( + regions_df.loc[i, "geometry"] + ) if regions_df.loc[i, "geometry"].geom_type == "GeometryCollection": - regions_df.loc[i, "geometry"] = unary_union([x for x in regions_df.loc[i, "geometry"].geoms if x.geom_type in ("Polygon", "MultiPolygon")]) - print("Snapping all geometries to a grid with precision 10^(", snap_magnitude, ") to avoid GEOS errors.") + regions_df.loc[i, "geometry"] = unary_union( + [ + x + for x in regions_df.loc[i, "geometry"].geoms + if x.geom_type in ("Polygon", "MultiPolygon") + ] + ) + print( + "Snapping all geometries to a grid with precision 10^(", + snap_magnitude, + ") to avoid GEOS errors.", + ) else: snap_magnitude = None # Construct data about overlaps of all orders, plus holes. - overlap_tower, holes_df = building_blocks(geometries_df, snap_magnitude=snap_magnitude, nest_within_regions=regions_df) + overlap_tower, holes_df = building_blocks( + geometries_df, snap_magnitude=snap_magnitude, nest_within_regions=regions_df + ) # Use data from the overlap tower to rebuild geometries with no overlaps. # If nest_within_regions is not None, resolve overlaps and fill holes (if applicable) @@ -181,11 +241,19 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, # Also remove any non-simply connected holes since our algorithm breaks # down in that case, regardless of whether or not a relative area # threshold has been set. - holes_df, num_holes_dropped_nsc, num_holes_dropped_aat = drop_bad_holes(reconstructed_df, holes_df, fill_gaps_threshold=fill_gaps_threshold) + holes_df, num_holes_dropped_nsc, num_holes_dropped_aat = drop_bad_holes( + reconstructed_df, holes_df, fill_gaps_threshold=fill_gaps_threshold + ) if num_holes_dropped_aat > 0: - print(num_holes_dropped_aat, "gaps will remain unfilled, because they exceed the area threshold.") + print( + num_holes_dropped_aat, + "gaps will remain unfilled, because they exceed the area threshold.", + ) if num_holes_dropped_nsc > 0: - print(num_holes_dropped_nsc, "gaps will remain unfilled, because they are not simply connected.") + print( + num_holes_dropped_nsc, + "gaps will remain unfilled, because they are not simply connected.", + ) print("Filling gaps...") reconstructed_df = smart_close_gaps(reconstructed_df, holes_df) @@ -197,17 +265,29 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, print("Resolving overlaps...") reconstructed_df = geometries_df.copy() - geometries_to_regions_assignment = assign(geometries_df.geometry, regions_df.geometry) + geometries_to_regions_assignment = assign( + geometries_df.geometry, regions_df.geometry + ) for r_ind in nest_within_regions.index: - geometries_this_region_indices = [g_ind for g_ind in geometries_df.index if geometries_to_regions_assignment[g_ind] == r_ind] - geometries_this_region_df = geometries_df.loc[geometries_this_region_indices] + geometries_this_region_indices = [ + g_ind + for g_ind in geometries_df.index + if geometries_to_regions_assignment[g_ind] == r_ind + ] + geometries_this_region_df = geometries_df.loc[ + geometries_this_region_indices + ] overlap_tower_this_region = [] for i in range(len(overlap_tower)): - overlap_tower_this_region.append(overlap_tower[i][overlap_tower[i]["region"] == r_ind]) + overlap_tower_this_region.append( + overlap_tower[i][overlap_tower[i]["region"] == r_ind] + ) - reconstructed_this_region_df = reconstruct_from_overlap_tower(geometries_this_region_df, overlap_tower_this_region, nested=True) + reconstructed_this_region_df = reconstruct_from_overlap_tower( + geometries_this_region_df, overlap_tower_this_region, nested=True + ) if fill_gaps: holes_this_region_df = holes_df[holes_df["region"] == r_ind] @@ -215,15 +295,37 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, # Also remove any non-simply connected holes since our algorithm breaks # down in that case, regardless of whether or not a relative area # threshold has been set. - holes_this_region_df, num_holes_dropped_this_region_nsc, num_holes_dropped_this_region_aat = drop_bad_holes(reconstructed_this_region_df, holes_this_region_df, fill_gaps_threshold=fill_gaps_threshold) + ( + holes_this_region_df, + num_holes_dropped_this_region_nsc, + num_holes_dropped_this_region_aat, + ) = drop_bad_holes( + reconstructed_this_region_df, + holes_this_region_df, + fill_gaps_threshold=fill_gaps_threshold, + ) if num_holes_dropped_this_region_aat > 0: - print(num_holes_dropped_this_region_aat, "gaps in region", r_ind, "will remain unfilled, because they exceed the area threshold.") + print( + num_holes_dropped_this_region_aat, + "gaps in region", + r_ind, + "will remain unfilled, because they exceed the area threshold.", + ) if num_holes_dropped_this_region_nsc > 0: - print(num_holes_dropped_this_region_nsc, "gaps in region", r_ind, "will remain unfilled, because they are not simply connected.") + print( + num_holes_dropped_this_region_nsc, + "gaps in region", + r_ind, + "will remain unfilled, because they are not simply connected.", + ) - reconstructed_this_region_df = smart_close_gaps(reconstructed_this_region_df, holes_this_region_df) + reconstructed_this_region_df = smart_close_gaps( + reconstructed_this_region_df, holes_this_region_df + ) - reconstructed_df.loc[list(reconstructed_this_region_df.index), "geometry"] = reconstructed_this_region_df["geometry"] + reconstructed_df.loc[ + list(reconstructed_this_region_df.index), "geometry" + ] = reconstructed_this_region_df["geometry"] # Check for geometries that have become (more) disconnected, generally with an extra # component of negligible area. If any are found and the area is negligible, @@ -231,76 +333,156 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, # If the area is not negligible, leave it alone and report it so that the user # can decide what to do about it. - disconnected_df = reconstructed_df[reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon")] + disconnected_df = reconstructed_df[ + reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon") + ] # This will include geometries that were disconnected in the original; need to # filter by whether they got worse. - -# FIX: Allow for the possibility that reconnecting one geometry inadvertently reconnects -# another one at the same time/ + # FIX: Allow for the possibility that reconnecting one geometry inadvertently reconnects + # another one at the same time/ if len(disconnected_df) > 0: geometries = get_geometries(reconstructed_df) spatial_index = STRtree(geometries) - index_by_iloc = dict((i, list(geometries.index)[i]) for i in range(len(geometries.index))) + index_by_iloc = dict( + (i, list(geometries.index)[i]) for i in range(len(geometries.index)) + ) for g_ind in disconnected_df.index: - if num_components(reconstructed_df.loc[g_ind, "geometry"]) > num_components(geometries0_df.loc[g_ind, "geometry"]): - excess = num_components(reconstructed_df.loc[g_ind, "geometry"]) - num_components(geometries0_df.loc[g_ind, "geometry"]) - component_num_list = list(range(len(reconstructed_df.loc[g_ind, "geometry"].geoms))) + if num_components(reconstructed_df.loc[g_ind, "geometry"]) > num_components( + geometries0_df.loc[g_ind, "geometry"] + ): + excess = num_components( + reconstructed_df.loc[g_ind, "geometry"] + ) - num_components(geometries0_df.loc[g_ind, "geometry"]) + component_num_list = list( + range(len(reconstructed_df.loc[g_ind, "geometry"].geoms)) + ) component_areas = [] for c_ind in range(len(reconstructed_df.loc[g_ind, "geometry"].geoms)): - component_areas.append((c_ind, reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind].area)) + component_areas.append( + ( + c_ind, + reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind].area, + ) + ) component_areas_sorted = sorted(component_areas, key=lambda tup: tup[1]) - big_area = max([reconstructed_df.loc[g_ind, "geometry"].area, geometries0_df.loc[g_ind, "geometry"].area]) + big_area = max( + [ + reconstructed_df.loc[g_ind, "geometry"].area, + geometries0_df.loc[g_ind, "geometry"].area, + ] + ) for i in range(excess): # Check whether the ith smallest component has small enough area, and if # so find a better polygon to add it to. c_ind = component_areas_sorted[i][0] this_fragment = reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind] - if component_areas_sorted[i][1] < disconnection_threshold*big_area: - possible_intersect_integer_indices = list(set(spatial_index.query(this_fragment).ravel())) - possible_intersect_indices = [(index_by_iloc[k]) for k in possible_intersect_integer_indices] + if ( + component_areas_sorted[i][1] + < disconnection_threshold * big_area + ): + possible_intersect_integer_indices = list( + set(spatial_index.query(this_fragment).ravel()) + ) + possible_intersect_indices = [ + (index_by_iloc[k]) + for k in possible_intersect_integer_indices + ] if nest_within_regions is not None: # Restrict to geometries in the same region as this geometry - possible_intersect_indices = [ind for ind in possible_intersect_indices if geometries_to_regions_assignment[ind] == geometries_to_regions_assignment[g_ind]] + possible_intersect_indices = [ + ind + for ind in possible_intersect_indices + if geometries_to_regions_assignment[ind] + == geometries_to_regions_assignment[g_ind] + ] shared_perimeters = [] for g_ind2 in possible_intersect_indices: - if g_ind2 != g_ind and not (this_fragment.boundary).intersection(reconstructed_df.loc[g_ind2, "geometry"].boundary).is_empty: - shared_perimeters.append((g_ind2, (this_fragment.boundary).intersection(reconstructed_df.loc[g_ind2, "geometry"].boundary).length)) + if ( + g_ind2 != g_ind + and not (this_fragment.boundary) + .intersection( + reconstructed_df.loc[g_ind2, "geometry"].boundary + ) + .is_empty + ): + shared_perimeters.append( + ( + g_ind2, + (this_fragment.boundary) + .intersection( + reconstructed_df.loc[ + g_ind2, "geometry" + ].boundary + ) + .length, + ) + ) # If this is an isolated fragment and doesn't touch any other # geometries, leave it alone; otherwise, choose a geometry to # adjoin it to by largest shared perimeter. if len(shared_perimeters) > 0: - component_num_list.remove(c_ind) # Tells us to take out this component later - max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] + component_num_list.remove( + c_ind + ) # Tells us to take out this component later + max_shared_perim = sorted( + shared_perimeters, key=lambda tup: tup[1] + )[-1] poly_to_add_to = max_shared_perim[0] - reconstructed_df.loc[poly_to_add_to, "geometry"] = unary_union( - [reconstructed_df.loc[poly_to_add_to, "geometry"], this_fragment]) + reconstructed_df.loc[poly_to_add_to, "geometry"] = ( + unary_union( + [ + reconstructed_df.loc[ + poly_to_add_to, "geometry" + ], + this_fragment, + ] + ) + ) if len(component_num_list) == 1: - reconstructed_df.loc[g_ind, "geometry"] = reconstructed_df.loc[g_ind, "geometry"].geoms[component_num_list[0]] + reconstructed_df.loc[g_ind, "geometry"] = reconstructed_df.loc[ + g_ind, "geometry" + ].geoms[component_num_list[0]] elif len(component_num_list) > 1: reconstructed_df.loc[g_ind, "geometry"] = MultiPolygon( - [reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind] for c_ind in component_num_list]) + [ + reconstructed_df.loc[g_ind, "geometry"].geoms[c_ind] + for c_ind in component_num_list + ] + ) else: - print("WARNING: A component of the geometry at index", g_ind, "was badly disconnected and redistributed to other geometries!") + print( + "WARNING: A component of the geometry at index", + g_ind, + "was badly disconnected and redistributed to other geometries!", + ) # We should usually now be back to the correct number of components everywhere, but # there may occasionally be exceptions, so check again and alert the user if not. - disconnected_df_2 = reconstructed_df[reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon")] + disconnected_df_2 = reconstructed_df[ + reconstructed_df["geometry"].apply(lambda x: x.geom_type != "Polygon") + ] if len(disconnected_df_2) > 0: for ind in disconnected_df_2.index: - if num_components(reconstructed_df.loc[ind, "geometry"]) > num_components(geometries0_df.loc[ind, "geometry"]): - print("WARNING: A component of the geometry at index", ind, "may have been disconnected!") + if num_components(reconstructed_df.loc[ind, "geometry"]) > num_components( + geometries0_df.loc[ind, "geometry"] + ): + print( + "WARNING: A component of the geometry at index", + ind, + "may have been disconnected!", + ) if min_rook_length is not None: # Find all inter-polygon boundaries shorter than min_rook_length and replace them @@ -318,11 +500,12 @@ def smart_repair(geometries_df, snapped=True, snap_precision=9, fill_gaps=True, # SUPPORTING FUNCTIONS ######### + def num_components(geom): """Counts the number of connected components of a shapely object.""" if geom.is_empty: return 0 - elif geom.geom_type in ("Polygon", "Point", "LineString"): + elif geom.geom_type in ("Polygon", "Point", "LineString"): return 1 elif geom.geom_type in ("MultiPolygon", "MultiLineString", "GeometryCollection"): return len(geom.geoms) @@ -331,10 +514,10 @@ def num_components(geom): def segments(curve): """Extracts a list of the individual line segments from a LineString""" return list(map(LineString, zip(curve.coords[:-1], curve.coords[1:]))) - - + + def contain_each_other(poly1, poly2): - return(poly1.contains(poly2) and poly2.contains(poly1)) + return poly1.contains(poly2) and poly2.contains(poly1) def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None): @@ -351,20 +534,26 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None geometries_df = geometries_df.copy() if nest_within_regions is not None: if isinstance(nest_within_regions, GeoDataFrame) is False: - raise TypeError("nest_within_regions must be either None or a GeoDataFrame.") + raise TypeError( + "nest_within_regions must be either None or a GeoDataFrame." + ) else: regions_df = nest_within_regions.copy() # Make a list of all the boundaries of all the polygons. # This won't work properly with MultiPolygons, so explode first: boundaries = [] - geometries_exploded_df = geometries_df.explode(index_parts=False).reset_index(drop=True) + geometries_exploded_df = geometries_df.explode(index_parts=False).reset_index( + drop=True + ) for i in geometries_exploded_df.index: boundaries.append(shapely.boundary(geometries_exploded_df.loc[i, "geometry"])) # Include region boundaries if applicable: if nest_within_regions is not None: - regions_exploded_df = regions_df.explode(index_parts=False).reset_index(drop=True) + regions_exploded_df = regions_df.explode(index_parts=False).reset_index( + drop=True + ) for i in regions_exploded_df.index: boundaries.append(shapely.boundary(regions_exploded_df.loc[i, "geometry"])) @@ -375,10 +564,12 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None elif geom.geom_type == "MultiLineString": boundaries_exploded += list(geom.geoms) boundaries_union = shapely.node(MultiLineString(boundaries_exploded)) - + # Snap the noded boundaries to a grid of size snap_magnitude-1 and re-node: if snap_magnitude is not None: - boundaries_2 = snap_multilinestring_to_grid(boundaries_union, n=snap_magnitude-1) + boundaries_2 = snap_multilinestring_to_grid( + boundaries_union, n=snap_magnitude - 1 + ) boundaries_2_exploded = [] for geom in boundaries_2.geoms: if geom.geom_type == "LineString": @@ -386,28 +577,36 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None elif geom.geom_type == "MultiLineString": boundaries_2_exploded += list(geom.geoms) boundaries_union = shapely.node(MultiLineString(boundaries_2_exploded)) - + # Create a geodataframe with all the pieces created by overlaps of all orders, # together with a set for each piece consisting of the polygons that created the overlap. - pieces_df = GeoDataFrame(columns=["polygon indices"], - geometry=GeoSeries(list(polygonize(boundaries_union))), - crs=geometries_df.crs) + pieces_df = GeoDataFrame( + columns=["polygon indices"], + geometry=GeoSeries(list(polygonize(boundaries_union))), + crs=geometries_df.crs, + ) pieces_df["polygon indices"] = [set() for x in range(len(pieces_df.index))] - + # Add a column to indicate the region for each piece; if there are no regions the # entries will remain as None. pieces_df["region"] = None g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) + g_index_by_iloc = dict( + (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) + ) # If region boundaries are included, also create an STRtree for the regions # and assign the main geometries to regions by largest area overlap. if nest_within_regions is not None: r_spatial_index = STRtree(regions_df["geometry"]) - r_index_by_iloc = dict((i, list(regions_df.index)[i]) for i in range(len(regions_df))) - geometries_to_regions_assignment = assign(geometries_df.geometry, regions_df.geometry) + r_index_by_iloc = dict( + (i, list(regions_df.index)[i]) for i in range(len(regions_df)) + ) + geometries_to_regions_assignment = assign( + geometries_df.geometry, regions_df.geometry + ) print("Identifying overlaps...") for i in progress(pieces_df.index, len(pieces_df.index)): @@ -415,28 +614,55 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None # Note that "None" is a possibility, and that each piece will belong to a unique # region because the regions GeoDataFrame/GeoSeries MUST be clean. if nest_within_regions is not None: - possible_region_integer_indices = list(set(r_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel())) - possible_region_indices = [r_index_by_iloc[k] for k in possible_region_integer_indices] + possible_region_integer_indices = list( + set(r_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel()) + ) + possible_region_indices = [ + r_index_by_iloc[k] for k in possible_region_integer_indices + ] for j in possible_region_indices: - if pieces_df.loc[i, "geometry"].representative_point().intersects(regions_df.loc[j, "geometry"]): + if ( + pieces_df.loc[i, "geometry"] + .representative_point() + .intersects(regions_df.loc[j, "geometry"]) + ): pieces_df.loc[i, "region"] = j # Now identify the set of geometries in the main geometry that each piece is # contained in. If region boundaries are included, then while determining which # geometries each piece is contained in, omit any geometries that are # assigned to a region other than the one the piece is contained in. - possible_geom_integer_indices = list(set(g_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel())) - possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] + possible_geom_integer_indices = list( + set(g_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel()) + ) + possible_geom_indices = [ + g_index_by_iloc[k] for k in possible_geom_integer_indices + ] for j in possible_geom_indices: if nest_within_regions is not None: - if pieces_df.loc[i, "geometry"].representative_point().intersects(geometries_df.loc[j, "geometry"]): - if geometries_to_regions_assignment[j] == pieces_df.loc[i, "region"]: - pieces_df.at[i, "polygon indices"] = pieces_df.at[i, "polygon indices"].union({j}) + if ( + pieces_df.loc[i, "geometry"] + .representative_point() + .intersects(geometries_df.loc[j, "geometry"]) + ): + if ( + geometries_to_regions_assignment[j] + == pieces_df.loc[i, "region"] + ): + pieces_df.at[i, "polygon indices"] = pieces_df.at[ + i, "polygon indices" + ].union({j}) else: - if pieces_df.loc[i, "geometry"].representative_point().intersects(geometries_df.loc[j, "geometry"]): - pieces_df.at[i, "polygon indices"] = pieces_df.at[i, "polygon indices"].union({j}) + if ( + pieces_df.loc[i, "geometry"] + .representative_point() + .intersects(geometries_df.loc[j, "geometry"]) + ): + pieces_df.at[i, "polygon indices"] = pieces_df.at[ + i, "polygon indices" + ].union({j}) # Organize this info into separate GeoDataFrames for overlaps of all orders - including # order zero, which corresponds to gaps. @@ -455,39 +681,60 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None pieces_df = pieces_df[~pieces_df["region"].isna()].reset_index(drop=True) holes_df = holes_df[~holes_df["region"].isna()].reset_index(drop=True) - consolidated_holes_df = GeoDataFrame(columns=["polygon indices", "geometry", "region", "overlap degree"], - geometry="geometry", crs=holes_df.crs) + consolidated_holes_df = GeoDataFrame( + columns=["polygon indices", "geometry", "region", "overlap degree"], + geometry="geometry", + crs=holes_df.crs, + ) for r_ind in regions_df.index: this_region_holes_df = holes_df[holes_df["region"] == r_ind] - this_region_consolidated_holes = GeoSeries([unary_union(this_region_holes_df["geometry"])]).explode(index_parts=False).reset_index(drop=True) - this_region_consolidated_holes_df = GeoDataFrame(geometry=this_region_consolidated_holes, crs=holes_df.crs) + this_region_consolidated_holes = ( + GeoSeries([unary_union(this_region_holes_df["geometry"])]) + .explode(index_parts=False) + .reset_index(drop=True) + ) + this_region_consolidated_holes_df = GeoDataFrame( + geometry=this_region_consolidated_holes, crs=holes_df.crs + ) this_region_consolidated_holes_df.insert(0, "polygon indices", None) - this_region_consolidated_holes_df["polygon indices"] = [set() for x in range(len(this_region_consolidated_holes_df.index))] + this_region_consolidated_holes_df["polygon indices"] = [ + set() for x in range(len(this_region_consolidated_holes_df.index)) + ] this_region_consolidated_holes_df.insert(2, "region", r_ind) this_region_consolidated_holes_df.insert(2, "overlap degree", 0) - - consolidated_holes_df = pandas.concat([consolidated_holes_df, this_region_consolidated_holes_df]).reset_index(drop=True) + + consolidated_holes_df = pandas.concat( + [consolidated_holes_df, this_region_consolidated_holes_df] + ).reset_index(drop=True) holes_df = consolidated_holes_df - + else: # Do the same thing we did for holes within each region to consolidate them: - all_consolidated_holes = GeoSeries([unary_union(holes_df["geometry"])]).explode(index_parts=False).reset_index(drop=True) - all_consolidated_holes_df = GeoDataFrame(geometry=all_consolidated_holes, crs=holes_df.crs) - + all_consolidated_holes = ( + GeoSeries([unary_union(holes_df["geometry"])]) + .explode(index_parts=False) + .reset_index(drop=True) + ) + all_consolidated_holes_df = GeoDataFrame( + geometry=all_consolidated_holes, crs=holes_df.crs + ) + all_consolidated_holes_df.insert(0, "polygon indices", None) - all_consolidated_holes_df["polygon indices"] = [set() for x in range(len(all_consolidated_holes_df.index))] + all_consolidated_holes_df["polygon indices"] = [ + set() for x in range(len(all_consolidated_holes_df.index)) + ] all_consolidated_holes_df.insert(2, "region", None) all_consolidated_holes_df.insert(2, "overlap degree", 0) - + holes_df = all_consolidated_holes_df # Here is a list of GeoDataFrames, one consisting of all overlaps of each order: overlap_tower = [] for i in range(max(pieces_df["overlap degree"])): - overlap_tower.append(pieces_df[pieces_df["overlap degree"] == i+1]) + overlap_tower.append(pieces_df[pieces_df["overlap degree"] == i + 1]) # Drop unnecessary "overlap degree" column and reindex each GeoDataFrame: for i in range(len(overlap_tower)): @@ -519,7 +766,9 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): for ind in overlap_tower[0].index: this_poly_ind = list(overlap_tower[0]["polygon indices"][ind])[0] this_piece = overlap_tower[0]["geometry"][ind] - geometries_df.loc[this_poly_ind, "geometry"] = unary_union([geometries_df.loc[this_poly_ind, "geometry"], this_piece]) + geometries_df.loc[this_poly_ind, "geometry"] = unary_union( + [geometries_df.loc[this_poly_ind, "geometry"], this_piece] + ) # We will need to know which geometries were disconnected by removing # overlaps, so add columns for numbers of components in the original and refined @@ -528,8 +777,12 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): geometries_df["num components refined"] = 0 for ind in geometries_df.index: - geometries_df.loc[ind, "num components orig"] = num_components(geometries0_df.loc[ind, "geometry"]) - geometries_df.loc[ind, "num components refined"] = num_components(geometries_df.loc[ind, "geometry"]) + geometries_df.loc[ind, "num components orig"] = num_components( + geometries0_df.loc[ind, "geometry"] + ) + geometries_df.loc[ind, "num components refined"] = num_components( + geometries_df.loc[ind, "geometry"] + ) # Now, start with the order 2 overlaps and gradually add overlaps at successively # higher orders until done. @@ -541,43 +794,79 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): # can disconnect more than one polygon, and only one of them gets to grab it back. # This will be addressed at the end of the reconstruction process. - geometries_disconnected_df = geometries_df[geometries_df["num components refined"] > geometries_df["num components orig"]] + geometries_disconnected_df = geometries_df[ + geometries_df["num components refined"] > geometries_df["num components orig"] + ] + + # FIX: Keep a list of overlaps that don't find a home during this process, and try them again + # at the end. This is necessary because on very rare occasions, a lower-order overlap might + # only adjoin higher-order overlaps and not be able to find a home until other overlaps have + # been assigned. -# FIX: Keep a list of overlaps that don't find a home during this process, and try them again -# at the end. This is necessary because on very rare occasions, a lower-order overlap might -# only adjoin higher-order overlaps and not be able to find a home until other overlaps have -# been assigned. - orphaned_overlaps = [] - + for i in range(1, max_overlap_level): overlaps_df = overlap_tower[i] overlaps_df_unused_indices = overlaps_df.index.tolist() o_spatial_index = STRtree(overlaps_df["geometry"]) - o_index_by_iloc = dict((i, list(overlaps_df.index)[i]) for i in range(len(overlaps_df))) + o_index_by_iloc = dict( + (i, list(overlaps_df.index)[i]) for i in range(len(overlaps_df)) + ) for g_ind in geometries_disconnected_df.index: - possible_overlap_integer_indices = list(set(o_spatial_index.query(geometries_disconnected_df.loc[g_ind, "geometry"]).ravel())) - possible_overlap_indices_0 = [o_index_by_iloc[k] for k in possible_overlap_integer_indices] - possible_overlap_indices = list(set(possible_overlap_indices_0) & set(overlaps_df_unused_indices)) + possible_overlap_integer_indices = list( + set( + o_spatial_index.query( + geometries_disconnected_df.loc[g_ind, "geometry"] + ).ravel() + ) + ) + possible_overlap_indices_0 = [ + o_index_by_iloc[k] for k in possible_overlap_integer_indices + ] + possible_overlap_indices = list( + set(possible_overlap_indices_0) & set(overlaps_df_unused_indices) + ) geom_finished = False for o_ind in possible_overlap_indices: # If the corresponding overlap intersects this geometry (and was # contained in it originally!), grab it. - if (geom_finished is False) and (g_ind in list(overlaps_df.loc[o_ind, "polygon indices"])) and (not geometries_disconnected_df.loc[g_ind, "geometry"].intersection(overlaps_df.loc[o_ind, "geometry"]).is_empty): - - if (geometries_disconnected_df.loc[g_ind, "geometry"].intersection(overlaps_df.loc[o_ind, "geometry"])).length > 0: - geometries_disconnected_df.loc[g_ind, "geometry"] = unary_union([ - geometries_disconnected_df.loc[g_ind, "geometry"], overlaps_df.loc[o_ind, "geometry"] - ]) + if ( + (geom_finished is False) + and (g_ind in list(overlaps_df.loc[o_ind, "polygon indices"])) + and ( + not geometries_disconnected_df.loc[g_ind, "geometry"] + .intersection(overlaps_df.loc[o_ind, "geometry"]) + .is_empty + ) + ): + + if ( + geometries_disconnected_df.loc[g_ind, "geometry"].intersection( + overlaps_df.loc[o_ind, "geometry"] + ) + ).length > 0: + geometries_disconnected_df.loc[g_ind, "geometry"] = unary_union( + [ + geometries_disconnected_df.loc[g_ind, "geometry"], + overlaps_df.loc[o_ind, "geometry"], + ] + ) overlaps_df_unused_indices.remove(o_ind) - if num_components(geometries_disconnected_df.loc[g_ind, "geometry"]) == geometries_df.loc[g_ind, "num components orig"]: + if ( + num_components( + geometries_disconnected_df.loc[g_ind, "geometry"] + ) + == geometries_df.loc[g_ind, "num components orig"] + ): geom_finished = True - geometries_df.loc[g_ind, "geometry"] = geometries_disconnected_df.loc[g_ind, "geometry"] + geometries_df.loc[g_ind, "geometry"] = geometries_disconnected_df.loc[ + g_ind, "geometry" + ] if geom_finished: geometries_disconnected_df = geometries_disconnected_df.drop(g_ind) @@ -585,54 +874,93 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): # That's all we can do for the disconnected geometries at this level. # Go on to filling in the rest of the overlaps by greatest perimeter. g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) + g_index_by_iloc = dict( + (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) + ) if nested is False: - print("Assigning order", i+1, "pieces...") - + print("Assigning order", i + 1, "pieces...") + for o_ind in overlaps_df_unused_indices: this_overlap = overlaps_df.loc[o_ind, "geometry"] shared_perimeters = [] - possible_geom_integer_indices = list(set(g_spatial_index.query(this_overlap).ravel())) - possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] + possible_geom_integer_indices = list( + set(g_spatial_index.query(this_overlap).ravel()) + ) + possible_geom_indices = [ + g_index_by_iloc[k] for k in possible_geom_integer_indices + ] for g_ind in possible_geom_indices: - if (g_ind in list(overlaps_df.loc[o_ind, "polygon indices"])) and not (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).is_empty: - shared_perimeters.append((g_ind, (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).length)) + if (g_ind in list(overlaps_df.loc[o_ind, "polygon indices"])) and not ( + this_overlap.boundary + ).intersection(geometries_df.loc[g_ind, "geometry"].boundary).is_empty: + shared_perimeters.append( + ( + g_ind, + (this_overlap.boundary) + .intersection(geometries_df.loc[g_ind, "geometry"].boundary) + .length, + ) + ) if len(shared_perimeters) > 0: max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( - [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap]) - + [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap] + ) + else: - orphaned_overlaps.append((overlaps_df.loc[o_ind, "geometry"], overlaps_df.loc[o_ind, "polygon indices"])) - -# After completing the overlap tower, try again to assign any orphaned overlaps: - + orphaned_overlaps.append( + ( + overlaps_df.loc[o_ind, "geometry"], + overlaps_df.loc[o_ind, "polygon indices"], + ) + ) + + # After completing the overlap tower, try again to assign any orphaned overlaps: + if len(orphaned_overlaps) > 0: for o_ind in range(len(orphaned_overlaps)): this_overlap = orphaned_overlaps[o_ind][0] this_overlap_polygon_indices = orphaned_overlaps[o_ind][1] shared_perimeters = [] - possible_geom_integer_indices = list(set(g_spatial_index.query(this_overlap).ravel())) - possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] + possible_geom_integer_indices = list( + set(g_spatial_index.query(this_overlap).ravel()) + ) + possible_geom_indices = [ + g_index_by_iloc[k] for k in possible_geom_integer_indices + ] for g_ind in possible_geom_indices: - if (g_ind in list(this_overlap_polygon_indices)) and not (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).is_empty: - shared_perimeters.append((g_ind, (this_overlap.boundary).intersection(geometries_df.loc[g_ind, "geometry"].boundary).length)) + if (g_ind in list(this_overlap_polygon_indices)) and not ( + this_overlap.boundary + ).intersection(geometries_df.loc[g_ind, "geometry"].boundary).is_empty: + shared_perimeters.append( + ( + g_ind, + (this_overlap.boundary) + .intersection(geometries_df.loc[g_ind, "geometry"].boundary) + .length, + ) + ) if len(shared_perimeters) > 0: max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( - [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap]) - + [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap] + ) + else: # It seems like this should REALLY never happen now, but I guess we'll see. if nested is False: - print("Couldn't find a polygon to glue a component in the intersection of geometries", overlaps_df.loc[o_ind, "polygon indices"], "to") + print( + "Couldn't find a polygon to glue a component in the intersection of geometries", + overlaps_df.loc[o_ind, "polygon indices"], + "to", + ) reconstructed_df = geometries_df del reconstructed_df["num components orig"] @@ -642,40 +970,58 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): def drop_bad_holes(reconstructed_df, holes_df, fill_gaps_threshold): - """ Identify holes that won't be filled and drop them from holes_df """ + """Identify holes that won't be filled and drop them from holes_df""" holes_df = holes_df.copy() if fill_gaps_threshold is not None: spatial_index = STRtree(reconstructed_df.geometry) - index_by_iloc = dict((i, list(reconstructed_df.index)[i]) for i in range(len(reconstructed_df.index))) + index_by_iloc = dict( + (i, list(reconstructed_df.index)[i]) + for i in range(len(reconstructed_df.index)) + ) hole_indices_to_drop_nsc = [] hole_indices_to_drop_aat = [] for h_ind in holes_df.index: this_hole = holes_df.loc[h_ind, "geometry"] - possible_intersect_integer_indices = list(set(spatial_index.query(this_hole).ravel())) - possible_intersect_indices = [(index_by_iloc[k]) for k in possible_intersect_integer_indices] - actual_intersect_indices = [g_ind for g_ind in possible_intersect_indices if not this_hole.intersection(reconstructed_df.loc[g_ind, "geometry"]).is_empty] + possible_intersect_integer_indices = list( + set(spatial_index.query(this_hole).ravel()) + ) + possible_intersect_indices = [ + (index_by_iloc[k]) for k in possible_intersect_integer_indices + ] + actual_intersect_indices = [ + g_ind + for g_ind in possible_intersect_indices + if not this_hole.intersection( + reconstructed_df.loc[g_ind, "geometry"] + ).is_empty + ] drop_this_hole_for_area = False if len(actual_intersect_indices) > 0: - max_geom_area = max(reconstructed_df.loc[g_ind, "geometry"].area for g_ind in actual_intersect_indices) - hole_area_ratio = this_hole.area/max_geom_area + max_geom_area = max( + reconstructed_df.loc[g_ind, "geometry"].area + for g_ind in actual_intersect_indices + ) + hole_area_ratio = this_hole.area / max_geom_area if hole_area_ratio > fill_gaps_threshold: hole_indices_to_drop_aat.append(h_ind) drop_this_hole_for_area = True - - if shapely.get_num_interior_rings(holes_df.loc[h_ind, "geometry"]) > 0 and not drop_this_hole_for_area: + + if ( + shapely.get_num_interior_rings(holes_df.loc[h_ind, "geometry"]) > 0 + and not drop_this_hole_for_area + ): hole_indices_to_drop_nsc.append(h_ind) - else: hole_indices_to_drop_nsc = [] - hole_indices_to_drop_aat = [] + hole_indices_to_drop_aat = [] for h_ind in holes_df.index: if shapely.get_num_interior_rings(holes_df.loc[h_ind, "geometry"]) > 0: hole_indices_to_drop_nsc.append(h_ind) - + hole_indices_to_drop = hole_indices_to_drop_nsc + hole_indices_to_drop_aat if len(hole_indices_to_drop) > 0: holes_df = holes_df.drop(hole_indices_to_drop).reset_index(drop=True) @@ -711,11 +1057,16 @@ def smart_close_gaps(geometries_df, holes_df): # Now proceed with filling simplified gaps. if len(holes_df) > 0: holes_to_process = deque(list(holes_df["geometry"])) - this_region = list(holes_df["region"])[0] # All holes in this dataframe should be from the same region + this_region = list(holes_df["region"])[ + 0 + ] # All holes in this dataframe should be from the same region if this_region is None: pbar = tqdm(desc="Gaps to fill", total=len(holes_to_process)) else: - pbar = tqdm(desc=f"Gaps to fill in region {this_region}", total=len(holes_to_process)) + pbar = tqdm( + desc=f"Gaps to fill in region {this_region}", + total=len(holes_to_process), + ) else: holes_to_process = deque([]) pbar = tqdm(desc="Gaps to fill", total=len(holes_to_process)) @@ -733,8 +1084,12 @@ def smart_close_gaps(geometries_df, holes_df): if len(set(this_hole_boundaries_df["target"]).difference({-1})) == 1: # Attach the gap to the unique non-exterior geometry that it intersects: - poly_to_add_to = list(set(this_hole_boundaries_df["target"]).difference({-1}))[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union([geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) + poly_to_add_to = list( + set(this_hole_boundaries_df["target"]).difference({-1}) + )[0] + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] + ) elif len(segments(this_hole.boundary)) == 3: # If the hole is a simple triangle if len(set(this_hole_boundaries_df["target"]).difference({-1})) == 3: @@ -745,25 +1100,47 @@ def smart_close_gaps(geometries_df, holes_df): for thb_ind in this_hole_boundaries_df.index: g_ind = this_hole_boundaries_df.loc[thb_ind, "target"] this_segment = this_hole_boundaries_df.loc[thb_ind, "geometry"] - this_segment_poly_to_add = make_valid(Polygon([this_segment.boundary.geoms[0], this_segment.boundary.geoms[1], this_hole_incenter])) - geometries_df.loc[g_ind, "geometry"] = unary_union([geometries_df.loc[g_ind, "geometry"], this_segment_poly_to_add]) + this_segment_poly_to_add = make_valid( + Polygon( + [ + this_segment.boundary.geoms[0], + this_segment.boundary.geoms[1], + this_hole_incenter, + ] + ) + ) + geometries_df.loc[g_ind, "geometry"] = unary_union( + [geometries_df.loc[g_ind, "geometry"], this_segment_poly_to_add] + ) else: # There are either 2 sides intersecting a common geometry or 1 # side intersecting an exterior boundary. In this case join the entire # triangle to the geometry that it shares the largest perimeter with. - touching_geoms = list(set(this_hole_boundaries_df["target"]).difference({-1})) - perim_1 = this_hole.intersection(geometries_df.loc[touching_geoms[0], "geometry"]).length - perim_2 = this_hole.intersection(geometries_df.loc[touching_geoms[1], "geometry"]).length + touching_geoms = list( + set(this_hole_boundaries_df["target"]).difference({-1}) + ) + perim_1 = this_hole.intersection( + geometries_df.loc[touching_geoms[0], "geometry"] + ).length + perim_2 = this_hole.intersection( + geometries_df.loc[touching_geoms[1], "geometry"] + ).length if perim_1 > perim_2: poly_to_add_to = touching_geoms[0] else: poly_to_add_to = touching_geoms[1] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union([geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] + ) else: - this_hole_df = GeoDataFrame(geometry=GeoSeries([this_hole]), crs=holes_df.crs) - this_hole_boundaries_df = construct_hole_boundaries(geometries_df, this_hole_df) + this_hole_df = GeoDataFrame( + geometry=GeoSeries([this_hole]), crs=holes_df.crs + ) + this_hole_boundaries_df = construct_hole_boundaries( + geometries_df, this_hole_df + ) # If this_hole falls into one of the simple cases above, put it back # in the queue. (Note that after convexification, @@ -774,15 +1151,29 @@ def smart_close_gaps(geometries_df, holes_df): this_hole_boundaries = [this_hole_boundaries_df.loc[0, "geometry"]] target_geometries = [this_hole_boundaries_df.loc[0, "target"]] - if this_hole_boundaries_df.loc[1, "geometry"].coords[0] == this_hole_boundaries_df.loc[0, "geometry"].coords[-1]: - this_hole_boundaries.append(this_hole_boundaries_df.loc[1, "geometry"]) + if ( + this_hole_boundaries_df.loc[1, "geometry"].coords[0] + == this_hole_boundaries_df.loc[0, "geometry"].coords[-1] + ): + this_hole_boundaries.append( + this_hole_boundaries_df.loc[1, "geometry"] + ) target_geometries.append(this_hole_boundaries_df.loc[1, "target"]) - this_hole_boundaries.append(this_hole_boundaries_df.loc[2, "geometry"]) + this_hole_boundaries.append( + this_hole_boundaries_df.loc[2, "geometry"] + ) target_geometries.append(this_hole_boundaries_df.loc[2, "target"]) - elif this_hole_boundaries_df.loc[2, "geometry"].coords[0] == this_hole_boundaries_df.loc[0, "geometry"].coords[-1]: - this_hole_boundaries.append(this_hole_boundaries_df.loc[2, "geometry"]) + elif ( + this_hole_boundaries_df.loc[2, "geometry"].coords[0] + == this_hole_boundaries_df.loc[0, "geometry"].coords[-1] + ): + this_hole_boundaries.append( + this_hole_boundaries_df.loc[2, "geometry"] + ) target_geometries.append(this_hole_boundaries_df.loc[2, "target"]) - this_hole_boundaries.append(this_hole_boundaries_df.loc[1, "geometry"]) + this_hole_boundaries.append( + this_hole_boundaries_df.loc[1, "geometry"] + ) target_geometries.append(this_hole_boundaries_df.loc[1, "target"]) # If one of the boundaries is an exterior region boundary, find @@ -793,34 +1184,97 @@ def smart_close_gaps(geometries_df, holes_df): ext_boundary_position = target_geometries.index(-1) # Cyclically permute so that the exterior boundary is in the # 1st position: - this_hole_boundaries = this_hole_boundaries[ext_boundary_position:] + this_hole_boundaries[0:ext_boundary_position] - target_geometries = target_geometries[ext_boundary_position:] + target_geometries[0:ext_boundary_position] + this_hole_boundaries = ( + this_hole_boundaries[ext_boundary_position:] + + this_hole_boundaries[0:ext_boundary_position] + ) + target_geometries = ( + target_geometries[ext_boundary_position:] + + target_geometries[0:ext_boundary_position] + ) main_vertex = Point(this_hole_boundaries[2].coords[0]) - nearest_ext_boundary_point = nearest_points(main_vertex, extract_unique_points(this_hole_boundaries[0]))[1] + nearest_ext_boundary_point = nearest_points( + main_vertex, extract_unique_points(this_hole_boundaries[0]) + )[1] - ext_boundary_points = list(extract_unique_points(this_hole_boundaries[0]).geoms) - nearest_point_position = ext_boundary_points.index(nearest_ext_boundary_point) + ext_boundary_points = list( + extract_unique_points(this_hole_boundaries[0]).geoms + ) + nearest_point_position = ext_boundary_points.index( + nearest_ext_boundary_point + ) if nearest_point_position == 0: # Add the entire hole to target_geometries[1]. - geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], this_hole]) + geometries_df.loc[target_geometries[1], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[1], "geometry"], + this_hole, + ] + ) + ) elif nearest_point_position == len(ext_boundary_points) - 1: # Add the entire hole to target_geometries[2]. - geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], this_hole]) + geometries_df.loc[target_geometries[2], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[2], "geometry"], + this_hole, + ] + ) + ) else: this_hole_triangulation = triangulate_polygon(this_hole) - sp = LineString(shortest_path_in_polygon(this_hole, main_vertex, nearest_ext_boundary_point, full_triangulation=this_hole_triangulation)) - - poly1_to_add_boundary = unary_union([this_hole_boundaries[1], sp, LineString(ext_boundary_points[nearest_point_position:])]) + sp = LineString( + shortest_path_in_polygon( + this_hole, + main_vertex, + nearest_ext_boundary_point, + full_triangulation=this_hole_triangulation, + ) + ) + + poly1_to_add_boundary = unary_union( + [ + this_hole_boundaries[1], + sp, + LineString( + ext_boundary_points[nearest_point_position:] + ), + ] + ) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], poly1_to_add]) - - poly2_to_add_boundary = unary_union([this_hole_boundaries[2], sp, LineString(ext_boundary_points[0:nearest_point_position+1])]) + geometries_df.loc[target_geometries[1], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[1], "geometry"], + poly1_to_add, + ] + ) + ) + + poly2_to_add_boundary = unary_union( + [ + this_hole_boundaries[2], + sp, + LineString( + ext_boundary_points[0 : nearest_point_position + 1] + ), + ] + ) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], poly2_to_add]) + geometries_df.loc[target_geometries[2], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[2], "geometry"], + poly2_to_add, + ] + ) + ) # Otherwise, construct the incenter of the circumscribing triangle. # If the incenter is in the interior of the hole, construct shortest paths @@ -829,71 +1283,191 @@ def smart_close_gaps(geometries_df, holes_df): # the hole between the OTHER two boundaries as if this boundary had no # adjoining geometry. else: - main_vertices = [this_hole_boundaries[i].boundary.geoms[0] for i in range(3)] - + main_vertices = [ + this_hole_boundaries[i].boundary.geoms[0] for i in range(3) + ] + this_hole_hull = Polygon(main_vertices) this_hole_hull_incenter = incenter(this_hole_hull) - + if this_hole.contains(this_hole_hull_incenter): this_hole_triangulation = triangulate_polygon(this_hole) - incenter_triangle = [poly for poly in this_hole_triangulation if poly.contains(this_hole_hull_incenter) or poly.boundary.contains(this_hole_hull_incenter)][0] - incenter_triangle_vertices = extract_unique_points(incenter_triangle.boundary).geoms - incenter_segments = [LineString([this_hole_hull_incenter, point]) for point in incenter_triangle_vertices] - this_hole_partition = polygonize(unary_union([this_hole.boundary] + incenter_segments)) - + incenter_triangle = [ + poly + for poly in this_hole_triangulation + if poly.contains(this_hole_hull_incenter) + or poly.boundary.contains(this_hole_hull_incenter) + ][0] + incenter_triangle_vertices = extract_unique_points( + incenter_triangle.boundary + ).geoms + incenter_segments = [ + LineString([this_hole_hull_incenter, point]) + for point in incenter_triangle_vertices + ] + this_hole_partition = polygonize( + unary_union([this_hole.boundary] + incenter_segments) + ) + paths_to_main_vertices = [] for i in range(3): - sub_hole = [poly for poly in this_hole_partition if poly.boundary.contains(main_vertices[i])][0] - paths_to_main_vertices.append(LineString(shortest_path_in_polygon(sub_hole, this_hole_hull_incenter, main_vertices[i]))) - - poly0_to_add_boundary = unary_union([this_hole_boundaries[0], paths_to_main_vertices[0], paths_to_main_vertices[1]]) + sub_hole = [ + poly + for poly in this_hole_partition + if poly.boundary.contains(main_vertices[i]) + ][0] + paths_to_main_vertices.append( + LineString( + shortest_path_in_polygon( + sub_hole, + this_hole_hull_incenter, + main_vertices[i], + ) + ) + ) + + poly0_to_add_boundary = unary_union( + [ + this_hole_boundaries[0], + paths_to_main_vertices[0], + paths_to_main_vertices[1], + ] + ) poly0_to_add = polygonize(poly0_to_add_boundary)[0] - geometries_df.loc[target_geometries[0], "geometry"] = unary_union([geometries_df.loc[target_geometries[0], "geometry"], poly0_to_add]) - - poly1_to_add_boundary = unary_union([this_hole_boundaries[1], paths_to_main_vertices[1], paths_to_main_vertices[2]]) + geometries_df.loc[target_geometries[0], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[0], "geometry"], + poly0_to_add, + ] + ) + ) + + poly1_to_add_boundary = unary_union( + [ + this_hole_boundaries[1], + paths_to_main_vertices[1], + paths_to_main_vertices[2], + ] + ) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], poly1_to_add]) - - poly2_to_add_boundary = unary_union([this_hole_boundaries[2], paths_to_main_vertices[2], paths_to_main_vertices[0]]) + geometries_df.loc[target_geometries[1], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[1], "geometry"], + poly1_to_add, + ] + ) + ) + + poly2_to_add_boundary = unary_union( + [ + this_hole_boundaries[2], + paths_to_main_vertices[2], + paths_to_main_vertices[0], + ] + ) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], poly2_to_add]) - + geometries_df.loc[target_geometries[2], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[2], "geometry"], + poly2_to_add, + ] + ) + ) + else: - incenter_boundary_dists = [this_hole_boundaries[i].distance(this_hole_hull_incenter) for i in range(3)] - - min_dist_position = incenter_boundary_dists.index(min(incenter_boundary_dists)) - this_hole_boundaries = this_hole_boundaries[min_dist_position:] + this_hole_boundaries[0:min_dist_position] - target_geometries = target_geometries[min_dist_position:] + target_geometries[0:min_dist_position] - - main_vertex = Point(this_hole_boundaries[2].coords[0]) - opp_boundary_int_points = list(extract_unique_points(this_hole_boundaries[0]).geoms)[1:-1] - nearest_opp_boundary_int_point = nearest_points(main_vertex, MultiPoint(opp_boundary_int_points))[1] - - opp_boundary_points = list(extract_unique_points(this_hole_boundaries[0]).geoms) - nearest_point_position = opp_boundary_points.index(nearest_opp_boundary_int_point) + incenter_boundary_dists = [ + this_hole_boundaries[i].distance(this_hole_hull_incenter) + for i in range(3) + ] + + min_dist_position = incenter_boundary_dists.index( + min(incenter_boundary_dists) + ) + this_hole_boundaries = ( + this_hole_boundaries[min_dist_position:] + + this_hole_boundaries[0:min_dist_position] + ) + target_geometries = ( + target_geometries[min_dist_position:] + + target_geometries[0:min_dist_position] + ) - #if nearest_point_position == 0: + main_vertex = Point(this_hole_boundaries[2].coords[0]) + opp_boundary_int_points = list( + extract_unique_points(this_hole_boundaries[0]).geoms + )[1:-1] + nearest_opp_boundary_int_point = nearest_points( + main_vertex, MultiPoint(opp_boundary_int_points) + )[1] + + opp_boundary_points = list( + extract_unique_points(this_hole_boundaries[0]).geoms + ) + nearest_point_position = opp_boundary_points.index( + nearest_opp_boundary_int_point + ) + + # if nearest_point_position == 0: # # Add the entire hole to target_geometries[1]. # geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], this_hole]) - #elif nearest_point_position == len(ext_boundary_points) - 1: + # elif nearest_point_position == len(ext_boundary_points) - 1: # # Add the entire hole to target_geometries[2]. # geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], this_hole]) - #else: - - this_hole_triangulation = triangulate_polygon(this_hole) - sp = LineString(shortest_path_in_polygon(this_hole, main_vertex, nearest_opp_boundary_int_point, full_triangulation=this_hole_triangulation)) + # else: - poly1_to_add_boundary = unary_union([this_hole_boundaries[1], sp, LineString(opp_boundary_points[nearest_point_position:])]) + this_hole_triangulation = triangulate_polygon(this_hole) + sp = LineString( + shortest_path_in_polygon( + this_hole, + main_vertex, + nearest_opp_boundary_int_point, + full_triangulation=this_hole_triangulation, + ) + ) + + poly1_to_add_boundary = unary_union( + [ + this_hole_boundaries[1], + sp, + LineString( + opp_boundary_points[nearest_point_position:] + ), + ] + ) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], poly1_to_add]) - - poly2_to_add_boundary = unary_union([this_hole_boundaries[2], sp, LineString(opp_boundary_points[0:nearest_point_position+1])]) + geometries_df.loc[target_geometries[1], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[1], "geometry"], + poly1_to_add, + ] + ) + ) + + poly2_to_add_boundary = unary_union( + [ + this_hole_boundaries[2], + sp, + LineString( + opp_boundary_points[0 : nearest_point_position + 1] + ), + ] + ) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], poly2_to_add]) + geometries_df.loc[target_geometries[2], "geometry"] = ( + unary_union( + [ + geometries_df.loc[target_geometries[2], "geometry"], + poly2_to_add, + ] + ) + ) - else: # If len(this_hole_boundaries_df) >= 4 this_hole_triangulation = triangulate_polygon(this_hole) thb_distances = [] @@ -901,21 +1475,36 @@ def smart_close_gaps(geometries_df, holes_df): for i in this_hole_boundaries_df.index: for j in this_hole_boundaries_df.index: if j > i: - this_distance = this_hole_boundaries_df.loc[i, "geometry"].distance(this_hole_boundaries_df.loc[j, "geometry"]) + this_distance = this_hole_boundaries_df.loc[ + i, "geometry" + ].distance(this_hole_boundaries_df.loc[j, "geometry"]) if this_distance != 0: thb_distances.append((i, j, this_distance)) - thb_distance_data_sorted = deque(sorted(thb_distances, key=lambda tup: tup[2])) + thb_distance_data_sorted = deque( + sorted(thb_distances, key=lambda tup: tup[2]) + ) found_triangles = False while found_triangles is False and len(thb_distance_data_sorted) > 0: boundary_distance_data = thb_distance_data_sorted.popleft() - boundaries_to_connect = (boundary_distance_data[0], boundary_distance_data[1]) - - nhb1 = this_hole_boundaries_df.loc[boundaries_to_connect[0], "geometry"] - nhb2 = this_hole_boundaries_df.loc[boundaries_to_connect[1], "geometry"] - geom1 = this_hole_boundaries_df.loc[boundaries_to_connect[0], "target"] - geom2 = this_hole_boundaries_df.loc[boundaries_to_connect[1], "target"] + boundaries_to_connect = ( + boundary_distance_data[0], + boundary_distance_data[1], + ) + + nhb1 = this_hole_boundaries_df.loc[ + boundaries_to_connect[0], "geometry" + ] + nhb2 = this_hole_boundaries_df.loc[ + boundaries_to_connect[1], "geometry" + ] + geom1 = this_hole_boundaries_df.loc[ + boundaries_to_connect[0], "target" + ] + geom2 = this_hole_boundaries_df.loc[ + boundaries_to_connect[1], "target" + ] # Construct the shortest paths between # (1) initial points of both boundaries; @@ -924,7 +1513,7 @@ def smart_close_gaps(geometries_df, holes_df): # hole boundary segments, but generically---and provably for at # at leat one non-adjacent pair---at a single interior point of # the hole. - # IF THE POINT IS IN THE INTERIOR, REPLACE IT WITH THE NEAREST + # IF THE POINT IS IN THE INTERIOR, REPLACE IT WITH THE NEAREST # POINT ON THE BOUNDARY TO MINIMIZE ROUNDING ERRORS CREATED BY # INTRODUCING NEW POINTS! # In the generic case, these paths together with the two @@ -956,23 +1545,59 @@ def smart_close_gaps(geometries_df, holes_df): geom_int = geom1 point1 = nhb_int.boundary.geoms[0] point2 = nhb_int.boundary.geoms[1] - nearest_ext_boundary_point = nearest_points(nhb_int, extract_unique_points(nhb_ext))[1] - path1 = LineString(shortest_path_in_polygon(this_hole, point1, nearest_ext_boundary_point, full_triangulation=this_hole_triangulation)) - path2 = LineString(shortest_path_in_polygon(this_hole, point2, nearest_ext_boundary_point, full_triangulation=this_hole_triangulation)) - polys_to_add_boundary = shapely.node(MultiLineString([nhb_int, path1, path2])) + nearest_ext_boundary_point = nearest_points( + nhb_int, extract_unique_points(nhb_ext) + )[1] + path1 = LineString( + shortest_path_in_polygon( + this_hole, + point1, + nearest_ext_boundary_point, + full_triangulation=this_hole_triangulation, + ) + ) + path2 = LineString( + shortest_path_in_polygon( + this_hole, + point2, + nearest_ext_boundary_point, + full_triangulation=this_hole_triangulation, + ) + ) + polys_to_add_boundary = shapely.node( + MultiLineString([nhb_int, path1, path2]) + ) polys_to_add = polygonize(polys_to_add_boundary) - - hole_partition_boundary = shapely.node(MultiLineString(list(this_hole_boundaries_df["geometry"]) + [path1, path2])) + + hole_partition_boundary = shapely.node( + MultiLineString( + list(this_hole_boundaries_df["geometry"]) + + [path1, path2] + ) + ) hole_partition_polys = polygonize(hole_partition_boundary) - + if len(polys_to_add) > 0: for poly_to_add in polys_to_add: if poly_to_add.area > 0: found_triangles = True - geometries_df.loc[geom_int, "geometry"] = unary_union([geometries_df.loc[geom_int, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] - #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] - #this_hole = this_hole.difference(poly_to_add) + geometries_df.loc[geom_int, "geometry"] = ( + unary_union( + [ + geometries_df.loc[ + geom_int, "geometry" + ], + poly_to_add, + ] + ) + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other(poly, poly_to_add) + ] + # hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + # this_hole = this_hole.difference(poly_to_add) else: # Start by constructing the shortest paths between the initial point @@ -985,9 +1610,26 @@ def smart_close_gaps(geometries_df, holes_df): point21 = nhb2.boundary.geoms[0] point22 = nhb2.boundary.geoms[1] - test_path1_vertices = shortest_path_in_polygon(this_hole, point11, point22, full_triangulation=this_hole_triangulation) - test_path2_vertices = shortest_path_in_polygon(this_hole, point12, point21, full_triangulation=this_hole_triangulation) - if len(set(test_path1_vertices).intersection(set(test_path2_vertices))) == 0: + test_path1_vertices = shortest_path_in_polygon( + this_hole, + point11, + point22, + full_triangulation=this_hole_triangulation, + ) + test_path2_vertices = shortest_path_in_polygon( + this_hole, + point12, + point21, + full_triangulation=this_hole_triangulation, + ) + if ( + len( + set(test_path1_vertices).intersection( + set(test_path2_vertices) + ) + ) + == 0 + ): # In this case we should be good to add triangles formed # by crossing paths between the initial and terminal # points between the two boundaries! @@ -1000,29 +1642,64 @@ def smart_close_gaps(geometries_df, holes_df): found_triangles = True if geom1 == geom2: - path1 = LineString(shortest_path_in_polygon(this_hole, point11, point22, full_triangulation=this_hole_triangulation)) - path2 = LineString(shortest_path_in_polygon(this_hole, point12, point21, full_triangulation=this_hole_triangulation)) + path1 = LineString( + shortest_path_in_polygon( + this_hole, + point11, + point22, + full_triangulation=this_hole_triangulation, + ) + ) + path2 = LineString( + shortest_path_in_polygon( + this_hole, + point12, + point21, + full_triangulation=this_hole_triangulation, + ) + ) else: - path1 = LineString(shortest_path_in_polygon(this_hole, point11, point21, full_triangulation=this_hole_triangulation)) - path2 = LineString(shortest_path_in_polygon(this_hole, point12, point22, full_triangulation=this_hole_triangulation)) - - polys_to_add_boundary = shapely.node(MultiLineString([nhb1, nhb2, path1, path2])) + path1 = LineString( + shortest_path_in_polygon( + this_hole, + point11, + point21, + full_triangulation=this_hole_triangulation, + ) + ) + path2 = LineString( + shortest_path_in_polygon( + this_hole, + point12, + point22, + full_triangulation=this_hole_triangulation, + ) + ) + + polys_to_add_boundary = shapely.node( + MultiLineString([nhb1, nhb2, path1, path2]) + ) polys_to_add = polygonize(polys_to_add_boundary) - - hole_partition_boundary = shapely.node(MultiLineString(list(this_hole_boundaries_df["geometry"]) + [path1, path2])) - hole_partition_polys = polygonize(hole_partition_boundary) + + hole_partition_boundary = shapely.node( + MultiLineString( + list(this_hole_boundaries_df["geometry"]) + + [path1, path2] + ) + ) + hole_partition_polys = polygonize( + hole_partition_boundary + ) # polys_to_add will consist of either 1 or 2 polygons, # each sharing a positive-length boundary with exactly one of # geom1, geom2. # Add each polygon to the geometry that it shares a boundary with. - -# FIX: In rare cases, taking the difference of this_hole and poly_to_add goes wrong due to -# some precision problem in GEOS. Avoid this by polygonizing the hole boundary along with -# the new boundaries and replacing the hole with the unary union of the pieces that are -# NOT the triangles we want to remove. - - - + + # FIX: In rare cases, taking the difference of this_hole and poly_to_add goes wrong due to + # some precision problem in GEOS. Avoid this by polygonizing the hole boundary along with + # the new boundaries and replacing the hole with the unary union of the pieces that are + # NOT the triangles we want to remove. + nhb1_segments = segments(nhb1) nhb2_segments = segments(nhb2) for poly_to_add in polys_to_add: @@ -1030,58 +1707,185 @@ def smart_close_gaps(geometries_df, holes_df): # Cover all bases with both possible orientations for # boundary segments, even though the proper orientation # SHOULD always be correct. - poly_segments_oriented = segments(poly_to_add.boundary) - poly_segments_reverse = [shapely.reverse(segment) for segment in poly_segments_oriented] - poly_segments_all = set(poly_segments_oriented + poly_segments_reverse) - if (len(set(nhb1_segments).intersection(poly_segments_all)) > 0) and (len(set(nhb2_segments).intersection(poly_segments_all)) == 0): - geometries_df.loc[geom1, "geometry"] = unary_union([geometries_df.loc[geom1, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] - #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] - #this_hole = this_hole.difference(poly_to_add) - - elif (len(set(nhb1_segments).intersection(poly_segments_all)) == 0) and (len(set(nhb2_segments).intersection(poly_segments_all)) > 0): - geometries_df.loc[geom2, "geometry"] = unary_union([geometries_df.loc[geom2, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] - #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] - #this_hole = this_hole.difference(poly_to_add) + poly_segments_oriented = segments( + poly_to_add.boundary + ) + poly_segments_reverse = [ + shapely.reverse(segment) + for segment in poly_segments_oriented + ] + poly_segments_all = set( + poly_segments_oriented + poly_segments_reverse + ) + if ( + len( + set(nhb1_segments).intersection( + poly_segments_all + ) + ) + > 0 + ) and ( + len( + set(nhb2_segments).intersection( + poly_segments_all + ) + ) + == 0 + ): + geometries_df.loc[geom1, "geometry"] = ( + unary_union( + [ + geometries_df.loc[ + geom1, "geometry" + ], + poly_to_add, + ] + ) + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other(poly, poly_to_add) + ] + # hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + # this_hole = this_hole.difference(poly_to_add) + + elif ( + len( + set(nhb1_segments).intersection( + poly_segments_all + ) + ) + == 0 + ) and ( + len( + set(nhb2_segments).intersection( + poly_segments_all + ) + ) + > 0 + ): + geometries_df.loc[geom2, "geometry"] = ( + unary_union( + [ + geometries_df.loc[ + geom2, "geometry" + ], + poly_to_add, + ] + ) + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other(poly, poly_to_add) + ] + # hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + # this_hole = this_hole.difference(poly_to_add) elif geom1 == geom2: - geometries_df.loc[geom1, "geometry"] = unary_union([geometries_df.loc[geom1, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] - #hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] - #this_hole = this_hole.difference(poly_to_add) - - # It's possible with this new construction that the boundary of + geometries_df.loc[geom1, "geometry"] = ( + unary_union( + [ + geometries_df.loc[ + geom1, "geometry" + ], + poly_to_add, + ] + ) + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other(poly, poly_to_add) + ] + # hole_partition_polys = [poly for poly in hole_partition_polys if shapely.normalize(poly) != shapely.normalize(poly_to_add)] + # this_hole = this_hole.difference(poly_to_add) + + # It's possible with this new construction that the boundary of # poly_to_add could intersect both nhb1 and nhb2 nontrivially. # In this case, join it to the one that it intersects with - # longer perimeter. - - elif (len(set(nhb1_segments).intersection(poly_segments_all)) > 0) and (len(set(nhb2_segments).intersection(poly_segments_all)) > 0): + # longer perimeter. + + elif ( + len( + set(nhb1_segments).intersection( + poly_segments_all + ) + ) + > 0 + ) and ( + len( + set(nhb2_segments).intersection( + poly_segments_all + ) + ) + > 0 + ): print("It happened!") - perim1 = linemerge(list(set(nhb1_segments).intersection(poly_segments_all))).length - perim2 = linemerge(list(set(nhb2_segments).intersection(poly_segments_all))).length + perim1 = linemerge( + list( + set(nhb1_segments).intersection( + poly_segments_all + ) + ) + ).length + perim2 = linemerge( + list( + set(nhb2_segments).intersection( + poly_segments_all + ) + ) + ).length if perim1 > perim2: - geometries_df.loc[geom1, "geometry"] = unary_union([geometries_df.loc[geom1, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] + geometries_df.loc[geom1, "geometry"] = ( + unary_union( + [ + geometries_df.loc[ + geom1, "geometry" + ], + poly_to_add, + ] + ) + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other( + poly, poly_to_add + ) + ] else: - geometries_df.loc[geom2, "geometry"] = unary_union([geometries_df.loc[geom2, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] - - - - - #print("Internal triangle construction went weird!") - #print(len(set(nhb1_segments).intersection(poly_segments_all)), len(set(nhb2_segments).intersection(poly_segments_all))) - #print(geom1, geom2) - #print("Temp crossing point:", list(temp_crossing_pt.coords)) - #print("Crossing point:", list(crossing_pt.coords)) - #print("Paths:", [list(x.coords) for x in paths]) - #print("Hole boundaries:") - #for i in this_hole_boundaries_df.index: + geometries_df.loc[geom2, "geometry"] = ( + unary_union( + [ + geometries_df.loc[ + geom2, "geometry" + ], + poly_to_add, + ] + ) + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other( + poly, poly_to_add + ) + ] + + # print("Internal triangle construction went weird!") + # print(len(set(nhb1_segments).intersection(poly_segments_all)), len(set(nhb2_segments).intersection(poly_segments_all))) + # print(geom1, geom2) + # print("Temp crossing point:", list(temp_crossing_pt.coords)) + # print("Crossing point:", list(crossing_pt.coords)) + # print("Paths:", [list(x.coords) for x in paths]) + # print("Hole boundaries:") + # for i in this_hole_boundaries_df.index: # print("Target:", this_hole_boundaries_df.loc[i, "target"]) # print(list(this_hole_boundaries_df.loc[i, "geometry"].coords)) - #print("poly_to_add boundaries:") - #print(list(poly_to_add.boundary.coords)) + # print("poly_to_add boundaries:") + # print(list(poly_to_add.boundary.coords)) # Now put the new hole(s) created by removing triangles back in the queue: if found_triangles and len(hole_partition_polys) > 0: @@ -1089,13 +1893,13 @@ def smart_close_gaps(geometries_df, holes_df): holes_to_process.extend(holes_to_add) pbar_increment -= len(holes_to_add) -# if found_triangles and not this_hole.is_empty: -# if this_hole.geom_type == "MultiPolygon": # 2 holes to add -# holes_to_add = [orient(geom) for geom in this_hole.geoms] -# elif this_hole.geom_type == "Polygon": # 1 hole to add -# holes_to_add = [orient(this_hole)] -# holes_to_process.extend(holes_to_add) -# pbar_increment -= len(holes_to_add) + # if found_triangles and not this_hole.is_empty: + # if this_hole.geom_type == "MultiPolygon": # 2 holes to add + # holes_to_add = [orient(geom) for geom in this_hole.geoms] + # elif this_hole.geom_type == "Polygon": # 1 hole to add + # holes_to_add = [orient(this_hole)] + # holes_to_process.extend(holes_to_add) + # pbar_increment -= len(holes_to_add) elif found_triangles is False: # This is rare, but it does happen occasionally in the scenario where @@ -1109,12 +1913,20 @@ def smart_close_gaps(geometries_df, holes_df): shared_perimeters = [] for i in this_hole_boundaries_df.index: if this_hole_boundaries_df.loc[i, "target"] != -1: - shared_perimeters.append((this_hole_boundaries_df.loc[i, "target"], this_hole_boundaries_df.loc[i, "geometry"].length)) + shared_perimeters.append( + ( + this_hole_boundaries_df.loc[i, "target"], + this_hole_boundaries_df.loc[i, "geometry"].length, + ) + ) if len(shared_perimeters) > 0: - max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] + max_shared_perim = sorted( + shared_perimeters, key=lambda tup: tup[1] + )[-1] poly_to_add_to = max_shared_perim[0] geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( - [geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) + [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] + ) pbar.update(pbar_increment) @@ -1148,11 +1960,15 @@ def small_rook_to_queen(geometries_df, min_rook_length): for ind in small_adj_df.index: if small_adj_df.loc[ind, "geometry"].geom_type == "GeometryCollection": small_adj_list = list(small_adj_df.loc[ind, "geometry"].geoms) - small_adj_list_no_point = [x for x in small_adj_list if x.geom_type != "Point"] + small_adj_list_no_point = [ + x for x in small_adj_list if x.geom_type != "Point" + ] small_adj_df.loc[ind, "geometry"] = MultiLineString(small_adj_list_no_point) if small_adj_df.loc[ind, "geometry"].geom_type == "MultiLineString": - small_adj_df.loc[ind, "geometry"] = linemerge(small_adj_df.loc[ind, "geometry"]) + small_adj_df.loc[ind, "geometry"] = linemerge( + small_adj_df.loc[ind, "geometry"] + ) small_adj_df = small_adj_df.explode(index_parts=False).reset_index(drop=True) @@ -1170,7 +1986,9 @@ def small_rook_to_queen(geometries_df, min_rook_length): for a_ind in small_adj_df.index: this_adj = small_adj_df.loc[a_ind, "geometry"] adj_diam = this_adj.length - fat_point_radius = 0.6*adj_diam # slightly more than the radius from the midpoint to the endpoints + fat_point_radius = ( + 0.6 * adj_diam + ) # slightly more than the radius from the midpoint to the endpoints endpoint1 = this_adj.coords[0] endpoint2 = this_adj.coords[-1] midpoint = LineString([endpoint1, endpoint2]).centroid @@ -1185,12 +2003,16 @@ def small_rook_to_queen(geometries_df, min_rook_length): polys_to_remove_complete = False while polys_to_remove_complete is False: all_polys_to_remove = unary_union(polys_to_remove_list) - if all_polys_to_remove.geom_type == "Polygon": # if it's all one big polygon now + if ( + all_polys_to_remove.geom_type == "Polygon" + ): # if it's all one big polygon now merged_polys_to_remove_list = [all_polys_to_remove] else: merged_polys_to_remove_list = list(all_polys_to_remove.geoms) - convex_polys_to_remove_list = [shapely.convex_hull(x) for x in merged_polys_to_remove_list] + convex_polys_to_remove_list = [ + shapely.convex_hull(x) for x in merged_polys_to_remove_list + ] if len(convex_polys_to_remove_list) == 1: polys_to_remove_complete = True @@ -1198,25 +2020,35 @@ def small_rook_to_queen(geometries_df, min_rook_length): # Note that if the unary union is a Polygon, then this next condition # below can't hold anyway and we want polys_to_remove_complete to remain # False. - if len(unary_union(convex_polys_to_remove_list).geoms) == len(convex_polys_to_remove_list): + if len(unary_union(convex_polys_to_remove_list).geoms) == len( + convex_polys_to_remove_list + ): polys_to_remove_complete = True polys_to_remove_list = convex_polys_to_remove_list # Build an STRtree to use for finding intersecting geometries. g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) + g_index_by_iloc = dict( + (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) + ) for a_ind in range(len(polys_to_remove_list)): poly_to_remove = polys_to_remove_list[a_ind] # Identify geometries that might intersect this polygon. - possible_geom_integer_indices = list(set(g_spatial_index.query(poly_to_remove).ravel())) - possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] + possible_geom_integer_indices = list( + set(g_spatial_index.query(poly_to_remove).ravel()) + ) + possible_geom_indices = [ + g_index_by_iloc[k] for k in possible_geom_integer_indices + ] # Use the boundaries of these geometries together with the boundary of the disk to # polygonize and divide geometries into pieces inside and outside the disk. - boundaries = [geometries_df.loc[i, "geometry"].boundary for i in possible_geom_indices] + boundaries = [ + geometries_df.loc[i, "geometry"].boundary for i in possible_geom_indices + ] boundaries.append(LineString(list(poly_to_remove.exterior.coords))) boundaries_exploded = [] @@ -1227,21 +2059,33 @@ def small_rook_to_queen(geometries_df, min_rook_length): boundaries_exploded += list(geom.geoms) boundaries_union = shapely.node(MultiLineString(boundaries_exploded)) - pieces_df = GeoDataFrame(columns=["polygon indices"], - geometry=GeoSeries(list(polygonize(boundaries_union))), - crs=geometries_df.crs) + pieces_df = GeoDataFrame( + columns=["polygon indices"], + geometry=GeoSeries(list(polygonize(boundaries_union))), + crs=geometries_df.crs, + ) # Associate the pieces to the main geometries. (Note that if there are # gaps, some pieces may be unassigned.) pieces_df["polygon indices"] = [set() for x in range(len(pieces_df.index))] for i in pieces_df.index: - temp_possible_geom_integer_indices = list(set(g_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel())) - temp_possible_geom_indices = [g_index_by_iloc[k] for k in temp_possible_geom_integer_indices] + temp_possible_geom_integer_indices = list( + set(g_spatial_index.query(pieces_df.loc[i, "geometry"]).ravel()) + ) + temp_possible_geom_indices = [ + g_index_by_iloc[k] for k in temp_possible_geom_integer_indices + ] for j in temp_possible_geom_indices: - if pieces_df.loc[i, "geometry"].representative_point().intersects(geometries_df.loc[j, "geometry"]): - pieces_df.loc[i, "polygon indices"] = pieces_df.loc[i, "polygon indices"].union({j}) + if ( + pieces_df.loc[i, "geometry"] + .representative_point() + .intersects(geometries_df.loc[j, "geometry"]) + ): + pieces_df.loc[i, "polygon indices"] = pieces_df.loc[ + i, "polygon indices" + ].union({j}) # Now rebuild the disk from the pieces that are inside the circle, and drop them from # pieces_df. Then we'll give the pieces outside the circle back to the geometries that they came from. @@ -1250,8 +2094,14 @@ def small_rook_to_queen(geometries_df, min_rook_length): pieces_df_indices_to_drop = [] for p_ind in pieces_df.index: - if pieces_df.loc[p_ind, "geometry"].representative_point().intersects(poly_to_remove): - poly_to_remove_refined = unary_union([poly_to_remove_refined, pieces_df.loc[p_ind, "geometry"]]) + if ( + pieces_df.loc[p_ind, "geometry"] + .representative_point() + .intersects(poly_to_remove) + ): + poly_to_remove_refined = unary_union( + [poly_to_remove_refined, pieces_df.loc[p_ind, "geometry"]] + ) pieces_df_indices_to_drop.append(p_ind) if len(pieces_df_indices_to_drop) > 0: pieces_df = pieces_df.drop(pieces_df_indices_to_drop) @@ -1260,37 +2110,65 @@ def small_rook_to_queen(geometries_df, min_rook_length): geometries_df.loc[g_ind, "geometry"] = Polygon() for p_ind in pieces_df.index: - if len(pieces_df.loc[p_ind, "polygon indices"]) == 1: # Note that it won't be >1 if the file is clean! + if ( + len(pieces_df.loc[p_ind, "polygon indices"]) == 1 + ): # Note that it won't be >1 if the file is clean! this_poly_ind = list(pieces_df.loc[p_ind, "polygon indices"])[0] this_piece = pieces_df.loc[p_ind, "geometry"] if this_poly_ind in possible_geom_indices: # This check is needed because the geometries in possible_geom_incides can form a # non-simply-connected region, in which case the interior holes - which may consist # of multiple geometries each - may be assigned someplace they shouldn't be! - geometries_df.loc[this_poly_ind, "geometry"] = unary_union([geometries_df.loc[this_poly_ind, "geometry"], this_piece]) + geometries_df.loc[this_poly_ind, "geometry"] = unary_union( + [geometries_df.loc[this_poly_ind, "geometry"], this_piece] + ) # Find the boundary arcs between geometries and poly_to_remove_refined (and make sure each arc is a connected piece): possible_geoms = geometries_df.loc[possible_geom_indices] - poly_to_remove_boundaries_df = intersections(GeoDataFrame(geometry=GeoSeries([poly_to_remove_refined], crs=geometries_df.crs)), possible_geoms, output_type="geodataframe") - poly_to_remove_boundaries_df = poly_to_remove_boundaries_df[poly_to_remove_boundaries_df.length > 0] + poly_to_remove_boundaries_df = intersections( + GeoDataFrame( + geometry=GeoSeries([poly_to_remove_refined], crs=geometries_df.crs) + ), + possible_geoms, + output_type="geodataframe", + ) + poly_to_remove_boundaries_df = poly_to_remove_boundaries_df[ + poly_to_remove_boundaries_df.length > 0 + ] for b_ind in poly_to_remove_boundaries_df.index: - if poly_to_remove_boundaries_df.loc[b_ind, "geometry"].geom_type == "MultiLineString": - poly_to_remove_boundaries_df.loc[b_ind, "geometry"] = linemerge(poly_to_remove_boundaries_df.loc[b_ind, "geometry"]) - - poly_to_remove_boundaries_df = poly_to_remove_boundaries_df.explode(index_parts=False).reset_index(drop=True) + if ( + poly_to_remove_boundaries_df.loc[b_ind, "geometry"].geom_type + == "MultiLineString" + ): + poly_to_remove_boundaries_df.loc[b_ind, "geometry"] = linemerge( + poly_to_remove_boundaries_df.loc[b_ind, "geometry"] + ) + + poly_to_remove_boundaries_df = poly_to_remove_boundaries_df.explode( + index_parts=False + ).reset_index(drop=True) poly_to_remove_centroid_coords = poly_to_remove_refined.centroid.coords[0] # For each boundary arc, create a "pie wedge" from the center of poly_to_remove_refined # subtending this arc. (Since the polygon is convex, these are guaranteed to piece # together nicely.) for b_ind in poly_to_remove_boundaries_df.index: - boundary_arc_coords = list(poly_to_remove_boundaries_df.loc[b_ind, "geometry"].coords) - boundary_wedge_coords = boundary_arc_coords + [poly_to_remove_centroid_coords] + boundary_arc_coords = list( + poly_to_remove_boundaries_df.loc[b_ind, "geometry"].coords + ) + boundary_wedge_coords = boundary_arc_coords + [ + poly_to_remove_centroid_coords + ] g_ind = poly_to_remove_boundaries_df.loc[b_ind, "target"] - geometries_df.loc[g_ind, "geometry"] = unary_union([geometries_df.loc[g_ind, "geometry"], Polygon(boundary_wedge_coords)]) + geometries_df.loc[g_ind, "geometry"] = unary_union( + [ + geometries_df.loc[g_ind, "geometry"], + Polygon(boundary_wedge_coords), + ] + ) return geometries_df @@ -1313,10 +2191,14 @@ def construct_hole_boundaries(geometries_df, holes_df): # Start by constructing an STRtree to find geometries that may intersect gaps. g_spatial_index = STRtree(geometries_df["geometry"]) - g_index_by_iloc = dict((i, list(geometries_df.index)[i]) for i in range(len(geometries_df))) + g_index_by_iloc = dict( + (i, list(geometries_df.index)[i]) for i in range(len(geometries_df)) + ) # Initialize the geodataframe for the gap boundaries - hole_boundaries_df = GeoDataFrame(columns=["source", "target"], geometry=GeoSeries([]), crs=geometries_df.crs) + hole_boundaries_df = GeoDataFrame( + columns=["source", "target"], geometry=GeoSeries([]), crs=geometries_df.crs + ) # For each gap and each geometry that it might possibly intersect, find all # common LineStrings in their boundaries (if any) and take their unary union to @@ -1327,8 +2209,12 @@ def construct_hole_boundaries(geometries_df, holes_df): this_hole_segments = segments(this_hole.boundary) this_hole_segments_used = [] - possible_geom_integer_indices = list(set(g_spatial_index.query(holes_df.loc[h_ind, "geometry"]).ravel())) - possible_geom_indices = [g_index_by_iloc[k] for k in possible_geom_integer_indices] + possible_geom_integer_indices = list( + set(g_spatial_index.query(holes_df.loc[h_ind, "geometry"]).ravel()) + ) + possible_geom_indices = [ + g_index_by_iloc[k] for k in possible_geom_integer_indices + ] for g_ind in possible_geom_indices: @@ -1350,26 +2236,49 @@ def construct_hole_boundaries(geometries_df, holes_df): for component in this_geom_boundary_components: this_geom_segments = this_geom_segments.union(set(segments(component))) - this_hole_this_geom_segments = [segment for segment in this_hole_segments if (segment in this_geom_segments or shapely.reverse(segment) in this_geom_segments)] + this_hole_this_geom_segments = [ + segment + for segment in this_hole_segments + if ( + segment in this_geom_segments + or shapely.reverse(segment) in this_geom_segments + ) + ] if len(this_hole_this_geom_segments) > 0: this_hole_segments_used += this_hole_this_geom_segments - this_hole_boundary_df = GeoDataFrame(geometry=GeoSeries([linemerge(this_hole_this_geom_segments)]), crs=geometries_df.crs) + this_hole_boundary_df = GeoDataFrame( + geometry=GeoSeries([linemerge(this_hole_this_geom_segments)]), + crs=geometries_df.crs, + ) this_hole_boundary_df.insert(0, "source", h_ind) this_hole_boundary_df.insert(1, "target", g_ind) - hole_boundaries_df = pandas.concat([hole_boundaries_df, this_hole_boundary_df]).reset_index(drop=True) + hole_boundaries_df = pandas.concat( + [hole_boundaries_df, this_hole_boundary_df] + ).reset_index(drop=True) # Finally, check for any exterior boundary: if len(this_hole_segments) > len(this_hole_segments_used): - exterior_segments = [segment for segment in this_hole_segments if segment not in this_hole_segments_used] - this_hole_exterior_boundary_df = GeoDataFrame(geometry=GeoSeries([linemerge(exterior_segments)]), crs=geometries_df.crs) + exterior_segments = [ + segment + for segment in this_hole_segments + if segment not in this_hole_segments_used + ] + this_hole_exterior_boundary_df = GeoDataFrame( + geometry=GeoSeries([linemerge(exterior_segments)]), + crs=geometries_df.crs, + ) this_hole_exterior_boundary_df.insert(0, "source", h_ind) this_hole_exterior_boundary_df.insert(1, "target", -1) - hole_boundaries_df = pandas.concat([hole_boundaries_df, this_hole_exterior_boundary_df]).reset_index(drop=True) + hole_boundaries_df = pandas.concat( + [hole_boundaries_df, this_hole_exterior_boundary_df] + ).reset_index(drop=True) - hole_boundaries_df = hole_boundaries_df.explode(index_parts=False).reset_index(drop=True) + hole_boundaries_df = hole_boundaries_df.explode(index_parts=False).reset_index( + drop=True + ) return hole_boundaries_df @@ -1397,18 +2306,21 @@ def incenter(triangle): # The incenter will be a weighted average of the coordinates of the vertices, # with coefficients proportional to a,b,c. - alpha = a/(a + b + c) - beta = b/(a + b + c) - gamma = c/(a + b + c) + alpha = a / (a + b + c) + beta = b / (a + b + c) + gamma = c / (a + b + c) - x_i = alpha*x_a + beta*x_b + gamma*x_c - y_i = alpha*y_a + beta*y_b + gamma*y_c + x_i = alpha * x_a + beta * x_b + gamma * x_c + y_i = alpha * y_a + beta * y_b + gamma * y_c # Occasionally for very tiny triangles, rounding errors produce a point not # contained in the triangle. In this case, replace the computed point with # the nearest vertex of the triangle. if not triangle.contains(Point(x_i, y_i)): - point_to_return = nearest_points(Point(x_i, y_i), MultiPoint([Point(x_a, y_a), Point(x_b, y_b), Point(x_c, y_c)]))[1] + point_to_return = nearest_points( + Point(x_i, y_i), + MultiPoint([Point(x_a, y_a), Point(x_b, y_b), Point(x_c, y_c)]), + )[1] else: point_to_return = Point(x_i, y_i) @@ -1429,9 +2341,17 @@ def triangulate_polygon(polygon): # Find an ear to cut from the polygon and add it to the list of triangles. for i in range(len(poly_vertices)): - triangle_to_check = Polygon([poly_vertices[i-1], poly_vertices[i], poly_vertices[i+1]]) - if poly.contains(triangle_to_check) and MultiPoint([poly_vertices[i-1], poly_vertices[i+1]]).contains(LineString([poly_vertices[i-1], poly_vertices[i+1]]).intersection(poly.boundary)): - #if poly.contains(triangle_to_check) and LineString([poly_vertices[i-1], poly_vertices[i+1]]).intersection(poly.boundary).difference(MultiPoint([poly_vertices[i-1], poly_vertices[i+1]])).is_empty: + triangle_to_check = Polygon( + [poly_vertices[i - 1], poly_vertices[i], poly_vertices[i + 1]] + ) + if poly.contains(triangle_to_check) and MultiPoint( + [poly_vertices[i - 1], poly_vertices[i + 1]] + ).contains( + LineString([poly_vertices[i - 1], poly_vertices[i + 1]]).intersection( + poly.boundary + ) + ): + # if poly.contains(triangle_to_check) and LineString([poly_vertices[i-1], poly_vertices[i+1]]).intersection(poly.boundary).difference(MultiPoint([poly_vertices[i-1], poly_vertices[i+1]])).is_empty: triangles.append(triangle_to_check) poly_vertices_reordered = poly_vertices[i:] + poly_vertices[0:i] poly = Polygon(poly_vertices_reordered[1:]) @@ -1454,19 +2374,27 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): the same polygon. """ if not (polygon.is_valid and polygon.geom_type == "Polygon"): - raise TypeError("shortest_path_in_polygon: Input polygon must be a valid Polygon.") + raise TypeError( + "shortest_path_in_polygon: Input polygon must be a valid Polygon." + ) if not extract_unique_points(polygon).contains(MultiPoint([start, end])): - raise TypeError("shortest_path_in_polygon: Start and end points must be vertices of the polygon.") + raise TypeError( + "shortest_path_in_polygon: Start and end points must be vertices of the polygon." + ) # First check for the easy case: If the line segment between the start and end points is # contained in the polygon, then that's the shortest path. (And the rest of the algorithm # won't work correctly because the simplified polygon will degenerate.) - - if MultiPoint([start, end]).contains(LineString([start, end]).intersection(polygon.boundary)) and polygon.contains(LineString([start, end])): - #if polygon.contains(LineString([start, end])) and set(LineString([start, end]).intersection(polygon.boundary).geoms) == {start, end}: + + if MultiPoint([start, end]).contains( + LineString([start, end]).intersection(polygon.boundary) + ) and polygon.contains(LineString([start, end])): + # if polygon.contains(LineString([start, end])) and set(LineString([start, end]).intersection(polygon.boundary).geoms) == {start, end}: return [start, end] - elif LineString([start, end]) in segments(polygon.boundary) or LineString([end, start]) in segments(polygon.boundary): + elif LineString([start, end]) in segments(polygon.boundary) or LineString( + [end, start] + ) in segments(polygon.boundary): return [start, end] else: @@ -1480,17 +2408,25 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): start_index = boundary_points.index(start) end_index = boundary_points.index(end) if start_index < end_index: - path_1 = LineString(boundary_points[start_index:end_index+1]) - path_2 = LineString(boundary_points[end_index:] + boundary_points[0:start_index+1]) + path_1 = LineString(boundary_points[start_index : end_index + 1]) + path_2 = LineString( + boundary_points[end_index:] + boundary_points[0 : start_index + 1] + ) else: - path_1 = LineString(boundary_points[start_index:] + boundary_points[0:end_index+1]) - path_2 = LineString(boundary_points[end_index:start_index+1]) - - if (extract_unique_points(path_1).geoms[0] == start) and (extract_unique_points(path_2).geoms[0] == end): + path_1 = LineString( + boundary_points[start_index:] + boundary_points[0 : end_index + 1] + ) + path_2 = LineString(boundary_points[end_index : start_index + 1]) + + if (extract_unique_points(path_1).geoms[0] == start) and ( + extract_unique_points(path_2).geoms[0] == end + ): right_path = path_1 left_path = shapely.reverse(path_2) - elif (extract_unique_points(path_2).geoms[0] == start) and (extract_unique_points(path_1).geoms[0] == end): + elif (extract_unique_points(path_2).geoms[0] == start) and ( + extract_unique_points(path_1).geoms[0] == end + ): right_path = path_2 left_path = shapely.reverse(path_1) @@ -1505,25 +2441,40 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): triangulation = [] for triangle in full_triangulation: - if not triangle.boundary.intersection(MultiPoint(right_path_points[1:-1])).is_empty and not triangle.boundary.intersection(MultiPoint(left_path_points[1:-1])).is_empty: + if ( + not triangle.boundary.intersection( + MultiPoint(right_path_points[1:-1]) + ).is_empty + and not triangle.boundary.intersection( + MultiPoint(left_path_points[1:-1]) + ).is_empty + ): triangulation.append(triangle) # Put the triangles for the sleeve in the correct order: - initial_triangle = [triangle for triangle in triangulation if start in extract_unique_points(triangle.boundary).geoms][0] + initial_triangle = [ + triangle + for triangle in triangulation + if start in extract_unique_points(triangle.boundary).geoms + ][0] ordered_triangulation = [initial_triangle] triangulation.remove(initial_triangle) while len(triangulation) > 0: leading_triangle = ordered_triangulation[-1] - next_triangle = [triangle for triangle in triangulation if leading_triangle.intersection(triangle).geom_type == "LineString"][0] + next_triangle = [ + triangle + for triangle in triangulation + if leading_triangle.intersection(triangle).geom_type == "LineString" + ][0] ordered_triangulation.append(next_triangle) triangulation.remove(next_triangle) # Regard the sleeve given by the union of these triangles as the "simplified" # polygon; the shortest path must be contained in this simplfied polygon. polygon_simplified = unary_union(ordered_triangulation) - + # Now use the ordered triangulation to order the vertices of the simplified polygon, # as well as the left and right paths restricted to the simplified polygon. ordered_path_vertices = [start] @@ -1532,8 +2483,12 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): for triangle in ordered_triangulation: this_triangle_vertices = set(extract_unique_points(triangle.boundary).geoms) - this_triangle_new_vertices = this_triangle_vertices.difference(set(ordered_path_vertices)) - ordered_path_vertices = ordered_path_vertices + list(this_triangle_new_vertices) + this_triangle_new_vertices = this_triangle_vertices.difference( + set(ordered_path_vertices) + ) + ordered_path_vertices = ordered_path_vertices + list( + this_triangle_new_vertices + ) for vertex in this_triangle_new_vertices: if vertex in right_path_points: right_path_simplified_points.append(vertex) @@ -1544,7 +2499,10 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): found_shortest_path = [start] left_funnel = [left_path_simplified_points[0], left_path_simplified_points[1]] - right_funnel = [right_path_simplified_points[0], right_path_simplified_points[1]] + right_funnel = [ + right_path_simplified_points[0], + right_path_simplified_points[1], + ] # We've already used the first 3 points on this list, so take them out. ordered_path_vertices = ordered_path_vertices[3:] @@ -1570,48 +2528,73 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): # vertex on the *other* funnel; it's guaranteed to be reflex. Make it # the new apex, and add the other funnel up to this point to # found_shortest_path. - - if polygon_simplified.contains(LineString([apex, point])) or polygon_simplified.boundary.contains(LineString([apex, point])): + + if polygon_simplified.contains( + LineString([apex, point]) + ) or polygon_simplified.boundary.contains(LineString([apex, point])): new_funnel_points = [apex] for i in range(1, len(this_funnel)): - if LineString([apex, point]).contains(LineString([this_funnel[i], point])): - new_funnel_points.append(this_funnel[i]) + if LineString([apex, point]).contains( + LineString([this_funnel[i], point]) + ): + new_funnel_points.append(this_funnel[i]) this_funnel = new_funnel_points + [point] else: for i in range(1, len(this_funnel)): - if polygon_simplified.contains(LineString([this_funnel[i], point])) or polygon_simplified.boundary.contains(LineString([this_funnel[i], point])): + if polygon_simplified.contains( + LineString([this_funnel[i], point]) + ) or polygon_simplified.boundary.contains( + LineString([this_funnel[i], point]) + ): first_seen = i break - - seg1 = list(LineString([this_funnel[first_seen-1], this_funnel[first_seen]]).coords) + + seg1 = list( + LineString( + [this_funnel[first_seen - 1], this_funnel[first_seen]] + ).coords + ) seg2 = list(LineString([this_funnel[first_seen], point]).coords) vec1 = (seg1[1][0] - seg1[0][0], seg1[1][1] - seg1[0][1]) vec2 = (seg2[1][0] - seg2[0][0], seg2[1][1] - seg2[0][1]) - cross_prod = vec1[0]*vec2[1] - vec1[1]*vec2[0] + cross_prod = vec1[0] * vec2[1] - vec1[1] * vec2[0] - if cross_prod*reflex_sign >= 0: + if cross_prod * reflex_sign >= 0: # If this vertex is reflex: - new_funnel_points = this_funnel[0:first_seen+1] - for i in range(first_seen+1, len(this_funnel)): - if LineString([this_funnel[first_seen], point]).contains(LineString([this_funnel[i], point])): - new_funnel_points.append(this_funnel[i]) + new_funnel_points = this_funnel[0 : first_seen + 1] + for i in range(first_seen + 1, len(this_funnel)): + if LineString([this_funnel[first_seen], point]).contains( + LineString([this_funnel[i], point]) + ): + new_funnel_points.append(this_funnel[i]) this_funnel = new_funnel_points + [point] - + else: - first_seen = min(i for i in range(1, len(other_funnel)) if polygon_simplified.contains(LineString([other_funnel[i], point])) or polygon_simplified.boundary.contains(LineString([other_funnel[i], point]))) - found_shortest_path += other_funnel[1: first_seen+1] + first_seen = min( + i + for i in range(1, len(other_funnel)) + if polygon_simplified.contains( + LineString([other_funnel[i], point]) + ) + or polygon_simplified.boundary.contains( + LineString([other_funnel[i], point]) + ) + ) + found_shortest_path += other_funnel[1 : first_seen + 1] apex = other_funnel[first_seen] - #new_funnel_points = [apex] + # new_funnel_points = [apex] other_funnel_start_index = first_seen - for i in range(first_seen+1, len(other_funnel)): - if LineString([apex, point]).contains(LineString([other_funnel[i], point])): + for i in range(first_seen + 1, len(other_funnel)): + if LineString([apex, point]).contains( + LineString([other_funnel[i], point]) + ): found_shortest_path.append(other_funnel[i]) apex = other_funnel[i] other_funnel_start_index = i - + this_funnel = [apex, point] other_funnel = other_funnel[other_funnel_start_index:] @@ -1646,15 +2629,22 @@ def convexify_hole_boundaries(geometries_df, holes_df): geometries_df = geometries_df.copy() holes_df = holes_df.copy() - completed_holes_df = GeoDataFrame(columns=["region"], geometry=GeoSeries([]), crs=holes_df.crs) + completed_holes_df = GeoDataFrame( + columns=["region"], geometry=GeoSeries([]), crs=holes_df.crs + ) if len(holes_df) > 0: holes_to_process = deque(list(holes_df["geometry"])) - this_region = list(holes_df["region"])[0] # All holes in this dataframe should be from the same region + this_region = list(holes_df["region"])[ + 0 + ] # All holes in this dataframe should be from the same region if this_region is None: pbar = tqdm(desc="Gaps to simplify", total=len(holes_to_process)) else: - pbar = tqdm(desc=f"Gaps to simplify in region {this_region}", total=len(holes_to_process)) + pbar = tqdm( + desc=f"Gaps to simplify in region {this_region}", + total=len(holes_to_process), + ) else: holes_to_process = deque([]) pbar = tqdm(desc="Gaps to simplify", total=len(holes_to_process)) @@ -1670,12 +2660,20 @@ def convexify_hole_boundaries(geometries_df, holes_df): # This is probably a small component of a region that isn't assigned to # any geometry in that region. Just leave it alone and let it be a hole. if this_region is not None: - print("Found a component of the region at index", this_region, "that does not intersect any geometry assigned to that region.") + print( + "Found a component of the region at index", + this_region, + "that does not intersect any geometry assigned to that region.", + ) elif len(set(this_hole_boundaries_df["target"]).difference({-1})) == 1: # Attach the hole to the unique non-exterior geometry that it intersects: - poly_to_add_to = list(set(this_hole_boundaries_df["target"]).difference({-1}))[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union([geometries_df.loc[poly_to_add_to, "geometry"], this_hole]) + poly_to_add_to = list( + set(this_hole_boundaries_df["target"]).difference({-1}) + )[0] + geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] + ) else: # Each remaining hole intersects at least 2 geometries nontrivially. @@ -1689,22 +2687,26 @@ def convexify_hole_boundaries(geometries_df, holes_df): # after convexifying. If they do, their UNION isn't guaranteed to be # convexified at the end, so we'll put that hole back in the queue for # another round of processing. - if len(set(this_hole_boundaries_df["target"])) == len(this_hole_boundaries_df): + if len(set(this_hole_boundaries_df["target"])) == len( + this_hole_boundaries_df + ): target_repetition = False else: target_repetition = True repeated_targets = [] for target in set(this_hole_boundaries_df["target"]): - boundaries_this_target = this_hole_boundaries_df[this_hole_boundaries_df["target"] == target] + boundaries_this_target = this_hole_boundaries_df[ + this_hole_boundaries_df["target"] == target + ] if len(boundaries_this_target) > 1: repeated_targets.append((target, len(boundaries_this_target))) new_hole_in_progress = this_hole - + if new_hole_in_progress.boundary.geom_type == "MultiLineString": print(new_hole_in_progress.geom_type) print([list(x.coords) for x in new_hole_in_progress.boundary.geoms]) - + this_hole_triangulation = triangulate_polygon(new_hole_in_progress) for thb_ind in this_hole_boundaries_df.index: @@ -1715,26 +2717,41 @@ def convexify_hole_boundaries(geometries_df, holes_df): start = list(extract_unique_points(thb).geoms)[0] end = list(extract_unique_points(thb).geoms)[-1] - sp = LineString(shortest_path_in_polygon(this_hole, start, end, full_triangulation=this_hole_triangulation)) + sp = LineString( + shortest_path_in_polygon( + this_hole, + start, + end, + full_triangulation=this_hole_triangulation, + ) + ) polys_to_add_boundary = shapely.node(MultiLineString([thb, sp])) - hole_partition_boundary = shapely.node(unary_union([new_hole_in_progress.boundary, sp])) - #piece_to_add_boundary = unary_union([thb, sp]) - #if piece_to_add_boundary.geom_type == "MultiLineString": + hole_partition_boundary = shapely.node( + unary_union([new_hole_in_progress.boundary, sp]) + ) + # piece_to_add_boundary = unary_union([thb, sp]) + # if piece_to_add_boundary.geom_type == "MultiLineString": # piece_to_add_boundary = linemerge(piece_to_add_boundary) - + polys_to_add = polygonize(polys_to_add_boundary) - hole_partition_polys = polygonize(hole_partition_boundary) - + hole_partition_polys = polygonize(hole_partition_boundary) + for poly_to_add in polys_to_add: - geometries_df.loc[this_geom, "geometry"] = unary_union([geometries_df.loc[this_geom, "geometry"], poly_to_add]) - hole_partition_polys = [poly for poly in hole_partition_polys if not contain_each_other(poly, poly_to_add)] - + geometries_df.loc[this_geom, "geometry"] = unary_union( + [geometries_df.loc[this_geom, "geometry"], poly_to_add] + ) + hole_partition_polys = [ + poly + for poly in hole_partition_polys + if not contain_each_other(poly, poly_to_add) + ] + new_hole_in_progress = unary_union(hole_partition_polys) - #piece_to_add = unary_union(polygonize(piece_to_add_boundary)) - #geometries_df.loc[this_geom, "geometry"] = unary_union([geometries_df.loc[this_geom, "geometry"], piece_to_add]) - #new_hole_in_progress = new_hole_in_progress.difference(piece_to_add) + # piece_to_add = unary_union(polygonize(piece_to_add_boundary)) + # geometries_df.loc[this_geom, "geometry"] = unary_union([geometries_df.loc[this_geom, "geometry"], piece_to_add]) + # new_hole_in_progress = new_hole_in_progress.difference(piece_to_add) if not new_hole_in_progress.is_empty: if new_hole_in_progress.geom_type == "Polygon": @@ -1744,7 +2761,9 @@ def convexify_hole_boundaries(geometries_df, holes_df): for new_hole in new_holes: new_hole = orient(new_hole) - new_hole_df = GeoDataFrame(geometry=GeoSeries([new_hole]), crs=holes_df.crs) + new_hole_df = GeoDataFrame( + geometry=GeoSeries([new_hole]), crs=holes_df.crs + ) new_hole_df.insert(0, "region", this_region) if target_repetition: @@ -1754,19 +2773,30 @@ def convexify_hole_boundaries(geometries_df, holes_df): # may have been concatenated after convexifying, resulting in a # non-convex boundary - so put the hole back in the queue for # another round of processing. - new_hole_boundaries_df = construct_hole_boundaries(geometries_df, new_hole_df) + new_hole_boundaries_df = construct_hole_boundaries( + geometries_df, new_hole_df + ) reprocess_hole = False for target in repeated_targets: - new_boundaries_this_target = new_hole_boundaries_df[new_hole_boundaries_df["target"] == target[0]] - if len(new_boundaries_this_target) > 0 and len(new_boundaries_this_target) < target[1]: + new_boundaries_this_target = new_hole_boundaries_df[ + new_hole_boundaries_df["target"] == target[0] + ] + if ( + len(new_boundaries_this_target) > 0 + and len(new_boundaries_this_target) < target[1] + ): reprocess_hole = True break if reprocess_hole: holes_to_process.append(new_hole) else: - completed_holes_df = pandas.concat([completed_holes_df, new_hole_df]).reset_index(drop=True) + completed_holes_df = pandas.concat( + [completed_holes_df, new_hole_df] + ).reset_index(drop=True) else: - completed_holes_df = pandas.concat([completed_holes_df, new_hole_df]).reset_index(drop=True) + completed_holes_df = pandas.concat( + [completed_holes_df, new_hole_df] + ).reset_index(drop=True) pbar.update(pbar_increment) diff --git a/poetry.lock b/poetry.lock index 01bb230..214de98 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -68,6 +68,51 @@ soupsieve = ">1.2" html5lib = ["html5lib"] lxml = ["lxml"] +[[package]] +name = "black" +version = "25.1.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2023.7.22" @@ -180,6 +225,21 @@ files = [ {file = "charset_normalizer-3.3.1-py3-none-any.whl", hash = "sha256:800561453acdecedaac137bf09cd719c7a440b6800ec182f077bb8e7025fb708"}, ] +[[package]] +name = "click" +version = "8.2.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, + {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -191,7 +251,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} [[package]] name = "coverage" @@ -490,6 +550,18 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + [[package]] name = "myst-parser" version = "4.0.1" @@ -691,6 +763,35 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + [[package]] name = "pluggy" version = "1.6.0" @@ -1017,7 +1118,7 @@ description = "Easily download, build, install, upgrade, and uninstall Python pa optional = false python-versions = ">=3.8" groups = ["dev"] -markers = "python_version == \"3.12\"" +markers = "python_version >= \"3.12\"" files = [ {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, @@ -1378,5 +1479,5 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.1" -python-versions = ">=3.11,<3.13" -content-hash = "a27551c76dc475808753ae67841ad6acaee9cfa4f9da5047948de3e901583bfe" +python-versions = ">=3.11,<3.14" +content-hash = "1c2fef13771f2784c720766de7a74cdc5de4e9362c5c34dcba1fbc55bbe74eb2" diff --git a/pyproject.toml b/pyproject.toml index dd1c2ad..9dc3032 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ include = ["best-practices.md"] exclude = ["examples", "tests"] [tool.poetry.dependencies] -python = ">=3.11,<3.13" +python = ">=3.11,<3.14" numpy = "^2.2.1" pandas = "^2.3.1" geopandas = "^1.1.1" @@ -39,6 +39,7 @@ pydata-sphinx-theme = "^0.16.1" myst-parser = "^4.0.1" sphinx-copybutton = "^0.5.2" sphinx-rtd-theme = "^3.0.2" +black = "^25.1.0" [build-system] requires = ["poetry-core>=2.0.0"] diff --git a/tests/test_indexed_geometries.py b/tests/test_indexed_geometries.py index 9f20598..2d24b3e 100644 --- a/tests/test_indexed_geometries.py +++ b/tests/test_indexed_geometries.py @@ -26,7 +26,9 @@ def test_indexed_has_a_spatial_index(four_square_grid): def test_indexed_queries_its_spatial_index_when_intersections_is_called( four_square_grid, square ): - with patch("maup.indexed_geometries.STRtree.query",) as query_fn: + with patch( + "maup.indexed_geometries.STRtree.query", + ) as query_fn: query_fn.return_value = np.array([]) IndexedGeometries(four_square_grid).intersections(square) query_fn.assert_called() @@ -50,7 +52,10 @@ def test_intersections_correct_when_all_overlapping(four_square_grid, square): assert any(overlap.intersection(p).area == p.area for overlap in overlaps) for p in overlaps: - assert any(p.intersection(expected).area == expected.area for expected in expected_polygons) + assert any( + p.intersection(expected).area == expected.area + for expected in expected_polygons + ) def test_returns_empty_when_no_overlaps(four_square_grid, distant_polygon): diff --git a/tests/test_intersections.py b/tests/test_intersections.py index f3a24dd..c014878 100644 --- a/tests/test_intersections.py +++ b/tests/test_intersections.py @@ -73,9 +73,7 @@ def manually_compute_intersections(sources, targets): records.append((i, j, intersection)) expected = ( - geopandas.GeoDataFrame( - records, columns=["source", "target", "geometry"] - ) + geopandas.GeoDataFrame(records, columns=["source", "target", "geometry"]) .set_index(["source", "target"]) .geometry ) diff --git a/tests/test_smart_repair.py b/tests/test_smart_repair.py index a6912d1..a18f77c 100644 --- a/tests/test_smart_repair.py +++ b/tests/test_smart_repair.py @@ -16,15 +16,31 @@ def toy_precincts_geoseries(): for i in range(4): for j in range(4): poly = Polygon( - [(0.5*i + 0.1*k, 0.5*j + (random.random() - 0.5)/12) for k in range(6)] + - [(0.5*(i+1) + (random.random() - 0.5)/12, 0.5*j + 0.1*k) for k in range(1,6)] + - [(0.5*(i+1) - 0.1*k, 0.5*(j+1) + (random.random() - 0.5)/12) for k in range(1,6)] + - [(0.5*i + (random.random() - 0.5)/12, 0.5*(j+1) - 0.1*k) for k in range(1,5)] + [ + (0.5 * i + 0.1 * k, 0.5 * j + (random.random() - 0.5) / 12) + for k in range(6) + ] + + [ + (0.5 * (i + 1) + (random.random() - 0.5) / 12, 0.5 * j + 0.1 * k) + for k in range(1, 6) + ] + + [ + ( + 0.5 * (i + 1) - 0.1 * k, + 0.5 * (j + 1) + (random.random() - 0.5) / 12, + ) + for k in range(1, 6) + ] + + [ + (0.5 * i + (random.random() - 0.5) / 12, 0.5 * (j + 1) - 0.1 * k) + for k in range(1, 5) + ] ) ppolys.append(poly) - + return geopandas.GeoSeries(ppolys) + @pytest.fixture def toy_precincts_geodataframe(): random.seed(2023) @@ -32,23 +48,41 @@ def toy_precincts_geodataframe(): for i in range(4): for j in range(4): poly = Polygon( - [(0.5*i + 0.1*k, 0.5*j + (random.random() - 0.5)/12) for k in range(6)] + - [(0.5*(i+1) + (random.random() - 0.5)/12, 0.5*j + 0.1*k) for k in range(1,6)] + - [(0.5*(i+1) - 0.1*k, 0.5*(j+1) + (random.random() - 0.5)/12) for k in range(1,6)] + - [(0.5*i + (random.random() - 0.5)/12, 0.5*(j+1) - 0.1*k) for k in range(1,5)] + [ + (0.5 * i + 0.1 * k, 0.5 * j + (random.random() - 0.5) / 12) + for k in range(6) + ] + + [ + (0.5 * (i + 1) + (random.random() - 0.5) / 12, 0.5 * j + 0.1 * k) + for k in range(1, 6) + ] + + [ + ( + 0.5 * (i + 1) - 0.1 * k, + 0.5 * (j + 1) + (random.random() - 0.5) / 12, + ) + for k in range(1, 6) + ] + + [ + (0.5 * i + (random.random() - 0.5) / 12, 0.5 * (j + 1) - 0.1 * k) + for k in range(1, 5) + ] ) ppolys.append(poly) - - return geopandas.GeoDataFrame(geometry = geopandas.GeoSeries(ppolys)) + + return geopandas.GeoDataFrame(geometry=geopandas.GeoSeries(ppolys)) + @pytest.fixture def toy_counties_geodataframe(): - cpoly1 = Polygon([(0,0), (1,0), (1,1), (0,1)]) - cpoly2 = Polygon([(1,0), (2,0), (2,1), (1,1)]) - cpoly3 = Polygon([(0,1), (1,1), (1,2), (0,2)]) - cpoly4 = Polygon([(1,1), (2,1), (2,2), (1,2)]) + cpoly1 = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) + cpoly2 = Polygon([(1, 0), (2, 0), (2, 1), (1, 1)]) + cpoly3 = Polygon([(0, 1), (1, 1), (1, 2), (0, 2)]) + cpoly4 = Polygon([(1, 1), (2, 1), (2, 2), (1, 2)]) - return geopandas.GeoDataFrame(geometry = geopandas.GeoSeries([cpoly1, cpoly2, cpoly3, cpoly4])) + return geopandas.GeoDataFrame( + geometry=geopandas.GeoSeries([cpoly1, cpoly2, cpoly3, cpoly4]) + ) class TestSmartRepair: @@ -62,22 +96,27 @@ def test_smart_repair_basic_output_from_gs_clean(self, toy_precincts_geoseries): assert isinstance(repaired_gs, geopandas.GeoSeries) assert doctor(repaired_gs) - def test_nest_within_regions(self, toy_precincts_geodataframe, toy_counties_geodataframe): - repaired_with_regions_gdf = smart_repair(toy_precincts_geodataframe, - nest_within_regions = toy_counties_geodataframe - ) + def test_nest_within_regions( + self, toy_precincts_geodataframe, toy_counties_geodataframe + ): + repaired_with_regions_gdf = smart_repair( + toy_precincts_geodataframe, nest_within_regions=toy_counties_geodataframe + ) p_to_c = assign(toy_precincts_geodataframe, toy_counties_geodataframe) for p in p_to_c.index: - assert toy_counties_geodataframe.geometry[p_to_c[p]].contains(repaired_with_regions_gdf.geometry[p]) + assert toy_counties_geodataframe.geometry[p_to_c[p]].contains( + repaired_with_regions_gdf.geometry[p] + ) def test_small_rook_to_queen(self, toy_precincts_geodataframe): repaired_basic_gdf = smart_repair(toy_precincts_geodataframe) assert min(adjacencies(repaired_basic_gdf).length) < 0.05 - - repaired_srtq_gdf = smart_repair(toy_precincts_geodataframe, min_rook_length=0.05) + + repaired_srtq_gdf = smart_repair( + toy_precincts_geodataframe, min_rook_length=0.05 + ) assert min(adjacencies(repaired_srtq_gdf).length) > 0.05 # There should also be a lot of unit tests for all the component functions, # but this could mushroom into a BIG project that will have to wait for another day! - From 93768fdbd26a01d01551cfdea2bdc0d8366bfe76 Mon Sep 17 00:00:00 2001 From: peterrrock2 <27579114+peterrrock2@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:19:54 -0600 Subject: [PATCH 4/6] Enable python 3.13 --- .github/workflows/tests.yaml | 2 +- .gitignore | 7 +- poetry.lock | 900 +++++++++++++++++++++++++++++++---- pyproject.toml | 4 +- 4 files changed, 824 insertions(+), 89 deletions(-) diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index e86cbd8..fb924e6 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.11, 3.12] + python-version: [3.11, 3.13] os: [ubuntu-latest, macOS-latest] steps: diff --git a/.gitignore b/.gitignore index 766ffde..bef19f8 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,10 @@ var/ .idea/ .venv +# Development environments +dev_files/ +.envrc + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. @@ -77,5 +81,4 @@ junit.xml .docs_venv # Pytest cache -.pytest_cache/ -.envrc \ No newline at end of file +.pytest_cache/ \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 214de98..0825eed 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "accessible-pygments" @@ -31,6 +31,35 @@ files = [ {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, ] +[[package]] +name = "appnope" +version = "0.1.4" +description = "Disable App Nap on macOS >= 10.9" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +markers = "platform_system == \"Darwin\"" +files = [ + {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, + {file = "appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee"}, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +description = "Annotate AST trees with source code positions" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, + {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, +] + +[package.extras] +astroid = ["astroid (>=2,<4)"] +test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] + [[package]] name = "babel" version = "2.13.1" @@ -125,6 +154,87 @@ files = [ {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, ] +[[package]] +name = "cffi" +version = "1.17.1" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "implementation_name == \"pypy\"" +files = [ + {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, + {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, + {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, + {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, + {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, + {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, + {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, + {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, + {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, + {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, + {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, + {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, + {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, + {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, + {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, + {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, + {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, + {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, + {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, + {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, + {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, + {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, + {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, + {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, + {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, + {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, + {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, + {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, + {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, + {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, + {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.1" @@ -253,6 +363,21 @@ files = [ ] markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\" or platform_system == \"Windows\""} +[[package]] +name = "comm" +version = "0.2.3" +description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417"}, + {file = "comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971"}, +] + +[package.extras] +test = ["pytest"] + [[package]] name = "coverage" version = "7.9.2" @@ -333,6 +458,54 @@ files = [ [package.extras] toml = ["tomli ; python_full_version <= \"3.11.0a6\""] +[[package]] +name = "debugpy" +version = "1.8.16" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "debugpy-1.8.16-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2a3958fb9c2f40ed8ea48a0d34895b461de57a1f9862e7478716c35d76f56c65"}, + {file = "debugpy-1.8.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5ca7314042e8a614cc2574cd71f6ccd7e13a9708ce3c6d8436959eae56f2378"}, + {file = "debugpy-1.8.16-cp310-cp310-win32.whl", hash = "sha256:8624a6111dc312ed8c363347a0b59c5acc6210d897e41a7c069de3c53235c9a6"}, + {file = "debugpy-1.8.16-cp310-cp310-win_amd64.whl", hash = "sha256:fee6db83ea5c978baf042440cfe29695e1a5d48a30147abf4c3be87513609817"}, + {file = "debugpy-1.8.16-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:67371b28b79a6a12bcc027d94a06158f2fde223e35b5c4e0783b6f9d3b39274a"}, + {file = "debugpy-1.8.16-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2abae6dd02523bec2dee16bd6b0781cccb53fd4995e5c71cc659b5f45581898"}, + {file = "debugpy-1.8.16-cp311-cp311-win32.whl", hash = "sha256:f8340a3ac2ed4f5da59e064aa92e39edd52729a88fbde7bbaa54e08249a04493"}, + {file = "debugpy-1.8.16-cp311-cp311-win_amd64.whl", hash = "sha256:70f5fcd6d4d0c150a878d2aa37391c52de788c3dc680b97bdb5e529cb80df87a"}, + {file = "debugpy-1.8.16-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:b202e2843e32e80b3b584bcebfe0e65e0392920dc70df11b2bfe1afcb7a085e4"}, + {file = "debugpy-1.8.16-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64473c4a306ba11a99fe0bb14622ba4fbd943eb004847d9b69b107bde45aa9ea"}, + {file = "debugpy-1.8.16-cp312-cp312-win32.whl", hash = "sha256:833a61ed446426e38b0dd8be3e9d45ae285d424f5bf6cd5b2b559c8f12305508"}, + {file = "debugpy-1.8.16-cp312-cp312-win_amd64.whl", hash = "sha256:75f204684581e9ef3dc2f67687c3c8c183fde2d6675ab131d94084baf8084121"}, + {file = "debugpy-1.8.16-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:85df3adb1de5258dca910ae0bb185e48c98801ec15018a263a92bb06be1c8787"}, + {file = "debugpy-1.8.16-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bee89e948bc236a5c43c4214ac62d28b29388453f5fd328d739035e205365f0b"}, + {file = "debugpy-1.8.16-cp313-cp313-win32.whl", hash = "sha256:cf358066650439847ec5ff3dae1da98b5461ea5da0173d93d5e10f477c94609a"}, + {file = "debugpy-1.8.16-cp313-cp313-win_amd64.whl", hash = "sha256:b5aea1083f6f50023e8509399d7dc6535a351cc9f2e8827d1e093175e4d9fa4c"}, + {file = "debugpy-1.8.16-cp38-cp38-macosx_14_0_x86_64.whl", hash = "sha256:2801329c38f77c47976d341d18040a9ac09d0c71bf2c8b484ad27c74f83dc36f"}, + {file = "debugpy-1.8.16-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:687c7ab47948697c03b8f81424aa6dc3f923e6ebab1294732df1ca9773cc67bc"}, + {file = "debugpy-1.8.16-cp38-cp38-win32.whl", hash = "sha256:a2ba6fc5d7c4bc84bcae6c5f8edf5988146e55ae654b1bb36fecee9e5e77e9e2"}, + {file = "debugpy-1.8.16-cp38-cp38-win_amd64.whl", hash = "sha256:d58c48d8dbbbf48a3a3a638714a2d16de537b0dace1e3432b8e92c57d43707f8"}, + {file = "debugpy-1.8.16-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:135ccd2b1161bade72a7a099c9208811c137a150839e970aeaf121c2467debe8"}, + {file = "debugpy-1.8.16-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:211238306331a9089e253fd997213bc4a4c65f949271057d6695953254095376"}, + {file = "debugpy-1.8.16-cp39-cp39-win32.whl", hash = "sha256:88eb9ffdfb59bf63835d146c183d6dba1f722b3ae2a5f4b9fc03e925b3358922"}, + {file = "debugpy-1.8.16-cp39-cp39-win_amd64.whl", hash = "sha256:c2c47c2e52b40449552843b913786499efcc3dbc21d6c49287d939cd0dbc49fd"}, + {file = "debugpy-1.8.16-py2.py3-none-any.whl", hash = "sha256:19c9521962475b87da6f673514f7fd610328757ec993bf7ec0d8c96f9a325f9e"}, + {file = "debugpy-1.8.16.tar.gz", hash = "sha256:31e69a1feb1cf6b51efbed3f6c9b0ef03bc46ff050679c4be7ea6d2e23540870"}, +] + +[[package]] +name = "decorator" +version = "5.2.1" +description = "Decorators for Humans" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, + {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, +] + [[package]] name = "docutils" version = "0.21.2" @@ -345,6 +518,21 @@ files = [ {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] +[[package]] +name = "executing" +version = "2.2.0" +description = "Get the currently executing AST node of a frame, and other information" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, + {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, +] + +[package.extras] +tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""] + [[package]] name = "geopandas" version = "1.1.1" @@ -405,6 +593,108 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "ipykernel" +version = "6.30.1" +description = "IPython Kernel for Jupyter" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "ipykernel-6.30.1-py3-none-any.whl", hash = "sha256:aa6b9fb93dca949069d8b85b6c79b2518e32ac583ae9c7d37c51d119e18b3fb4"}, + {file = "ipykernel-6.30.1.tar.gz", hash = "sha256:6abb270161896402e76b91394fcdce5d1be5d45f456671e5080572f8505be39b"}, +] + +[package.dependencies] +appnope = {version = ">=0.1.2", markers = "platform_system == \"Darwin\""} +comm = ">=0.1.1" +debugpy = ">=1.6.5" +ipython = ">=7.23.1" +jupyter-client = ">=8.0.0" +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +matplotlib-inline = ">=0.1" +nest-asyncio = ">=1.4" +packaging = ">=22" +psutil = ">=5.7" +pyzmq = ">=25" +tornado = ">=6.2" +traitlets = ">=5.4.0" + +[package.extras] +cov = ["coverage[toml]", "matplotlib", "pytest-cov", "trio"] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling", "trio"] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] +test = ["flaky", "ipyparallel", "pre-commit", "pytest (>=7.0,<9)", "pytest-asyncio (>=0.23.5)", "pytest-cov", "pytest-timeout"] + +[[package]] +name = "ipython" +version = "9.4.0" +description = "IPython: Productive Interactive Computing" +optional = false +python-versions = ">=3.11" +groups = ["dev"] +files = [ + {file = "ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066"}, + {file = "ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +decorator = "*" +ipython-pygments-lexers = "*" +jedi = ">=0.16" +matplotlib-inline = "*" +pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} +prompt_toolkit = ">=3.0.41,<3.1.0" +pygments = ">=2.4.0" +stack_data = "*" +traitlets = ">=5.13.0" +typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""} + +[package.extras] +all = ["ipython[doc,matplotlib,test,test-extra]"] +black = ["black"] +doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] +matplotlib = ["matplotlib"] +test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"] +test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +description = "Defines a variety of Pygments lexers for highlighting IPython code." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, + {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, +] + +[package.dependencies] +pygments = "*" + +[[package]] +name = "jedi" +version = "0.19.2" +description = "An autocompletion tool for Python that can be used for text editors." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, + {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, +] + +[package.dependencies] +parso = ">=0.8.4,<0.9.0" + +[package.extras] +docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] + [[package]] name = "jinja2" version = "3.1.2" @@ -423,6 +713,50 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "jupyter-client" +version = "8.6.3" +description = "Jupyter protocol implementation and client libraries" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, + {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, +] + +[package.dependencies] +jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +python-dateutil = ">=2.8.2" +pyzmq = ">=23.0" +tornado = ">=6.2" +traitlets = ">=5.3" + +[package.extras] +docs = ["ipykernel", "myst-parser", "pydata-sphinx-theme", "sphinx (>=4)", "sphinx-autodoc-typehints", "sphinxcontrib-github-alt", "sphinxcontrib-spelling"] +test = ["coverage", "ipykernel (>=6.14)", "mypy", "paramiko ; sys_platform == \"win32\"", "pre-commit", "pytest (<8.2.0)", "pytest-cov", "pytest-jupyter[client] (>=0.4.1)", "pytest-timeout"] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +description = "Jupyter core package. A base package on which Jupyter projects rely." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0"}, + {file = "jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941"}, +] + +[package.dependencies] +platformdirs = ">=2.5" +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +traitlets = ">=5.3" + +[package.extras] +docs = ["intersphinx-registry", "myst-parser", "pydata-sphinx-theme", "sphinx-autodoc-typehints", "sphinxcontrib-spelling", "traitlets"] +test = ["ipykernel", "pre-commit", "pytest (<9)", "pytest-cov", "pytest-timeout"] + [[package]] name = "markdown-it-py" version = "3.0.0" @@ -518,6 +852,21 @@ files = [ {file = "MarkupSafe-2.1.3.tar.gz", hash = "sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad"}, ] +[[package]] +name = "matplotlib-inline" +version = "0.1.7" +description = "Inline Matplotlib backend for Jupyter" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, + {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, +] + +[package.dependencies] +traitlets = "*" + [[package]] name = "mdit-py-plugins" version = "0.4.2" @@ -589,65 +938,100 @@ rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-bo testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pygments (<2.19)", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] +[[package]] +name = "nest-asyncio" +version = "1.6.0" +description = "Patch asyncio to allow nested event loops" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, + {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, +] + [[package]] name = "numpy" -version = "2.3.1" +version = "2.3.2" description = "Fundamental package for array computing in Python" optional = false python-versions = ">=3.11" groups = ["main"] files = [ - {file = "numpy-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6ea9e48336a402551f52cd8f593343699003d2353daa4b72ce8d34f66b722070"}, - {file = "numpy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ccb7336eaf0e77c1635b232c141846493a588ec9ea777a7c24d7166bb8533ae"}, - {file = "numpy-2.3.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:0bb3a4a61e1d327e035275d2a993c96fa786e4913aa089843e6a2d9dd205c66a"}, - {file = "numpy-2.3.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:e344eb79dab01f1e838ebb67aab09965fb271d6da6b00adda26328ac27d4a66e"}, - {file = "numpy-2.3.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:467db865b392168ceb1ef1ffa6f5a86e62468c43e0cfb4ab6da667ede10e58db"}, - {file = "numpy-2.3.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:afed2ce4a84f6b0fc6c1ce734ff368cbf5a5e24e8954a338f3bdffa0718adffb"}, - {file = "numpy-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0025048b3c1557a20bc80d06fdeb8cc7fc193721484cca82b2cfa072fec71a93"}, - {file = "numpy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5ee121b60aa509679b682819c602579e1df14a5b07fe95671c8849aad8f2115"}, - {file = "numpy-2.3.1-cp311-cp311-win32.whl", hash = "sha256:a8b740f5579ae4585831b3cf0e3b0425c667274f82a484866d2adf9570539369"}, - {file = "numpy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:d4580adadc53311b163444f877e0789f1c8861e2698f6b2a4ca852fda154f3ff"}, - {file = "numpy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:ec0bdafa906f95adc9a0c6f26a4871fa753f25caaa0e032578a30457bff0af6a"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2959d8f268f3d8ee402b04a9ec4bb7604555aeacf78b360dc4ec27f1d508177d"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:762e0c0c6b56bdedfef9a8e1d4538556438288c4276901ea008ae44091954e29"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:867ef172a0976aaa1f1d1b63cf2090de8b636a7674607d514505fb7276ab08fc"}, - {file = "numpy-2.3.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:4e602e1b8682c2b833af89ba641ad4176053aaa50f5cacda1a27004352dde943"}, - {file = "numpy-2.3.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:8e333040d069eba1652fb08962ec5b76af7f2c7bce1df7e1418c8055cf776f25"}, - {file = "numpy-2.3.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:e7cbf5a5eafd8d230a3ce356d892512185230e4781a361229bd902ff403bc660"}, - {file = "numpy-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1b8f26d1086835f442286c1d9b64bb3974b0b1e41bb105358fd07d20872952"}, - {file = "numpy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ee8340cb48c9b7a5899d1149eece41ca535513a9698098edbade2a8e7a84da77"}, - {file = "numpy-2.3.1-cp312-cp312-win32.whl", hash = "sha256:e772dda20a6002ef7061713dc1e2585bc1b534e7909b2030b5a46dae8ff077ab"}, - {file = "numpy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cfecc7822543abdea6de08758091da655ea2210b8ffa1faf116b940693d3df76"}, - {file = "numpy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:7be91b2239af2658653c5bb6f1b8bccafaf08226a258caf78ce44710a0160d30"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25a1992b0a3fdcdaec9f552ef10d8103186f5397ab45e2d25f8ac51b1a6b97e8"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dea630156d39b02a63c18f508f85010230409db5b2927ba59c8ba4ab3e8272e"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bada6058dd886061f10ea15f230ccf7dfff40572e99fef440a4a857c8728c9c0"}, - {file = "numpy-2.3.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:a894f3816eb17b29e4783e5873f92faf55b710c2519e5c351767c51f79d8526d"}, - {file = "numpy-2.3.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:18703df6c4a4fee55fd3d6e5a253d01c5d33a295409b03fda0c86b3ca2ff41a1"}, - {file = "numpy-2.3.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5902660491bd7a48b2ec16c23ccb9124b8abfd9583c5fdfa123fe6b421e03de1"}, - {file = "numpy-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:36890eb9e9d2081137bd78d29050ba63b8dab95dff7912eadf1185e80074b2a0"}, - {file = "numpy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a780033466159c2270531e2b8ac063704592a0bc62ec4a1b991c7c40705eb0e8"}, - {file = "numpy-2.3.1-cp313-cp313-win32.whl", hash = "sha256:39bff12c076812595c3a306f22bfe49919c5513aa1e0e70fac756a0be7c2a2b8"}, - {file = "numpy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d5ee6eec45f08ce507a6570e06f2f879b374a552087a4179ea7838edbcbfa42"}, - {file = "numpy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:0c4d9e0a8368db90f93bd192bfa771ace63137c3488d198ee21dfb8e7771916e"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:b0b5397374f32ec0649dd98c652a1798192042e715df918c20672c62fb52d4b8"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c5bdf2015ccfcee8253fb8be695516ac4457c743473a43290fd36eba6a1777eb"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d70f20df7f08b90a2062c1f07737dd340adccf2068d0f1b9b3d56e2038979fee"}, - {file = "numpy-2.3.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:2fb86b7e58f9ac50e1e9dd1290154107e47d1eef23a0ae9145ded06ea606f992"}, - {file = "numpy-2.3.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:23ab05b2d241f76cb883ce8b9a93a680752fbfcbd51c50eff0b88b979e471d8c"}, - {file = "numpy-2.3.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ce2ce9e5de4703a673e705183f64fd5da5bf36e7beddcb63a25ee2286e71ca48"}, - {file = "numpy-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c4913079974eeb5c16ccfd2b1f09354b8fed7e0d6f2cab933104a09a6419b1ee"}, - {file = "numpy-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:010ce9b4f00d5c036053ca684c77441f2f2c934fd23bee058b4d6f196efd8280"}, - {file = "numpy-2.3.1-cp313-cp313t-win32.whl", hash = "sha256:6269b9edfe32912584ec496d91b00b6d34282ca1d07eb10e82dfc780907d6c2e"}, - {file = "numpy-2.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:2a809637460e88a113e186e87f228d74ae2852a2e0c44de275263376f17b5bdc"}, - {file = "numpy-2.3.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eccb9a159db9aed60800187bc47a6d3451553f0e1b08b068d8b277ddfbb9b244"}, - {file = "numpy-2.3.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ad506d4b09e684394c42c966ec1527f6ebc25da7f4da4b1b056606ffe446b8a3"}, - {file = "numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:ebb8603d45bc86bbd5edb0d63e52c5fd9e7945d3a503b77e486bd88dde67a19b"}, - {file = "numpy-2.3.1-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:15aa4c392ac396e2ad3d0a2680c0f0dee420f9fed14eef09bdb9450ee6dcb7b7"}, - {file = "numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c6e0bf9d1a2f50d2b65a7cf56db37c095af17b59f6c132396f7c6d5dd76484df"}, - {file = "numpy-2.3.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:eabd7e8740d494ce2b4ea0ff05afa1b7b291e978c0ae075487c51e8bd93c0c68"}, - {file = "numpy-2.3.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e610832418a2bc09d974cc9fecebfa51e9532d6190223bc5ef6a7402ebf3b5cb"}, - {file = "numpy-2.3.1.tar.gz", hash = "sha256:1ec9ae20a4226da374362cca3c62cd753faf2f951440b0e3b98e93c235441d2b"}, + {file = "numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9"}, + {file = "numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168"}, + {file = "numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b"}, + {file = "numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8"}, + {file = "numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d"}, + {file = "numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3"}, + {file = "numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f"}, + {file = "numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097"}, + {file = "numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220"}, + {file = "numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170"}, + {file = "numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89"}, + {file = "numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b"}, + {file = "numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f"}, + {file = "numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0"}, + {file = "numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b"}, + {file = "numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370"}, + {file = "numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73"}, + {file = "numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc"}, + {file = "numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be"}, + {file = "numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036"}, + {file = "numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f"}, + {file = "numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07"}, + {file = "numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3"}, + {file = "numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b"}, + {file = "numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6"}, + {file = "numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089"}, + {file = "numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2"}, + {file = "numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f"}, + {file = "numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee"}, + {file = "numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6"}, + {file = "numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b"}, + {file = "numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56"}, + {file = "numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2"}, + {file = "numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab"}, + {file = "numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2"}, + {file = "numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a"}, + {file = "numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286"}, + {file = "numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8"}, + {file = "numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a"}, + {file = "numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91"}, + {file = "numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5"}, + {file = "numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5"}, + {file = "numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450"}, + {file = "numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a"}, + {file = "numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a"}, + {file = "numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b"}, + {file = "numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125"}, + {file = "numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19"}, + {file = "numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f"}, + {file = "numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5"}, + {file = "numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58"}, + {file = "numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0"}, + {file = "numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2"}, + {file = "numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b"}, + {file = "numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910"}, + {file = "numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e"}, + {file = "numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45"}, + {file = "numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b"}, + {file = "numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2"}, + {file = "numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0"}, + {file = "numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0"}, + {file = "numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2"}, + {file = "numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf"}, + {file = "numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1"}, + {file = "numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b"}, + {file = "numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981"}, + {file = "numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619"}, + {file = "numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48"}, ] [[package]] @@ -763,6 +1147,22 @@ sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-d test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] xml = ["lxml (>=4.9.2)"] +[[package]] +name = "parso" +version = "0.8.4" +description = "A Python Parser" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, + {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, +] + +[package.extras] +qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] +testing = ["docopt", "pytest"] + [[package]] name = "pathspec" version = "0.12.1" @@ -775,6 +1175,22 @@ files = [ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, ] +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + [[package]] name = "platformdirs" version = "4.3.8" @@ -808,6 +1224,86 @@ files = [ dev = ["pre-commit", "tox"] testing = ["coverage", "pytest", "pytest-benchmark"] +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +description = "Library for building powerful interactive command lines in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, + {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, +] + +[package.dependencies] +wcwidth = "*" + +[[package]] +name = "psutil" +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, +] + +[package.extras] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +description = "Safely evaluate AST nodes without side effects" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, + {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, +] + +[package.extras] +tests = ["pytest"] + +[[package]] +name = "pycparser" +version = "2.22" +description = "C parser in Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +markers = "implementation_name == \"pypy\"" +files = [ + {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, + {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, +] + [[package]] name = "pydata-sphinx-theme" version = "0.16.1" @@ -905,39 +1401,67 @@ test = ["pytest", "pytest-cov"] [[package]] name = "pyproj" -version = "3.6.1" +version = "3.7.2" description = "Python interface to PROJ (cartographic projections and coordinate transformations library)" optional = false -python-versions = ">=3.9" +python-versions = ">=3.11" groups = ["main"] files = [ - {file = "pyproj-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ab7aa4d9ff3c3acf60d4b285ccec134167a948df02347585fdd934ebad8811b4"}, - {file = "pyproj-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc0472302919e59114aa140fd7213c2370d848a7249d09704f10f5b062031fe"}, - {file = "pyproj-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5279586013b8d6582e22b6f9e30c49796966770389a9d5b85e25a4223286cd3f"}, - {file = "pyproj-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80fafd1f3eb421694857f254a9bdbacd1eb22fc6c24ca74b136679f376f97d35"}, - {file = "pyproj-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c41e80ddee130450dcb8829af7118f1ab69eaf8169c4bf0ee8d52b72f098dc2f"}, - {file = "pyproj-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:db3aedd458e7f7f21d8176f0a1d924f1ae06d725228302b872885a1c34f3119e"}, - {file = "pyproj-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ebfbdbd0936e178091309f6cd4fcb4decd9eab12aa513cdd9add89efa3ec2882"}, - {file = "pyproj-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:447db19c7efad70ff161e5e46a54ab9cc2399acebb656b6ccf63e4bc4a04b97a"}, - {file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7e13c40183884ec7f94eb8e0f622f08f1d5716150b8d7a134de48c6110fee85"}, - {file = "pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65ad699e0c830e2b8565afe42bd58cc972b47d829b2e0e48ad9638386d994915"}, - {file = "pyproj-3.6.1-cp311-cp311-win32.whl", hash = "sha256:8b8acc31fb8702c54625f4d5a2a6543557bec3c28a0ef638778b7ab1d1772132"}, - {file = "pyproj-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:38a3361941eb72b82bd9a18f60c78b0df8408416f9340521df442cebfc4306e2"}, - {file = "pyproj-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1e9fbaf920f0f9b4ee62aab832be3ae3968f33f24e2e3f7fbb8c6728ef1d9746"}, - {file = "pyproj-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6d227a865356f225591b6732430b1d1781e946893789a609bb34f59d09b8b0f8"}, - {file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83039e5ae04e5afc974f7d25ee0870a80a6bd6b7957c3aca5613ccbe0d3e72bf"}, - {file = "pyproj-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb059ba3bced6f6725961ba758649261d85ed6ce670d3e3b0a26e81cf1aa8d"}, - {file = "pyproj-3.6.1-cp312-cp312-win32.whl", hash = "sha256:2d6ff73cc6dbbce3766b6c0bce70ce070193105d8de17aa2470009463682a8eb"}, - {file = "pyproj-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:7a27151ddad8e1439ba70c9b4b2b617b290c39395fa9ddb7411ebb0eb86d6fb0"}, - {file = "pyproj-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ba1f9b03d04d8cab24d6375609070580a26ce76eaed54631f03bab00a9c737b"}, - {file = "pyproj-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:18faa54a3ca475bfe6255156f2f2874e9a1c8917b0004eee9f664b86ccc513d3"}, - {file = "pyproj-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd43bd9a9b9239805f406fd82ba6b106bf4838d9ef37c167d3ed70383943ade1"}, - {file = "pyproj-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50100b2726a3ca946906cbaa789dd0749f213abf0cbb877e6de72ca7aa50e1ae"}, - {file = "pyproj-3.6.1-cp39-cp39-win32.whl", hash = "sha256:9274880263256f6292ff644ca92c46d96aa7e57a75c6df3f11d636ce845a1877"}, - {file = "pyproj-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:36b64c2cb6ea1cc091f329c5bd34f9c01bb5da8c8e4492c709bda6a09f96808f"}, - {file = "pyproj-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd93c1a0c6c4aedc77c0fe275a9f2aba4d59b8acf88cebfc19fe3c430cfabf4f"}, - {file = "pyproj-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6420ea8e7d2a88cb148b124429fba8cd2e0fae700a2d96eab7083c0928a85110"}, - {file = "pyproj-3.6.1.tar.gz", hash = "sha256:44aa7c704c2b7d8fb3d483bbf75af6cb2350d30a63b144279a09b75fead501bf"}, + {file = "pyproj-3.7.2-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:2514d61f24c4e0bb9913e2c51487ecdaeca5f8748d8313c933693416ca41d4d5"}, + {file = "pyproj-3.7.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:8693ca3892d82e70de077701ee76dd13d7bca4ae1c9d1e739d72004df015923a"}, + {file = "pyproj-3.7.2-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5e26484d80fea56273ed1555abaea161e9661d81a6c07815d54b8e883d4ceb25"}, + {file = "pyproj-3.7.2-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:281cb92847814e8018010c48b4069ff858a30236638631c1a91dd7bfa68f8a8a"}, + {file = "pyproj-3.7.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9c8577f0b7bb09118ec2e57e3babdc977127dd66326d6c5d755c76b063e6d9dc"}, + {file = "pyproj-3.7.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a23f59904fac3a5e7364b3aa44d288234af267ca041adb2c2b14a903cd5d3ac5"}, + {file = "pyproj-3.7.2-cp311-cp311-win32.whl", hash = "sha256:f2af4ed34b2cf3e031a2d85b067a3ecbd38df073c567e04b52fa7a0202afde8a"}, + {file = "pyproj-3.7.2-cp311-cp311-win_amd64.whl", hash = "sha256:0b7cb633565129677b2a183c4d807c727d1c736fcb0568a12299383056e67433"}, + {file = "pyproj-3.7.2-cp311-cp311-win_arm64.whl", hash = "sha256:38b08d85e3a38e455625b80e9eb9f78027c8e2649a21dec4df1f9c3525460c71"}, + {file = "pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab"}, + {file = "pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68"}, + {file = "pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a"}, + {file = "pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630"}, + {file = "pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260"}, + {file = "pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9"}, + {file = "pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d"}, + {file = "pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128"}, + {file = "pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3"}, + {file = "pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1"}, + {file = "pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7"}, + {file = "pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa"}, + {file = "pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681"}, + {file = "pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5"}, + {file = "pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67"}, + {file = "pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3"}, + {file = "pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7"}, + {file = "pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100"}, + {file = "pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279"}, + {file = "pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6"}, + {file = "pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220"}, + {file = "pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c"}, + {file = "pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c"}, + {file = "pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69"}, + {file = "pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2"}, + {file = "pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3"}, + {file = "pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd"}, + {file = "pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02"}, + {file = "pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08"}, + {file = "pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b"}, + {file = "pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281"}, + {file = "pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516"}, + {file = "pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e"}, + {file = "pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25"}, + {file = "pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112"}, + {file = "pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6"}, + {file = "pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37"}, + {file = "pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b"}, + {file = "pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357"}, + {file = "pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81"}, + {file = "pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888"}, + {file = "pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59"}, + {file = "pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa"}, + {file = "pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c"}, + {file = "pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4"}, + {file = "pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c"}, ] [package.dependencies] @@ -991,7 +1515,7 @@ version = "2.8.2" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -1012,6 +1536,37 @@ files = [ {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, ] +[[package]] +name = "pywin32" +version = "311" +description = "Python for Window Extensions" +optional = false +python-versions = "*" +groups = ["dev"] +markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, + {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, + {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, + {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, + {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, + {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, + {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, + {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, + {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, + {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, + {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, + {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, + {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, + {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, + {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, + {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, + {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, + {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, + {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, + {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, +] + [[package]] name = "pyyaml" version = "6.0.1" @@ -1073,6 +1628,111 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "pyzmq" +version = "27.0.1" +description = "Python bindings for 0MQ" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyzmq-27.0.1-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:90a4da42aa322de8a3522461e3b5fe999935763b27f69a02fced40f4e3cf9682"}, + {file = "pyzmq-27.0.1-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e648dca28178fc879c814cf285048dd22fd1f03e1104101106505ec0eea50a4d"}, + {file = "pyzmq-27.0.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bca8abc31799a6f3652d13f47e0b0e1cab76f9125f2283d085a3754f669b607"}, + {file = "pyzmq-27.0.1-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:092f4011b26d6b0201002f439bd74b38f23f3aefcb358621bdc3b230afc9b2d5"}, + {file = "pyzmq-27.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6f02f30a4a6b3efe665ab13a3dd47109d80326c8fd286311d1ba9f397dc5f247"}, + {file = "pyzmq-27.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:f293a1419266e3bf3557d1f8778f9e1ffe7e6b2c8df5c9dca191caf60831eb74"}, + {file = "pyzmq-27.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ce181dd1a7c6c012d0efa8ab603c34b5ee9d86e570c03415bbb1b8772eeb381c"}, + {file = "pyzmq-27.0.1-cp310-cp310-win32.whl", hash = "sha256:f65741cc06630652e82aa68ddef4986a3ab9073dd46d59f94ce5f005fa72037c"}, + {file = "pyzmq-27.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:44909aa3ed2234d69fe81e1dade7be336bcfeab106e16bdaa3318dcde4262b93"}, + {file = "pyzmq-27.0.1-cp310-cp310-win_arm64.whl", hash = "sha256:4401649bfa0a38f0f8777f8faba7cd7eb7b5b8ae2abc7542b830dd09ad4aed0d"}, + {file = "pyzmq-27.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296"}, + {file = "pyzmq-27.0.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1"}, + {file = "pyzmq-27.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808"}, + {file = "pyzmq-27.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f"}, + {file = "pyzmq-27.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a"}, + {file = "pyzmq-27.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1"}, + {file = "pyzmq-27.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab"}, + {file = "pyzmq-27.0.1-cp311-cp311-win32.whl", hash = "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed"}, + {file = "pyzmq-27.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530"}, + {file = "pyzmq-27.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41"}, + {file = "pyzmq-27.0.1-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd"}, + {file = "pyzmq-27.0.1-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd"}, + {file = "pyzmq-27.0.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a"}, + {file = "pyzmq-27.0.1-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577"}, + {file = "pyzmq-27.0.1-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e"}, + {file = "pyzmq-27.0.1-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb"}, + {file = "pyzmq-27.0.1-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55"}, + {file = "pyzmq-27.0.1-cp312-abi3-win32.whl", hash = "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb"}, + {file = "pyzmq-27.0.1-cp312-abi3-win_amd64.whl", hash = "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686"}, + {file = "pyzmq-27.0.1-cp312-abi3-win_arm64.whl", hash = "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed"}, + {file = "pyzmq-27.0.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:497bd8af534ae55dc4ef67eebd1c149ff2a0b0f1e146db73c8b5a53d83c1a5f5"}, + {file = "pyzmq-27.0.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a066ea6ad6218b4c233906adf0ae67830f451ed238419c0db609310dd781fbe7"}, + {file = "pyzmq-27.0.1-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:72d235d6365ca73d8ce92f7425065d70f5c1e19baa458eb3f0d570e425b73a96"}, + {file = "pyzmq-27.0.1-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:313a7b374e3dc64848644ca348a51004b41726f768b02e17e689f1322366a4d9"}, + {file = "pyzmq-27.0.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:119ce8590409702394f959c159d048002cbed2f3c0645ec9d6a88087fc70f0f1"}, + {file = "pyzmq-27.0.1-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:45c3e00ce16896ace2cd770ab9057a7cf97d4613ea5f2a13f815141d8b6894b9"}, + {file = "pyzmq-27.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:678e50ec112bdc6df5a83ac259a55a4ba97a8b314c325ab26b3b5b071151bc61"}, + {file = "pyzmq-27.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d0b96c30be9f9387b18b18b6133c75a7b1b0065da64e150fe1feb5ebf31ece1c"}, + {file = "pyzmq-27.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88dc92d9eb5ea4968123e74db146d770b0c8d48f0e2bfb1dbc6c50a8edb12d64"}, + {file = "pyzmq-27.0.1-cp313-cp313t-win32.whl", hash = "sha256:6dcbcb34f5c9b0cefdfc71ff745459241b7d3cda5b27c7ad69d45afc0821d1e1"}, + {file = "pyzmq-27.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9fd0fda730461f510cfd9a40fafa5355d65f5e3dbdd8d6dfa342b5b3f5d1949"}, + {file = "pyzmq-27.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:56a3b1853f3954ec1f0e91085f1350cc57d18f11205e4ab6e83e4b7c414120e0"}, + {file = "pyzmq-27.0.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f98f6b7787bd2beb1f0dde03f23a0621a0c978edf673b7d8f5e7bc039cbe1b60"}, + {file = "pyzmq-27.0.1-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:351bf5d8ca0788ca85327fda45843b6927593ff4c807faee368cc5aaf9f809c2"}, + {file = "pyzmq-27.0.1-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5268a5a9177afff53dc6d70dffe63114ba2a6e7b20d9411cc3adeba09eeda403"}, + {file = "pyzmq-27.0.1-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a4aca06ba295aa78bec9b33ec028d1ca08744c36294338c41432b7171060c808"}, + {file = "pyzmq-27.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1c363c6dc66352331d5ad64bb838765c6692766334a6a02fdb05e76bd408ae18"}, + {file = "pyzmq-27.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:87aebf4acd7249bdff8d3df03aed4f09e67078e6762cfe0aecf8d0748ff94cde"}, + {file = "pyzmq-27.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e4f22d67756518d71901edf73b38dc0eb4765cce22c8fe122cc81748d425262b"}, + {file = "pyzmq-27.0.1-cp314-cp314t-win32.whl", hash = "sha256:8c62297bc7aea2147b472ca5ca2b4389377ad82898c87cabab2a94aedd75e337"}, + {file = "pyzmq-27.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bee5248d5ec9223545f8cc4f368c2d571477ae828c99409125c3911511d98245"}, + {file = "pyzmq-27.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0fc24bf45e4a454e55ef99d7f5c8b8712539200ce98533af25a5bfa954b6b390"}, + {file = "pyzmq-27.0.1-cp38-cp38-macosx_10_15_universal2.whl", hash = "sha256:9d16fdfd7d70a6b0ca45d36eb19f7702fa77ef6256652f17594fc9ce534c9da6"}, + {file = "pyzmq-27.0.1-cp38-cp38-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d0356a21e58c3e99248930ff73cc05b1d302ff50f41a8a47371aefb04327378a"}, + {file = "pyzmq-27.0.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a27fa11ebaccc099cac4309c799aa33919671a7660e29b3e465b7893bc64ec81"}, + {file = "pyzmq-27.0.1-cp38-cp38-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b25e72e115399a4441aad322258fa8267b873850dc7c276e3f874042728c2b45"}, + {file = "pyzmq-27.0.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f8c3b74f1cd577a5a9253eae7ed363f88cbb345a990ca3027e9038301d47c7f4"}, + {file = "pyzmq-27.0.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:19dce6c93656f9c469540350d29b128cd8ba55b80b332b431b9a1e9ff74cfd01"}, + {file = "pyzmq-27.0.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:da81512b83032ed6cdf85ca62e020b4c23dda87f1b6c26b932131222ccfdbd27"}, + {file = "pyzmq-27.0.1-cp38-cp38-win32.whl", hash = "sha256:7418fb5736d0d39b3ecc6bec4ff549777988feb260f5381636d8bd321b653038"}, + {file = "pyzmq-27.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:af2ee67b3688b067e20fea3fe36b823a362609a1966e7e7a21883ae6da248804"}, + {file = "pyzmq-27.0.1-cp39-cp39-macosx_10_15_universal2.whl", hash = "sha256:05a94233fdde585eb70924a6e4929202a747eea6ed308a6171c4f1c715bbe39e"}, + {file = "pyzmq-27.0.1-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:c96702e1082eab62ae583d64c4e19c9b848359196697e536a0c57ae9bd165bd5"}, + {file = "pyzmq-27.0.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c9180d1f5b4b73e28b64e63cc6c4c097690f102aa14935a62d5dd7426a4e5b5a"}, + {file = "pyzmq-27.0.1-cp39-cp39-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e971d8680003d0af6020713e52f92109b46fedb463916e988814e04c8133578a"}, + {file = "pyzmq-27.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe632fa4501154d58dfbe1764a0495734d55f84eaf1feda4549a1f1ca76659e9"}, + {file = "pyzmq-27.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:4c3874344fd5fa6d58bb51919708048ac4cab21099f40a227173cddb76b4c20b"}, + {file = "pyzmq-27.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0ec09073ed67ae236785d543df3b322282acc0bdf6d1b748c3e81f3043b21cb5"}, + {file = "pyzmq-27.0.1-cp39-cp39-win32.whl", hash = "sha256:f44e7ea288d022d4bf93b9e79dafcb4a7aea45a3cbeae2116792904931cefccf"}, + {file = "pyzmq-27.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:ffe6b809a97ac6dea524b3b837d5b28743d8c2f121141056d168ff0ba8f614ef"}, + {file = "pyzmq-27.0.1-cp39-cp39-win_arm64.whl", hash = "sha256:fde26267416c8478c95432c81489b53f57b0b5d24cd5c8bfaebf5bbaac4dc90c"}, + {file = "pyzmq-27.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:544b995a6a1976fad5d7ff01409b4588f7608ccc41be72147700af91fd44875d"}, + {file = "pyzmq-27.0.1-pp310-pypy310_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0f772eea55cccce7f45d6ecdd1d5049c12a77ec22404f6b892fae687faa87bee"}, + {file = "pyzmq-27.0.1-pp310-pypy310_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9d63d66059114a6756d09169c9209ffceabacb65b9cb0f66e6fc344b20b73e6"}, + {file = "pyzmq-27.0.1-pp310-pypy310_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1da8e645c655d86f0305fb4c65a0d848f461cd90ee07d21f254667287b5dbe50"}, + {file = "pyzmq-27.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1843fd0daebcf843fe6d4da53b8bdd3fc906ad3e97d25f51c3fed44436d82a49"}, + {file = "pyzmq-27.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94"}, + {file = "pyzmq-27.0.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0"}, + {file = "pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb"}, + {file = "pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f"}, + {file = "pyzmq-27.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811"}, + {file = "pyzmq-27.0.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:9d7b6b90da7285642f480b48c9efd1d25302fd628237d8f6f6ee39ba6b2d2d34"}, + {file = "pyzmq-27.0.1-pp38-pypy38_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:d2976b7079f09f48d59dc123293ed6282fca6ef96a270f4ea0364e4e54c8e855"}, + {file = "pyzmq-27.0.1-pp38-pypy38_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2852f67371918705cc18b321695f75c5d653d5d8c4a9b946c1eec4dab2bd6fdf"}, + {file = "pyzmq-27.0.1-pp38-pypy38_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be45a895f98877271e8a0b6cf40925e0369121ce423421c20fa6d7958dc753c2"}, + {file = "pyzmq-27.0.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:64ca3c7c614aefcdd5e358ecdd41d1237c35fe1417d01ec0160e7cdb0a380edc"}, + {file = "pyzmq-27.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d97b59cbd8a6c8b23524a8ce237ff9504d987dc07156258aa68ae06d2dd5f34d"}, + {file = "pyzmq-27.0.1-pp39-pypy39_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:27a78bdd384dbbe7b357af95f72efe8c494306b5ec0a03c31e2d53d6763e5307"}, + {file = "pyzmq-27.0.1-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b007e5dcba684e888fbc90554cb12a2f4e492927c8c2761a80b7590209821743"}, + {file = "pyzmq-27.0.1-pp39-pypy39_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:95594b2ceeaa94934e3e94dd7bf5f3c3659cf1a26b1fb3edcf6e42dad7e0eaf2"}, + {file = "pyzmq-27.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:70b719a130b81dd130a57ac0ff636dc2c0127c5b35ca5467d1b67057e3c7a4d2"}, + {file = "pyzmq-27.0.1.tar.gz", hash = "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b"}, +] + +[package.dependencies] +cffi = {version = "*", markers = "implementation_name == \"pypy\""} + [[package]] name = "requests" version = "2.31.0" @@ -1193,7 +1853,7 @@ version = "1.16.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1413,6 +2073,48 @@ lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] +[[package]] +name = "stack-data" +version = "0.6.3" +description = "Extract data from python stack frames and tracebacks for informative displays" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, + {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, +] + +[package.dependencies] +asttokens = ">=2.1.0" +executing = ">=1.2.0" +pure-eval = "*" + +[package.extras] +tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] + +[[package]] +name = "tornado" +version = "6.5.2" +description = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6"}, + {file = "tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882"}, + {file = "tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4"}, + {file = "tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04"}, + {file = "tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0"}, + {file = "tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f"}, + {file = "tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af"}, + {file = "tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0"}, +] + [[package]] name = "tqdm" version = "4.67.1" @@ -1435,6 +2137,22 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] +[[package]] +name = "traitlets" +version = "5.14.3" +description = "Traitlets Python configuration system" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, + {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, +] + +[package.extras] +docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] +test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] + [[package]] name = "typing-extensions" version = "4.14.1" @@ -1477,7 +2195,19 @@ secure = ["certifi", "cryptography (>=1.9)", "idna (>=2.0.0)", "pyopenssl (>=17. socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.14" -content-hash = "1c2fef13771f2784c720766de7a74cdc5de4e9362c5c34dcba1fbc55bbe74eb2" +content-hash = "2ded9184fd1f78cb5782b15004ae9f7d058609703c2776a1f74944632d11c199" diff --git a/pyproject.toml b/pyproject.toml index 9dc3032..2188c2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,11 +24,12 @@ exclude = ["examples", "tests"] [tool.poetry.dependencies] python = ">=3.11,<3.14" -numpy = "^2.2.1" +numpy = "^2.3.2" pandas = "^2.3.1" geopandas = "^1.1.1" Shapely = "^2.1.1" tqdm = "^4.67.1" +pyproj = ">=3.7.2" [tool.poetry.group.dev.dependencies] pytest = "^8.4.1" @@ -40,6 +41,7 @@ myst-parser = "^4.0.1" sphinx-copybutton = "^0.5.2" sphinx-rtd-theme = "^3.0.2" black = "^25.1.0" +ipykernel = "^6.30.1" [build-system] requires = ["poetry-core>=2.0.0"] From c9fe334d63098c444175ad4d77b7766d05c48409 Mon Sep 17 00:00:00 2001 From: peterrrock2 <27579114+peterrrock2@users.noreply.github.com> Date: Wed, 20 Aug 2025 11:39:38 -0600 Subject: [PATCH 5/6] Fix warnings in tests --- maup/__init__.py | 5 +-- maup/assign.py | 9 +++++- maup/repair.py | 4 ++- poetry.lock | 72 +++++++++++++++++++++---------------------- pyproject.toml | 2 ++ tests/test_assign.py | 10 ++++-- tests/test_prorate.py | 11 +++++-- tests/test_repair.py | 17 ++++++++-- 8 files changed, 83 insertions(+), 47 deletions(-) diff --git a/maup/__init__.py b/maup/__init__.py index db2e86c..d557b8d 100644 --- a/maup/__init__.py +++ b/maup/__init__.py @@ -1,6 +1,6 @@ import geopandas from .adjacencies import adjacencies -from .assign import assign +from .assign import assign, AssigmentWarning from .indexed_geometries import IndexedGeometries from .intersections import intersections, prorate from .repair import ( @@ -24,9 +24,10 @@ "`geopandas.options.use_pygeos = False` before importing your shapefile." ) -__version__ = "2.0.2" +__version__ = "2.0.3" __all__ = [ "adjacencies", + "AssigmentWarning", "assign", "IndexedGeometries", "intersections", diff --git a/maup/assign.py b/maup/assign.py index 78d4942..4a1eba0 100644 --- a/maup/assign.py +++ b/maup/assign.py @@ -6,6 +6,10 @@ from .crs import require_same_crs +class AssigmentWarning(UserWarning): + """Warning raised when some source geometries are not assigned to any target.""" + + @require_same_crs def assign(sources, targets): """Assign source geometries to targets. A source is assigned to the @@ -25,7 +29,10 @@ def assign(sources, targets): # Warn here if there are still unassigned source geometries. unassigned = sources[assignment.isna()] if len(unassigned): # skip if done - warnings.warn("Warning: Some units in the source geometry were unassigned.") + warnings.warn( + "Warning: Some units in the source geometry were unassigned.", + AssigmentWarning, + ) return assignment.astype(targets.index.dtype, errors="ignore") diff --git a/maup/repair.py b/maup/repair.py index 5f76385..de2b7d4 100644 --- a/maup/repair.py +++ b/maup/repair.py @@ -497,7 +497,9 @@ def absorb_by_shared_perimeter( # This difference in indices is expected since not all target geometries may have sources # to absorb, so it would be nice to remove this warning. - result = targets.union(sources_to_absorb) + # NOTE: align=True is needed to avoid a warning. This is consistent with what the + # function did previously, and was added to make the hidden behaviour more explicit. + result = targets.union(sources_to_absorb, align=True) # The .union call only returns the targets who had a corresponding # source to absorb. Now we fill in all of the unchanged targets. diff --git a/poetry.lock b/poetry.lock index 0825eed..a0f5e0c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1349,43 +1349,43 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pyogrio" -version = "0.11.0" +version = "0.11.1" description = "Vectorized spatial vector file format I/O using GDAL/OGR" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pyogrio-0.11.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:47e7aa1e2f345a08009a38c14db16ccdadb31313919efe0903228265df3e1962"}, - {file = "pyogrio-0.11.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:ad9734da7c95cb272f311c1a8ea61181f3ae0f539d5da5af5c88acee0fd6b707"}, - {file = "pyogrio-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1784372868fb20ba32422ce803ad464b39ec26b41587576122b3884ba7533f2c"}, - {file = "pyogrio-0.11.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c307b54b939a1ade5caf737c9297d4c0f8af314c455bc79228fe9bee2fe2e183"}, - {file = "pyogrio-0.11.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:6013501408a0f676ffb9758e83b4e06ef869885d6315417e098c4d3737ba1e39"}, - {file = "pyogrio-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:2b6f2c56f01ea552480e6f7d3deb1228e3babd35a0f314aa076505e2c4f55711"}, - {file = "pyogrio-0.11.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:862b79d36d39c1f755739bde00cfd82fd1034fd287084d9202b14e3a85576f5c"}, - {file = "pyogrio-0.11.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:21b1924c02513185e3df1301dfc9d313f1450d7c366f8629e26757f51ba31003"}, - {file = "pyogrio-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:103313202414ffa7378016791d287442541af60ac57b78536f0c67f3a82904a4"}, - {file = "pyogrio-0.11.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:2e48956e68c41a17cbf3df32d979553de2839a082a7a9b0beef14948aa4ca5df"}, - {file = "pyogrio-0.11.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:ec5666cc8bf97aef9993c998198f85fe209b8a9ad4737696d3d2ab573b3e9a5b"}, - {file = "pyogrio-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:8ad3744e679de2a31b1a885dc5ea260e3482f0d5e71461a88f431cda8d536b17"}, - {file = "pyogrio-0.11.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:a6f114d32c5c8a157c6fbf74e3ecfe69be7efb29363102f2aad14c9813de637a"}, - {file = "pyogrio-0.11.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:596e3f26e792882e35f25715634c12c1d6658a3d8d178c0089a9462c56b48be5"}, - {file = "pyogrio-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d693ca24e80bd7ede7b27ea3598593be5b41fb7cec315a57f5bb24d15faef8"}, - {file = "pyogrio-0.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:961100786ae44e2f27b4049b5262e378a3cba07872fc22051905fed8b4ce42db"}, - {file = "pyogrio-0.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:334563d24defc5d706bd2a1fa7d7433e33140e64b0fb9cb4afc715e4f6035c2b"}, - {file = "pyogrio-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:bf1f9128136abcbd1605d6fc6bf8c529c2092558246d8046ee6fbc383c550074"}, - {file = "pyogrio-0.11.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:0b39e34199460dcd6a606db184094e69bcba89d1babb9a76cee74a134b53b232"}, - {file = "pyogrio-0.11.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5a952ef7a68fdfaf796a91b88c706108cb50ddd0a74096418e84aab7ac8a38be"}, - {file = "pyogrio-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4527abcac23bdac5781f9be9a7dd55fccd9967c7241a8e53de8ea1a06ea0cc2b"}, - {file = "pyogrio-0.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:373a29d56a9016978aff57b88a640b5a8c3024dba7be1c059ad5af4ba932b59e"}, - {file = "pyogrio-0.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ea2a131369ae8e62e30fa4f7e1442074d4828417d05ded660acea04a6a1d199b"}, - {file = "pyogrio-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:bf041d65bd1e89a4bb61845579c2963f2cca1bb33cde79f4ec2c0e0dc6f93afb"}, - {file = "pyogrio-0.11.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:d981a47fc7ade7eb488c0f8b9e1488973bc60b4a6692f2c7ca3812dc38c474c6"}, - {file = "pyogrio-0.11.0-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:d583cab4225fa55bfd9bf730436dcc664a90eb77e22367259a49cedb0f6729ce"}, - {file = "pyogrio-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b23ad2b89d4943d3f8eda011d2e50c1cab02cce9cd34cae263a597410886cd43"}, - {file = "pyogrio-0.11.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:dd872ee4cc5314b3881015c0dddf55f2f1f25f078bd08fcfe240f2264e6073ac"}, - {file = "pyogrio-0.11.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:81cd98c44005c455ae2bbe3490623506bf340bb674ec3c161f9260de01f6bd1b"}, - {file = "pyogrio-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:5fb0da79e2c73856c2b2178d5ec11b9f2ab36213b356f1221c4514cd94e3e91b"}, - {file = "pyogrio-0.11.0.tar.gz", hash = "sha256:a7e0a97bc10c0d7204f6bf52e1b928cba0554c35a907c32b23065aed1ed97b3f"}, + {file = "pyogrio-0.11.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:838ead7df8388d938ce848354e384ae5aa46fe7c5f74f9da2d58f064bda053f7"}, + {file = "pyogrio-0.11.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:6f51aa9fc3632e6dcb3dd5562b4a56a3a31850c3f630aef3587d5889a1f65275"}, + {file = "pyogrio-0.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4982107653ce30de395678b50a1ee00299a4cfcb41043778f1b66c5911b8adbe"}, + {file = "pyogrio-0.11.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b20ffbf72013d464012d8f0f69322459a6528bef08c85f85b8a42b056f730b0"}, + {file = "pyogrio-0.11.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:5b8d60ead740b366cdc2f3b076d21349e5a5d4b9a0e6726922c5a031206b93b2"}, + {file = "pyogrio-0.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:1948027b2809f2248f69b069ab9833d56b53658f182a3b418d12d3d3eb9959d7"}, + {file = "pyogrio-0.11.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d36162ddc1a309bb941a3cfb550b8f88c862c67ef2f52df6460100e5e958bbc6"}, + {file = "pyogrio-0.11.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:845c78d5e7c9ec1c7d00250c07e144e5fe504fdb4ccdc141d9413f85b8c55c91"}, + {file = "pyogrio-0.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50aa869509f189fa1bff4d90d2d4c7860b963e693af85f2957646306e882b631"}, + {file = "pyogrio-0.11.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:dd0f44dd2d849d32aea3f73647c74083996917e446479645bf93de6656160f2d"}, + {file = "pyogrio-0.11.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:36b910d4037694b2935b5b1c1eb757dcc2906dca05cb2992cbdaf1291b54ff97"}, + {file = "pyogrio-0.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:cb744097f302f19dcc5c93ee5e9cfd707b864c9a418e399f0908406a60003728"}, + {file = "pyogrio-0.11.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f186456ebe5d5f61e7bd883bad25a59d43d6304178d4f0d3e03273f42b40a4cc"}, + {file = "pyogrio-0.11.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b8a199bc0e421eac444af96942b7553268e43d0cadf30d0d6d41017de05b7e9e"}, + {file = "pyogrio-0.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afce80b4b32f043fcf76a50e8572e3ad8d9d3e6abbbfa6137f0975ba55c4eeb8"}, + {file = "pyogrio-0.11.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:0cfd79caf0b8cb7bbf30b419dff7f21509169efcf4d431172c61b44fe1029dba"}, + {file = "pyogrio-0.11.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ab3aa6dbf2441d2407ce052233f2966324a3cff752bd43d99e4c779ea54e0a16"}, + {file = "pyogrio-0.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:cd10035eb3b5e5a43bdafbd777339d2274e9b75972658364f0ce31c4d3400d1e"}, + {file = "pyogrio-0.11.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3b368c597357ff262f3b46591ded86409462ee594ef42556708b090d121f873c"}, + {file = "pyogrio-0.11.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:1cb82cfd3493f32396e9c3f9255e17885610f62a323870947f4e04dd59bc3595"}, + {file = "pyogrio-0.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d61aae22e67030fd354f03e21c6462537bf56160134dd8663709335a5a46b28"}, + {file = "pyogrio-0.11.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:76150a3cd787c31628191c7abc6f8c796660125852fb65ae15dd7be1e9196816"}, + {file = "pyogrio-0.11.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e929452f6988c0365dd32ff2485d9488160a709fee28743abbbc18d663169ed0"}, + {file = "pyogrio-0.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6d56862b89a05fccd7211171c88806b6ec9b5effb79bf807cce0a57c1f2a606"}, + {file = "pyogrio-0.11.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9ae8efbe4f9f215b2321655f988be8bb133829037dbefebc2643f52da4e7782a"}, + {file = "pyogrio-0.11.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7cbbc24a785cca733b80c96e8e10f7c316df295786ac9900c145e2b12f828050"}, + {file = "pyogrio-0.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e924de96f1a436567fb57cd94b02b2572c066663c5b6431d2827993d8f3a646"}, + {file = "pyogrio-0.11.1-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:580001084562b55059f161b8c8f2c15135a4523256a3b910ea3a58cd8ffb6c4f"}, + {file = "pyogrio-0.11.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:56d2315f28cdbde98c23f719c85a0f0ee1953a1eae617505c7349c660847dbf5"}, + {file = "pyogrio-0.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:db372785b2a32ad6006477366c4c07285d98f7a7e6d356b2eba15a4fbaaa167f"}, + {file = "pyogrio-0.11.1.tar.gz", hash = "sha256:e1441dc9c866f10d8e6ae7ea9249a10c1f57ea921b1f19a5b0977ab91ef8082c"}, ] [package.dependencies] @@ -1511,14 +1511,14 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" groups = ["main", "dev"] files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [package.dependencies] @@ -2210,4 +2210,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.14" -content-hash = "2ded9184fd1f78cb5782b15004ae9f7d058609703c2776a1f74944632d11c199" +content-hash = "f0e4d690265ed5c11396fe6d416a6252ea605f6b8f95a8de8edca8b82858e2ca" diff --git a/pyproject.toml b/pyproject.toml index 2188c2c..7f05ea3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ geopandas = "^1.1.1" Shapely = "^2.1.1" tqdm = "^4.67.1" pyproj = ">=3.7.2" +pyogrio = "^0.11.1" [tool.poetry.group.dev.dependencies] pytest = "^8.4.1" @@ -42,6 +43,7 @@ sphinx-copybutton = "^0.5.2" sphinx-rtd-theme = "^3.0.2" black = "^25.1.0" ipykernel = "^6.30.1" +python-dateutil = "^2.9.0.post0" [build-system] requires = ["poetry-core>=2.0.0"] diff --git a/tests/test_assign.py b/tests/test_assign.py index d0440e0..3da9d10 100644 --- a/tests/test_assign.py +++ b/tests/test_assign.py @@ -1,9 +1,10 @@ import geopandas import pandas from numpy import nan +import pytest from maup import assign -from maup.assign import assign_by_area, assign_by_covering +from maup.assign import assign_by_area, assign_by_covering, AssigmentWarning def test_assign_assigns_geometries_when_they_nest_neatly( @@ -147,7 +148,12 @@ def test_example_case(): blocks.to_crs("EPSG:5070", inplace=True) precincts.to_crs("EPSG:5070", inplace=True) columns = ["TOTPOP", "BVAP", "WVAP", "HISP"] - assignment = assign(blocks, precincts) + + with pytest.warns( + AssigmentWarning, match="Some units in the source geometry were unassigned." + ): + assignment = assign(blocks, precincts) + precincts[columns] = blocks[columns].groupby(assignment).sum() assert (precincts[columns] > 0).sum().sum() > len(precincts) for col in columns: # fails because it does not neatly cover diff --git a/tests/test_prorate.py b/tests/test_prorate.py index eb76837..6ad96c9 100644 --- a/tests/test_prorate.py +++ b/tests/test_prorate.py @@ -2,7 +2,7 @@ import pandas import pytest -from maup import assign, intersections, prorate, normalize +from maup import assign, intersections, prorate, normalize, AssigmentWarning @pytest.fixture @@ -85,8 +85,13 @@ def test_example_case(): # like boundary intersections, which we do not want to include in # our proration. pieces = intersections(old_precincts, new_precincts, area_cutoff=0) - # Weight by prorated population from blocks - weights = blocks["TOTPOP"].groupby(assign(blocks, pieces)).sum() + + with pytest.warns( + AssigmentWarning, match="Some units in the source geometry were unassigned." + ): + # Weight by prorated population from blocks + weights = blocks["TOTPOP"].groupby(assign(blocks, pieces)).sum() + weights = normalize(weights, level=0) # Use blocks to estimate population of each piece new_precincts[columns] = prorate(pieces, old_precincts[columns], weights=weights) diff --git a/tests/test_repair.py b/tests/test_repair.py index cfac09d..32c3ff4 100644 --- a/tests/test_repair.py +++ b/tests/test_repair.py @@ -1,6 +1,7 @@ import geopandas import maup from maup.repair import count_overlaps, autorepair, quick_repair +from maup.assign import AssigmentWarning import pytest # These tests are losely based off the test_example_case in test_prorate.py @@ -66,7 +67,13 @@ def test_crop_to(): # Calculate without cropping pieces = maup.intersections(old_precincts, new_precincts, area_cutoff=0) - weights = blocks["TOTPOP"].groupby(maup.assign(blocks, pieces)).sum() + + with pytest.warns( + AssigmentWarning, match="Some units in the source geometry were unassigned." + ): + # Weight by prorated population from blocks + weights = blocks["TOTPOP"].groupby(maup.assign(blocks, pieces)).sum() + weights = maup.normalize(weights, level=0) new_precincts[columns] = maup.prorate( pieces, old_precincts[columns], weights=weights @@ -76,7 +83,13 @@ def test_crop_to(): old_precincts["geometries"] = maup.crop_to(old_precincts, new_precincts) new_precincts_cropped = new_precincts.copy() pieces = maup.intersections(old_precincts, new_precincts_cropped, area_cutoff=0) - weights = blocks["TOTPOP"].groupby(maup.assign(blocks, pieces)).sum() + + with pytest.warns( + AssigmentWarning, match="Some units in the source geometry were unassigned." + ): + # Weight by prorated population from blocks + weights = blocks["TOTPOP"].groupby(maup.assign(blocks, pieces)).sum() + weights = maup.normalize(weights, level=0) new_precincts_cropped[columns] = maup.prorate( pieces, old_precincts[columns], weights=weights From bb45dfb3de49dfbf7538433f0b7638ffdd85d06d Mon Sep 17 00:00:00 2001 From: peterrrock2 <27579114+peterrrock2@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:22:16 -0600 Subject: [PATCH 6/6] Change `unary_union` -> `union_all` so shapely and geopandas consistent --- maup/repair.py | 16 ++-- maup/smart_repair.py | 200 ++++++++++++++++++++----------------------- 2 files changed, 99 insertions(+), 117 deletions(-) diff --git a/maup/repair.py b/maup/repair.py index de2b7d4..00ced32 100644 --- a/maup/repair.py +++ b/maup/repair.py @@ -11,7 +11,7 @@ MultiLineString, GeometryCollection, ) -from shapely.ops import unary_union +from shapely import union_all from .adjacencies import adjacencies from .assign import assign_to_max @@ -81,7 +81,7 @@ def holes_of_union(geometries): f"Must be a Polygon or MultiPolygon (got types {set([x.geom_type for x in geometries])})!" ) - union = unary_union(geometries) + union = union_all(geometries) series = holes(union) series.crs = geometries.crs return series @@ -164,7 +164,7 @@ def resolve_overlaps(geometries, relative_threshold=0.1, force_polygons=False): pandas.concat([overlaps.droplevel(1), overlaps.droplevel(0)]), crs=overlaps.crs ) with_overlaps_removed = geometries.apply( - lambda x: x.difference(unary_union(to_remove)) + lambda x: x.difference(union_all(to_remove)) ) return absorb_by_shared_perimeter( @@ -263,7 +263,7 @@ def crop_to(source, target): """ Crops the source geometries to the target geometries. """ - target_union = unary_union(get_geometries(target)) + target_union = union_all(get_geometries(target)) cropped_geometries = get_geometries(source).apply( lambda x: x.intersection(target_union) ) @@ -291,7 +291,7 @@ def expand_to(source, target, force_polygons=False): else: geometries = get_geometries(source).make_valid() - source_union = unary_union(geometries) + source_union = union_all(geometries) leftover_geometries = get_geometries(target).apply(lambda x: x - source_union) leftover_geometries = leftover_geometries[~leftover_geometries.is_empty].explode( @@ -322,14 +322,14 @@ def doctor(source, target=None, silent=False, accept_holes=False): False. (Default is accept_holes = False.) """ shapefiles = [source] - source_union = unary_union(get_geometries(source)) + source_union = union_all(get_geometries(source)) health_check = True if target is not None: shapefiles.append(target) - target_union = unary_union(get_geometries(target)) + target_union = union_all(get_geometries(target)) sym_area = target_union.symmetric_difference(source_union).area if sym_area != 0: @@ -484,7 +484,7 @@ def absorb_by_shared_perimeter( assignment = assignment[under_threshold] sources_to_absorb = GeoSeries( - sources.groupby(assignment).apply(unary_union), + sources.groupby(assignment).apply(union_all), crs=sources.crs, ) diff --git a/maup/smart_repair.py b/maup/smart_repair.py index 936c043..71890a3 100644 --- a/maup/smart_repair.py +++ b/maup/smart_repair.py @@ -7,9 +7,9 @@ import shapely from geopandas import GeoSeries, GeoDataFrame -from shapely import make_valid, extract_unique_points +from shapely import make_valid, extract_unique_points, union_all from shapely.strtree import STRtree -from shapely.ops import unary_union, polygonize, linemerge, nearest_points +from shapely.ops import polygonize, linemerge, nearest_points from shapely.geometry import ( Polygon, MultiPolygon, @@ -151,7 +151,7 @@ def smart_repair( if geometries_df.loc[i, "geometry"] is None: geometries_df.loc[i, "geometry"] = Polygon() if geometries_df.loc[i, "geometry"].geom_type == "GeometryCollection": - geometries_df.loc[i, "geometry"] = unary_union( + geometries_df.loc[i, "geometry"] = union_all( [ x for x in geometries_df.loc[i, "geometry"].geoms @@ -193,7 +193,7 @@ def smart_repair( geometries_df.loc[i, "geometry"] ) if geometries_df.loc[i, "geometry"].geom_type == "GeometryCollection": - geometries_df.loc[i, "geometry"] = unary_union( + geometries_df.loc[i, "geometry"] = union_all( [ x for x in geometries_df.loc[i, "geometry"].geoms @@ -206,7 +206,7 @@ def smart_repair( regions_df.loc[i, "geometry"] ) if regions_df.loc[i, "geometry"].geom_type == "GeometryCollection": - regions_df.loc[i, "geometry"] = unary_union( + regions_df.loc[i, "geometry"] = union_all( [ x for x in regions_df.loc[i, "geometry"].geoms @@ -439,7 +439,7 @@ def smart_repair( )[-1] poly_to_add_to = max_shared_perim[0] reconstructed_df.loc[poly_to_add_to, "geometry"] = ( - unary_union( + union_all( [ reconstructed_df.loc[ poly_to_add_to, "geometry" @@ -689,7 +689,7 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None for r_ind in regions_df.index: this_region_holes_df = holes_df[holes_df["region"] == r_ind] this_region_consolidated_holes = ( - GeoSeries([unary_union(this_region_holes_df["geometry"])]) + GeoSeries([union_all(this_region_holes_df["geometry"])]) .explode(index_parts=False) .reset_index(drop=True) ) @@ -713,7 +713,7 @@ def building_blocks(geometries_df, snap_magnitude=None, nest_within_regions=None else: # Do the same thing we did for holes within each region to consolidate them: all_consolidated_holes = ( - GeoSeries([unary_union(holes_df["geometry"])]) + GeoSeries([union_all(holes_df["geometry"])]) .explode(index_parts=False) .reset_index(drop=True) ) @@ -766,7 +766,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): for ind in overlap_tower[0].index: this_poly_ind = list(overlap_tower[0]["polygon indices"][ind])[0] this_piece = overlap_tower[0]["geometry"][ind] - geometries_df.loc[this_poly_ind, "geometry"] = unary_union( + geometries_df.loc[this_poly_ind, "geometry"] = union_all( [geometries_df.loc[this_poly_ind, "geometry"], this_piece] ) @@ -849,7 +849,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): overlaps_df.loc[o_ind, "geometry"] ) ).length > 0: - geometries_disconnected_df.loc[g_ind, "geometry"] = unary_union( + geometries_disconnected_df.loc[g_ind, "geometry"] = union_all( [ geometries_disconnected_df.loc[g_ind, "geometry"], overlaps_df.loc[o_ind, "geometry"], @@ -907,7 +907,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): if len(shared_perimeters) > 0: max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + geometries_df.loc[poly_to_add_to, "geometry"] = union_all( [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap] ) @@ -949,7 +949,7 @@ def reconstruct_from_overlap_tower(geometries_df, overlap_tower, nested=False): if len(shared_perimeters) > 0: max_shared_perim = sorted(shared_perimeters, key=lambda tup: tup[1])[-1] poly_to_add_to = max_shared_perim[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + geometries_df.loc[poly_to_add_to, "geometry"] = union_all( [geometries_df.loc[poly_to_add_to, "geometry"], this_overlap] ) @@ -1087,7 +1087,7 @@ def smart_close_gaps(geometries_df, holes_df): poly_to_add_to = list( set(this_hole_boundaries_df["target"]).difference({-1}) )[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + geometries_df.loc[poly_to_add_to, "geometry"] = union_all( [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] ) @@ -1109,7 +1109,7 @@ def smart_close_gaps(geometries_df, holes_df): ] ) ) - geometries_df.loc[g_ind, "geometry"] = unary_union( + geometries_df.loc[g_ind, "geometry"] = union_all( [geometries_df.loc[g_ind, "geometry"], this_segment_poly_to_add] ) @@ -1130,7 +1130,7 @@ def smart_close_gaps(geometries_df, holes_df): poly_to_add_to = touching_geoms[0] else: poly_to_add_to = touching_geoms[1] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + geometries_df.loc[poly_to_add_to, "geometry"] = union_all( [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] ) @@ -1207,24 +1207,20 @@ def smart_close_gaps(geometries_df, holes_df): if nearest_point_position == 0: # Add the entire hole to target_geometries[1]. - geometries_df.loc[target_geometries[1], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[1], "geometry"], - this_hole, - ] - ) + geometries_df.loc[target_geometries[1], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[1], "geometry"], + this_hole, + ] ) elif nearest_point_position == len(ext_boundary_points) - 1: # Add the entire hole to target_geometries[2]. - geometries_df.loc[target_geometries[2], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[2], "geometry"], - this_hole, - ] - ) + geometries_df.loc[target_geometries[2], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[2], "geometry"], + this_hole, + ] ) else: @@ -1238,7 +1234,7 @@ def smart_close_gaps(geometries_df, holes_df): ) ) - poly1_to_add_boundary = unary_union( + poly1_to_add_boundary = union_all( [ this_hole_boundaries[1], sp, @@ -1248,16 +1244,14 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.loc[target_geometries[1], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[1], "geometry"], - poly1_to_add, - ] - ) + geometries_df.loc[target_geometries[1], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[1], "geometry"], + poly1_to_add, + ] ) - poly2_to_add_boundary = unary_union( + poly2_to_add_boundary = union_all( [ this_hole_boundaries[2], sp, @@ -1267,13 +1261,11 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.loc[target_geometries[2], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[2], "geometry"], - poly2_to_add, - ] - ) + geometries_df.loc[target_geometries[2], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[2], "geometry"], + poly2_to_add, + ] ) # Otherwise, construct the incenter of the circumscribing triangle. @@ -1306,7 +1298,7 @@ def smart_close_gaps(geometries_df, holes_df): for point in incenter_triangle_vertices ] this_hole_partition = polygonize( - unary_union([this_hole.boundary] + incenter_segments) + union_all([this_hole.boundary] + incenter_segments) ) paths_to_main_vertices = [] @@ -1326,7 +1318,7 @@ def smart_close_gaps(geometries_df, holes_df): ) ) - poly0_to_add_boundary = unary_union( + poly0_to_add_boundary = union_all( [ this_hole_boundaries[0], paths_to_main_vertices[0], @@ -1334,16 +1326,14 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly0_to_add = polygonize(poly0_to_add_boundary)[0] - geometries_df.loc[target_geometries[0], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[0], "geometry"], - poly0_to_add, - ] - ) + geometries_df.loc[target_geometries[0], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[0], "geometry"], + poly0_to_add, + ] ) - poly1_to_add_boundary = unary_union( + poly1_to_add_boundary = union_all( [ this_hole_boundaries[1], paths_to_main_vertices[1], @@ -1351,16 +1341,14 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.loc[target_geometries[1], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[1], "geometry"], - poly1_to_add, - ] - ) + geometries_df.loc[target_geometries[1], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[1], "geometry"], + poly1_to_add, + ] ) - poly2_to_add_boundary = unary_union( + poly2_to_add_boundary = union_all( [ this_hole_boundaries[2], paths_to_main_vertices[2], @@ -1368,13 +1356,11 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.loc[target_geometries[2], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[2], "geometry"], - poly2_to_add, - ] - ) + geometries_df.loc[target_geometries[2], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[2], "geometry"], + poly2_to_add, + ] ) else: @@ -1412,11 +1398,11 @@ def smart_close_gaps(geometries_df, holes_df): # if nearest_point_position == 0: # # Add the entire hole to target_geometries[1]. - # geometries_df.loc[target_geometries[1], "geometry"] = unary_union([geometries_df.loc[target_geometries[1], "geometry"], this_hole]) + # geometries_df.loc[target_geometries[1], "geometry"] = union_all([geometries_df.loc[target_geometries[1], "geometry"], this_hole]) # elif nearest_point_position == len(ext_boundary_points) - 1: # # Add the entire hole to target_geometries[2]. - # geometries_df.loc[target_geometries[2], "geometry"] = unary_union([geometries_df.loc[target_geometries[2], "geometry"], this_hole]) + # geometries_df.loc[target_geometries[2], "geometry"] = union_all([geometries_df.loc[target_geometries[2], "geometry"], this_hole]) # else: @@ -1430,7 +1416,7 @@ def smart_close_gaps(geometries_df, holes_df): ) ) - poly1_to_add_boundary = unary_union( + poly1_to_add_boundary = union_all( [ this_hole_boundaries[1], sp, @@ -1440,16 +1426,14 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly1_to_add = polygonize(poly1_to_add_boundary)[0] - geometries_df.loc[target_geometries[1], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[1], "geometry"], - poly1_to_add, - ] - ) + geometries_df.loc[target_geometries[1], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[1], "geometry"], + poly1_to_add, + ] ) - poly2_to_add_boundary = unary_union( + poly2_to_add_boundary = union_all( [ this_hole_boundaries[2], sp, @@ -1459,13 +1443,11 @@ def smart_close_gaps(geometries_df, holes_df): ] ) poly2_to_add = polygonize(poly2_to_add_boundary)[0] - geometries_df.loc[target_geometries[2], "geometry"] = ( - unary_union( - [ - geometries_df.loc[target_geometries[2], "geometry"], - poly2_to_add, - ] - ) + geometries_df.loc[target_geometries[2], "geometry"] = union_all( + [ + geometries_df.loc[target_geometries[2], "geometry"], + poly2_to_add, + ] ) else: # If len(this_hole_boundaries_df) >= 4 @@ -1582,7 +1564,7 @@ def smart_close_gaps(geometries_df, holes_df): if poly_to_add.area > 0: found_triangles = True geometries_df.loc[geom_int, "geometry"] = ( - unary_union( + union_all( [ geometries_df.loc[ geom_int, "geometry" @@ -1733,7 +1715,7 @@ def smart_close_gaps(geometries_df, holes_df): == 0 ): geometries_df.loc[geom1, "geometry"] = ( - unary_union( + union_all( [ geometries_df.loc[ geom1, "geometry" @@ -1766,7 +1748,7 @@ def smart_close_gaps(geometries_df, holes_df): > 0 ): geometries_df.loc[geom2, "geometry"] = ( - unary_union( + union_all( [ geometries_df.loc[ geom2, "geometry" @@ -1785,7 +1767,7 @@ def smart_close_gaps(geometries_df, holes_df): elif geom1 == geom2: geometries_df.loc[geom1, "geometry"] = ( - unary_union( + union_all( [ geometries_df.loc[ geom1, "geometry" @@ -1839,7 +1821,7 @@ def smart_close_gaps(geometries_df, holes_df): ).length if perim1 > perim2: geometries_df.loc[geom1, "geometry"] = ( - unary_union( + union_all( [ geometries_df.loc[ geom1, "geometry" @@ -1857,7 +1839,7 @@ def smart_close_gaps(geometries_df, holes_df): ] else: geometries_df.loc[geom2, "geometry"] = ( - unary_union( + union_all( [ geometries_df.loc[ geom2, "geometry" @@ -1924,7 +1906,7 @@ def smart_close_gaps(geometries_df, holes_df): shared_perimeters, key=lambda tup: tup[1] )[-1] poly_to_add_to = max_shared_perim[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + geometries_df.loc[poly_to_add_to, "geometry"] = union_all( [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] ) @@ -2002,7 +1984,7 @@ def small_rook_to_queen(geometries_df, min_rook_length): polys_to_remove_list = disks_to_remove_list polys_to_remove_complete = False while polys_to_remove_complete is False: - all_polys_to_remove = unary_union(polys_to_remove_list) + all_polys_to_remove = union_all(polys_to_remove_list) if ( all_polys_to_remove.geom_type == "Polygon" ): # if it's all one big polygon now @@ -2016,11 +1998,11 @@ def small_rook_to_queen(geometries_df, min_rook_length): if len(convex_polys_to_remove_list) == 1: polys_to_remove_complete = True - elif unary_union(convex_polys_to_remove_list).geom_type == "MultiPolygon": + elif union_all(convex_polys_to_remove_list).geom_type == "MultiPolygon": # Note that if the unary union is a Polygon, then this next condition # below can't hold anyway and we want polys_to_remove_complete to remain # False. - if len(unary_union(convex_polys_to_remove_list).geoms) == len( + if len(union_all(convex_polys_to_remove_list).geoms) == len( convex_polys_to_remove_list ): polys_to_remove_complete = True @@ -2099,7 +2081,7 @@ def small_rook_to_queen(geometries_df, min_rook_length): .representative_point() .intersects(poly_to_remove) ): - poly_to_remove_refined = unary_union( + poly_to_remove_refined = union_all( [poly_to_remove_refined, pieces_df.loc[p_ind, "geometry"]] ) pieces_df_indices_to_drop.append(p_ind) @@ -2119,7 +2101,7 @@ def small_rook_to_queen(geometries_df, min_rook_length): # This check is needed because the geometries in possible_geom_incides can form a # non-simply-connected region, in which case the interior holes - which may consist # of multiple geometries each - may be assigned someplace they shouldn't be! - geometries_df.loc[this_poly_ind, "geometry"] = unary_union( + geometries_df.loc[this_poly_ind, "geometry"] = union_all( [geometries_df.loc[this_poly_ind, "geometry"], this_piece] ) @@ -2163,7 +2145,7 @@ def small_rook_to_queen(geometries_df, min_rook_length): g_ind = poly_to_remove_boundaries_df.loc[b_ind, "target"] - geometries_df.loc[g_ind, "geometry"] = unary_union( + geometries_df.loc[g_ind, "geometry"] = union_all( [ geometries_df.loc[g_ind, "geometry"], Polygon(boundary_wedge_coords), @@ -2473,7 +2455,7 @@ def shortest_path_in_polygon(polygon, start, end, full_triangulation=None): # Regard the sleeve given by the union of these triangles as the "simplified" # polygon; the shortest path must be contained in this simplfied polygon. - polygon_simplified = unary_union(ordered_triangulation) + polygon_simplified = union_all(ordered_triangulation) # Now use the ordered triangulation to order the vertices of the simplified polygon, # as well as the left and right paths restricted to the simplified polygon. @@ -2671,7 +2653,7 @@ def convexify_hole_boundaries(geometries_df, holes_df): poly_to_add_to = list( set(this_hole_boundaries_df["target"]).difference({-1}) )[0] - geometries_df.loc[poly_to_add_to, "geometry"] = unary_union( + geometries_df.loc[poly_to_add_to, "geometry"] = union_all( [geometries_df.loc[poly_to_add_to, "geometry"], this_hole] ) @@ -2728,9 +2710,9 @@ def convexify_hole_boundaries(geometries_df, holes_df): polys_to_add_boundary = shapely.node(MultiLineString([thb, sp])) hole_partition_boundary = shapely.node( - unary_union([new_hole_in_progress.boundary, sp]) + union_all([new_hole_in_progress.boundary, sp]) ) - # piece_to_add_boundary = unary_union([thb, sp]) + # piece_to_add_boundary = union_all([thb, sp]) # if piece_to_add_boundary.geom_type == "MultiLineString": # piece_to_add_boundary = linemerge(piece_to_add_boundary) @@ -2738,7 +2720,7 @@ def convexify_hole_boundaries(geometries_df, holes_df): hole_partition_polys = polygonize(hole_partition_boundary) for poly_to_add in polys_to_add: - geometries_df.loc[this_geom, "geometry"] = unary_union( + geometries_df.loc[this_geom, "geometry"] = union_all( [geometries_df.loc[this_geom, "geometry"], poly_to_add] ) hole_partition_polys = [ @@ -2747,10 +2729,10 @@ def convexify_hole_boundaries(geometries_df, holes_df): if not contain_each_other(poly, poly_to_add) ] - new_hole_in_progress = unary_union(hole_partition_polys) + new_hole_in_progress = union_all(hole_partition_polys) - # piece_to_add = unary_union(polygonize(piece_to_add_boundary)) - # geometries_df.loc[this_geom, "geometry"] = unary_union([geometries_df.loc[this_geom, "geometry"], piece_to_add]) + # piece_to_add = union_all(polygonize(piece_to_add_boundary)) + # geometries_df.loc[this_geom, "geometry"] = union_all([geometries_df.loc[this_geom, "geometry"], piece_to_add]) # new_hole_in_progress = new_hole_in_progress.difference(piece_to_add) if not new_hole_in_progress.is_empty: