Source code for generalization.n100.building.hospital_church_clusters

# Importing modules
import arcpy
import random

# Importing custom modules
from custom_tools.general_tools import custom_arcpy

# Importing file manager
from file_manager.n100.file_manager_buildings import Building_N100

# Importing environment setup
from env_setup import environment_setup


# Importing timing decorator
from custom_tools.decorators.timing_decorator import timing_decorator


# Main function
[docs] @timing_decorator def main(): """ This script detects and reduces hospital and church clusters. As an example can a cluster of 5 close hospital points be reduced to only consist of 1 point. """ environment_setup.main() selecting_all_other_points_that_are_not_hospital_and_church() hospital_church_selections() find_clusters() reducing_clusters() hospitals_and_churches_too_close()
[docs] def selecting_all_other_points_that_are_not_hospital_and_church(): """ This script selects all other points that are not classified as hospital, church and touristhuts. """ # Selecting all hospitals and making feature layer custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.point_propagate_displacement___points_after_propagate_displacement___n100_building.value, expression="byggtyp_nbr IN (970, 719, 671)", output_name=Building_N100.hospital_church_clusters___all_other_points_that_are_not_hospital_church___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, inverted=True, )
[docs] @timing_decorator def hospital_church_selections(): """ Selects hospitals and churches from the input point feature class, creating separate feature layers for each category. Hospitals are selected based on 'byggtyp_nbr' values 970 and 719. Churches are selected based on 'byggtyp_nbr' value 671. """ # SQL-expressions to select hospitals and churches sql_select_all_hospital = "byggtyp_nbr IN (970, 719)" sql_select_all_church = "byggtyp_nbr = 671" # Selecting all hospitals and making feature layer custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.point_propagate_displacement___points_after_propagate_displacement___n100_building.value, expression=sql_select_all_hospital, output_name=Building_N100.hospital_church_clusters___hospital_points___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, ) # Selecting all churches and making feature layer custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.point_propagate_displacement___points_after_propagate_displacement___n100_building.value, expression=sql_select_all_church, output_name=Building_N100.hospital_church_clusters___church_points___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, )
# Finding and removing hospital and church clusters
[docs] @timing_decorator def find_clusters(): """ What: Finds hospital and church clusters. A cluster is defined as two or more points that are closer together than 200 meters. How: Clusters are found for both hospitals and churches using the 'FindPointClusters' tool. The CLUSTER_IDs are joined with the original hospital and church feature classes. Points belonging to a hospital or church cluster are selected as new layers. Points not belonging to a cluster are selected as new layers. The tool FindPointClusters has a search distance of **'200 Meters'** and a minimum of **'2 Points'**. Why? Clusters amongst hospital and churches are found because it causes "noise" in the map. We do not want the map to display clusters of big symbols, this can make the map harder to read. Therefore, we want to identify clusters and reduce these. """ print("Finding hospital and church clusters...") # Finding hospital clusters arcpy.gapro.FindPointClusters( input_points=Building_N100.hospital_church_clusters___hospital_points___n100_building.value, out_feature_class=Building_N100.hospital_church_clusters___all_hospital_clusters___n100_building.value, clustering_method="DBSCAN", minimum_points="2", search_distance="250 Meters", ) # Finding church clusters arcpy.gapro.FindPointClusters( input_points=Building_N100.hospital_church_clusters___church_points___n100_building.value, out_feature_class=Building_N100.hospital_church_clusters___all_church_clusters___n100_building.value, clustering_method="DBSCAN", minimum_points="2", search_distance="250 Meters", ) print("Joining fields...") # Join CLUSTER_ID to church points OBJECTID arcpy.management.JoinField( in_data=Building_N100.hospital_church_clusters___hospital_points___n100_building.value, in_field="OBJECTID", join_table=Building_N100.hospital_church_clusters___all_hospital_clusters___n100_building.value, join_field="OBJECTID", fields="CLUSTER_ID", ) # Join CLUSTER_ID to church points OBJECTID arcpy.management.JoinField( in_data=Building_N100.hospital_church_clusters___church_points___n100_building.value, in_field="OBJECTID", join_table=Building_N100.hospital_church_clusters___all_church_clusters___n100_building.value, join_field="OBJECTID", fields="CLUSTER_ID", ) expression_cluster = "CLUSTER_ID > 0" expression_not_cluster = "CLUSTER_ID < 0" # Making feature class of hospital points in a cluster custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___hospital_points___n100_building.value, expression=expression_cluster, output_name=Building_N100.hospital_church_clusters___hospital_points_in_cluster___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, ) # Making feature class of hospital points NOT in a cluster custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___hospital_points___n100_building.value, expression=expression_not_cluster, output_name=Building_N100.hospital_church_clusters___hospital_points_not_in_cluster___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, ) # Making feature class of church points in a cluster custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___church_points___n100_building.value, expression=expression_cluster, output_name=Building_N100.hospital_church_clusters___church_points_in_cluster___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, ) # Making feature class of church points NOT in a cluster custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___church_points___n100_building.value, expression=expression_not_cluster, output_name=Building_N100.hospital_church_clusters___church_points_not_in_cluster___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, )
[docs] @timing_decorator def reducing_clusters(): """ What: Reduces hospital and church clusters by keeping only one point for each detected cluster. This ensures that spatial redundancy is minimized, and each cluster is represented by a single point, which is helpful for cleaner visualizations in the map. Why: Clusters of hospitals or churches may result in overlapping or redundant data points, especially in dense areas. Reducing clusters by retaining only the most central point ensures the dataset remains representative without unnecessary duplication. How: The function creates a minimum bounding polygon for each cluster of points, which is then converted into a center point. The nearest hospital or church point to the center point is identified and retained, while the remaining points in the cluster are discarded. Hospital and church points that are not part of a cluster are merged with the retained cluster points. The 'RECTANGLE_BY_AREA' option is used to create the minimum bounding geometry for each cluster. The nearest point to the center of the bounding polygon is identified using a 'Near' analysis, and a dictionary is used to store the minimum distance values for each cluster. If two points have the same distance to the center, one is selected randomly. Finally, the non-clustered points are merged with the selected points from clusters, resulting in a merged feature class with reduced hospital and church points. """ # List of hospital and church layers to merge at the end # Hospital and church points that are NOT in a cluster are already put in the list merge_hospitals_and_churches_list = [ Building_N100.hospital_church_clusters___hospital_points_not_in_cluster___n100_building.value, Building_N100.hospital_church_clusters___church_points_not_in_cluster___n100_building.value, ] # Check if there are any hospital clusters, getting the number count_hospital = int( arcpy.management.GetCount( Building_N100.hospital_church_clusters___hospital_points_in_cluster___n100_building.value ).getOutput(0) ) if count_hospital > 0: print("Hospital clusters found.") print("Minimum Bounding Geometry ...") # Finding minimum bounding geometry for hospital clusters arcpy.management.MinimumBoundingGeometry( in_features=Building_N100.hospital_church_clusters___hospital_points_in_cluster___n100_building.value, out_feature_class=Building_N100.hospital_church_clusters___minimum_bounding_geometry_hospital___n100_hospital.value, geometry_type="RECTANGLE_BY_AREA", group_option="LIST", group_field="CLUSTER_ID", ) print("Feature to Point...") # Transforming minimum bounding geometry for hospital clusters to center point arcpy.management.FeatureToPoint( in_features=Building_N100.hospital_church_clusters___minimum_bounding_geometry_hospital___n100_hospital.value, out_feature_class=Building_N100.hospital_church_clusters___feature_to_point_hospital___n100_building.value, ) print("Near...") # Find hospital point closest to the center point, Near fid is automatically added to hospital points attribute table arcpy.analysis.Near( in_features=Building_N100.hospital_church_clusters___hospital_points_in_cluster___n100_building.value, near_features=Building_N100.hospital_church_clusters___feature_to_point_hospital___n100_building.value, search_radius=None, ) # Create a dictionary to store the minimum NEAR_DIST for each CLUSTER_ID min_near_dist_dict_hospital = {} print("Reducing clusters...") # Iterate through the feature class to find the minimum NEAR_DIST for each CLUSTER_ID with arcpy.da.SearchCursor( Building_N100.hospital_church_clusters___hospital_points_in_cluster___n100_building.value, ["CLUSTER_ID", "NEAR_DIST", "OBJECTID"], ) as cursor: for cluster_id, near_dist, object_id in cursor: # Check if the CLUSTER_ID is not in the dictionary, or if the current NEAR_DIST is smaller than the stored one, # or if the NEAR_DIST is equal but the OBJECTID is smaller if ( cluster_id not in min_near_dist_dict_hospital or near_dist < min_near_dist_dict_hospital[cluster_id][0] ): # Update the dictionary with the current NEAR_DIST and OBJECTID min_near_dist_dict_hospital[cluster_id] = (near_dist, object_id) # If NEAR_DIST is equal and OBJECTID is larger, randomly replace the existing OBJECTID elif ( near_dist == min_near_dist_dict_hospital[cluster_id][0] and object_id > min_near_dist_dict_hospital[cluster_id][1] ): if random.random() < 0.5: # Randomly choose which OBJECTID to keep min_near_dist_dict_hospital[cluster_id] = (near_dist, object_id) # Create a list of OBJECTIDs with the minimum NEAR_DIST for each CLUSTER_ID min_near_dist_object_ids_hospital = [ object_id for _, object_id in min_near_dist_dict_hospital.values() ] # Make a new layer of hospital cluster points that we want to keep custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___hospital_points_in_cluster___n100_building.value, expression=f"OBJECTID IN ({','.join(map(str, min_near_dist_object_ids_hospital))})", output_name=Building_N100.hospital_church_clusters___chosen_hospitals_from_cluster___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, inverted=False, ) # Add selected hospitals to the merge list merge_hospitals_and_churches_list.append( Building_N100.hospital_church_clusters___chosen_hospitals_from_cluster___n100_building.value ) # Check if there are any hospital clusters, getting the number count_church = int( arcpy.management.GetCount( Building_N100.hospital_church_clusters___church_points_in_cluster___n100_building.value ).getOutput(0) ) if count_church > 0: print("Church clusters found.") print("Minimum Bounding Geometry running...") # Finding minimum bounding geometry for church clusters arcpy.management.MinimumBoundingGeometry( in_features=Building_N100.hospital_church_clusters___church_points_in_cluster___n100_building.value, out_feature_class=Building_N100.hospital_church_clusters___minimum_bounding_geometry_church___n100_building.value, geometry_type="RECTANGLE_BY_AREA", group_option="LIST", group_field="CLUSTER_ID", ) print("Feature to Point...") # Transforming minimum bounding geometry for church clusters to center point arcpy.management.FeatureToPoint( in_features=Building_N100.hospital_church_clusters___minimum_bounding_geometry_church___n100_building.value, out_feature_class=Building_N100.hospital_church_clusters___feature_to_point_church___n100_building.value, ) print("Near...") # Find church point closest to the center point. Near fid is automatically added to church points attribute table arcpy.analysis.Near( in_features=Building_N100.hospital_church_clusters___church_points_in_cluster___n100_building.value, near_features=Building_N100.hospital_church_clusters___feature_to_point_church___n100_building.value, search_radius=None, ) # Create a dictionary to store the minimum NEAR_DIST for each CLUSTER_ID min_near_dist_dict_church = {} # Iterate through the feature class to find the minimum NEAR_DIST for each CLUSTER_ID with arcpy.da.SearchCursor( Building_N100.hospital_church_clusters___church_points_in_cluster___n100_building.value, ["CLUSTER_ID", "NEAR_DIST", "OBJECTID"], ) as cursor: for cluster_id, near_dist, object_id in cursor: # Check if the CLUSTER_ID is not in the dictionary, or if the current NEAR_DIST is smaller than the stored one, # or if the NEAR_DIST is equal but the OBJECTID is smaller if ( cluster_id not in min_near_dist_dict_church or near_dist < min_near_dist_dict_church[cluster_id][0] or ( near_dist == min_near_dist_dict_church[cluster_id][0] and object_id < min_near_dist_dict_church[cluster_id][1] ) ): # Update the dictionary with the current NEAR_DIST and OBJECTID min_near_dist_dict_church[cluster_id] = (near_dist, object_id) # If NEAR_DIST is equal and OBJECTID is larger, randomly replace the existing OBJECTID elif ( near_dist == min_near_dist_dict_church[cluster_id][0] and object_id > min_near_dist_dict_church[cluster_id][1] ): if random.random() < 0.5: # Randomly choose which OBJECTID to keep min_near_dist_dict_church[cluster_id] = (near_dist, object_id) # Create a list of OBJECTIDs with the minimum NEAR_DIST for each CLUSTER_ID min_near_dist_object_ids_church = [ object_id for _, object_id in min_near_dist_dict_church.values() ] # Make a layer of church points in a cluster custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___church_points_in_cluster___n100_building.value, expression=f"OBJECTID IN ({','.join(map(str, min_near_dist_object_ids_church))})", output_name=Building_N100.hospital_church_clusters___chosen_churches_from_cluster___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, ) # Add selected churches to the merge list merge_hospitals_and_churches_list.append( Building_N100.hospital_church_clusters___chosen_churches_from_cluster___n100_building.value ) # Merge the final hospital and church layers arcpy.management.Merge( inputs=merge_hospitals_and_churches_list, output=Building_N100.hospital_church_clusters___reduced_hospital_and_church_points_merged___n100_building.value, )
[docs] @timing_decorator def hospitals_and_churches_too_close(): """ This function identifies and filters out church points that are located too close to hospital points (within 215 meters). Only churches that are farther than 215 meters from hospitals are retained, while the rest are excluded from the final dataset. The result is a reduced and cleaner spatial dataset of hospitals and churches. """ # SQL-expressions to select hospitals and churches sql_select_all_hospital = "byggtyp_nbr IN (970, 719)" sql_select_all_church = "byggtyp_nbr = 671" # Selecting all hospitals and making feature layer custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___reduced_hospital_and_church_points_merged___n100_building.value, expression=sql_select_all_hospital, output_name=Building_N100.hospital_church_clusters___selecting_hospital_points_after_cluster_reduction___n100_building.value, ) # Selecting all churches and making feature layer custom_arcpy.select_attribute_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___reduced_hospital_and_church_points_merged___n100_building.value, expression=sql_select_all_church, output_name=Building_N100.hospital_church_clusters___selecting_church_points_after_cluster_reduction___n100_building.value, selection_type=custom_arcpy.SelectionType.NEW_SELECTION, ) # Selecting ONLY churches that are MORE THAN 215 Meters away from hospitals custom_arcpy.select_location_and_make_permanent_feature( input_layer=Building_N100.hospital_church_clusters___selecting_church_points_after_cluster_reduction___n100_building.value, overlap_type=custom_arcpy.OverlapType.WITHIN_A_DISTANCE, select_features=Building_N100.hospital_church_clusters___selecting_hospital_points_after_cluster_reduction___n100_building.value, output_name=Building_N100.hospital_church_clusters___church_points_NOT_too_close_to_hospitals___n100_building.value, search_distance="215 Meters", inverted=True, ) # Merge the final hospital and church layers after potentially deleting churches arcpy.management.Merge( inputs=[ Building_N100.hospital_church_clusters___selecting_hospital_points_after_cluster_reduction___n100_building.value, Building_N100.hospital_church_clusters___church_points_NOT_too_close_to_hospitals___n100_building.value, Building_N100.hospital_church_clusters___all_other_points_that_are_not_hospital_church___n100_building.value, ], output=Building_N100.hospital_church_clusters___final___n100_building.value, ) print("Hospital and church clusters finished.")
if __name__ == "__main__": main()