How to offset a polygon feature class in ArcGIS Pro and prevent reverse-engineering
This article will walk you through the steps to offset a polygon feature class in ArcGIS Pro.
Software requirement(s):
- ArcGIS Pro 3.3.0
Language (s):
- Python (step-by-step tutorial below)
Download (s):
You can download the dataset (Polygons_Offset.ppkx) used in this article for training purposes here https://tribalgis-my.sharepoint.com/:u:/p/sarques/Ec-6pzbZOhNHmdy_gmeWIRcBO17Lg_lqXAFyHwSJrIHphw?e=PXEbDv
Explanation of Methods
1. Buffer Tool
- Original Locations: The buffer method creates new features that are a specified distance away from the original features. While it does not modify the original features, it keeps them intact in the dataset. Thus, the original locations can still be easily referenced.
- Traceability: The new buffer features are directly related to the original features, making it possible to trace back to the original locations.
2. Offset Tool
- Original Locations: The offset tool creates new features at a specified distance from the original features. If the offset distance is consistent and known, it is possible to reverse-engineer the original locations.
- Traceability: Because the offset is consistent and not randomized, it is easier to trace back to the original features.
3. Python Script for Random Offset, Simplification, and Rotation
- Original Locations: The Python script that we created below applies random offsets, simplifies, and rotates the polygons, introducing multiple layers of concealment. You can choose to apply all three transformations, any two, or just the random offset.
- Traceability: This method significantly reduces the ability to reverse-engineer the original locations due to the randomness and additional transformations applied to the data.
- Safety: This makes your Python script the safest method for ensuring the privacy of the original locations, as it introduces variability and complexity that makes it difficult to trace back to the original features.
Steps to Ensure Original Polygons Cannot Be Traced
1. Do Not Store Original Geometry
Avoid storing the original coordinates or geometry as attributes in the shared dataset. This prevents any reverse engineering of the original locations.
2. Use a Randomized Offset
Instead of a uniform offset, apply a randomized or variable offset to the polygons. This makes it more difficult to reverse the process.
Example: Use a script or tool to apply random distances within a specified range to each polygon (see script below).
3. Remove Metadata and History
- Ensure that any geoprocessing history or metadata that might contain details of the offset operation is removed before sharing.
- Use the Clear Geoprocessing History tool in ArcGIS Pro.
4. Create a New Feature Class
- After applying the offset, create a new feature class and copy the offset polygons into it. This ensures that any hidden metadata or attributes related to the original polygons are not carried over.
- Use Data > Export Features to export the offset polygons to a new feature class.
5. Generalize the Polygons
- Apply a generalization tool to slightly alter the geometry of the offset polygons. This adds an additional layer of modification.
- Use the Simplify Polygon tool with minor settings to make the geometry less precise.
Steps and Adjustments for Offsetting Polygons in ArcGIS Pro (python)
Here's a comprehensive guide with all the adjustments and steps needed to offset polygons in ArcGIS Pro using the provided script below.
We are using a feature class polygon representing the Lummi Nation's boundary (Census Bureau, tl 2023). You can download the package above.
The attribute table of the Lummi Nation's feature class displays the latitude ('INTPTLAT') and longitude ('INTPTLON') information. In this case, we need to offset the polygon and also remove the coordinate information from the attribute table.
You may have different field names for latitude and longitude, such as 'X' or 'Y', 'Lat' or 'Long', or 'Latitude' and 'Longitude'. You will need to replace these names in the script below accordingly. Look for "INTPTLAT" and "INTPTLON" and replace them by your field names.
Caution: Python is case sensitive (e.g. Latitude is different than latitude).
Run the Script
- Open the Python window in ArcGIS Pro (Analysis, Python) or use an external Python IDE configured to use the ArcGIS Pro Python environment.
- Copy and paste the script below into the Python window or IDE.
- Modify the script with your feature class (fc), .gdb names and/or pathnames, output feature class, coordinate fields.
- Execute the script.
Your pathname should have forward slashes as backslashes in file paths are interpreted as escape characters. You can edit them manually and replace the backslashes to forward slashes.
Alternatively, you can use raw string literals by prefixing the path with r or by using double backslashes.
Note: Python is space-sensitive, indentation-sensitive, and case sensitive.
import arcpy import random # Set the input and output feature classes input_fc = "OriginalPolygons" # Name of your feature class (fc) as it appears in the Table of Contents output_gdb = "path/to/your/geodatabase/something.gdb" # pathname to your geodatabase output_fc = "OffsetPolygons" # Name of your output feature class (fc). aka your offset polygon offset_range = (15, 50) # Offset range in feet # Create a copy of the feature class arcpy.management.CopyFeatures(input_fc, f"{output_gdb}/{output_fc}") # Add fields to store the offset distances (optional, for verification) arcpy.management.AddField(f"{output_gdb}/{output_fc}", "Offset_X", "DOUBLE") arcpy.management.AddField(f"{output_gdb}/{output_fc}", "Offset_Y", "DOUBLE") # Calculate random offsets and apply to the geometry with arcpy.da.UpdateCursor(f"{output_gdb}/{output_fc}", ["SHAPE@", "Offset_X", "Offset_Y"]) as cursor: for row in cursor: offset_distance = random.uniform(*offset_range) # Apply the offset to the geometry new_geometry = row[0].buffer(offset_distance) row[0] = new_geometry row[1] = offset_distance row[2] = offset_distance cursor.updateRow(row) # Remove the offset fields and any fields containing original coordinates fields_to_remove = ["Offset_X", "Offset_Y", "INTPTLAT", "INTPTLON"] arcpy.management.DeleteField(f"{output_gdb}/{output_fc}", fields_to_remove) # Verify fields # List all fields in the feature class fields = arcpy.ListFields(f"{output_gdb}/{output_fc}") # Check if 'Offset_X', 'Offset_Y', 'INTPTLAT', and 'INTPTLON' fields are present fields_exist = {field.name for field in fields} removed_fields = set(fields_to_remove) # Print the results if removed_fields.isdisjoint(fields_exist): print(f"Fields {removed_fields} are not present in the attribute table.") else: print(f"Some fields in {removed_fields} are still present in the attribute table: {removed_fields & fields_exist}") # Debug information print("All fields in the feature class:") for field in fields: print(field.name)
Below is my python script using the example of the Lummi Nation's polygon.
My polygon feature class or input_fc: Lummi Nation_tl2023_proj
My geodatabase: Polygons_Offset.gdb
My output feature class or output_fc: Lummi_OffsetPolygons
My latitude: INTPTLAT
My longitude: INTPTLON
Expected Output
A new feature class named "OffsetPolygons" (your output fc) will be created in the specified geodatabase something.gdb (your output geodatabase).
Each polygon in the original feature class (your input fc) will be offset by a random distance between 15 and 50 feet (or any values you specified).
The script will print out all the fields in the feature class after attempting to delete Offset_X, Offset_Y, Your Latitude, and Your Longitude.
If any of the fields to be removed are still present, the script will specifically identify which ones.
The optional fields Offset_X and Offset_Y are added for verification and then removed to ensure no traceable information about the original polygons remains.
Verification
1. Check Output in ArcGIS Pro
Verify that the new OffsetPolygons feature class appears in the geodatabase and map.
Inspect the polygons to ensure they have been offset correctly.
2. Verify No Original Geometry
Ensure that the fields Offset_X and Offset_Y and "Your Latitude" and "Your Longitude" are removed to prevent reverse engineering of the original polygons.
By following the provided script and steps, the offset operation significantly alters the original polygons, and no direct attributes or metadata are left that would allow someone to trace back to the original polygon locations.
However, there are a few additional considerations to ensure maximum privacy:
Random Offset Distance
The script uses a random offset distance between 15 and 50 feet, which varies for each polygon, making it difficult to reverse-engineer the original locations.
Removal of Verification Fields
The Offset_X and Offset_Y fields, which store the offset distances, are added for verification purposes and then removed. This ensures that no direct information about the offset distance is left in the final dataset.
Removal of Coordinate Fields
The latitude and longitude fields, which store the coordinates are removed. This ensures that no direct information about the location is left in the final dataset.
3. Additional steps for enhanced privacy (optional)
Applying additional generalization or simplification to the polygons can further obscure the original shapes, making it even harder to deduce the original locations.
- Generalize Polygons
Use the Simplify Polygon tool to slightly alter the geometry of the offset polygons.
- Random Rotation (vector geometry)
Apply a small random rotation to each polygon to further obscure their original positions.
We added these two steps in the script below.
import arcpy import random import math # Set the input and output feature classes input_fc = "OriginalPolygons" # Name as it appears in the Table of Contents output_gdb = "path/to/your/geodatabase/something.gdb" # pathname to your geodatabase output_fc = "OffsetPolygons" # Name of your output feature class (fc) aka your offset, simplified and rotated polygon simplified_fc = f"{output_gdb}/SimplifiedPolygons" offset_range = (15, 50) # Offset range in feet # Create a copy of the feature class arcpy.management.CopyFeatures(input_fc, f"{output_gdb}/{output_fc}") # Add fields to store the offset distances (optional, for verification) arcpy.management.AddField(f"{output_gdb}/{output_fc}", "Offset_X", "DOUBLE") arcpy.management.AddField(f"{output_gdb}/{output_fc}", "Offset_Y", "DOUBLE") # Calculate random offsets and apply to the geometry with arcpy.da.UpdateCursor(f"{output_gdb}/{output_fc}", ["SHAPE@", "Offset_X", "Offset_Y"]) as cursor: for row in cursor: offset_distance = random.uniform(*offset_range) # Apply the offset to the geometry new_geometry = row[0].buffer(offset_distance) row[0] = new_geometry row[1] = offset_distance row[2] = offset_distance cursor.updateRow(row) # Remove the offset fields and any fields containing original coordinates fields_to_remove = ["Offset_X", "Offset_Y", "INTPTLAT", "INTPTLON"] arcpy.management.DeleteField(f"{output_gdb}/{output_fc}", fields_to_remove) # Verify fields # List all fields in the feature class fields = arcpy.ListFields(f"{output_gdb}/{output_fc}") # Check if 'Offset_X', 'Offset_Y', 'INTPTLAT', and 'INTPTLON' fields are present fields_exist = {field.name for field in fields} removed_fields = set(fields_to_remove) # Print the results if removed_fields.isdisjoint(fields_exist): print(f"Fields {removed_fields} are not present in the attribute table.") else: print(f"Some fields in {removed_fields} are still present in the attribute table: {removed_fields & fields_exist}") # Debug information print("All fields in the feature class:") for field in fields: print(field.name) # Simplify polygons after verification arcpy.cartography.SimplifyPolygon(f"{output_gdb}/{output_fc}", simplified_fc, "BEND_SIMPLIFY", 5) # 5 feet tolerance, adjust as needed # Function to rotate a point around a given origin def rotate_point(point, angle, origin): angle_rad = math.radians(angle) x, y = point.X - origin.X, point.Y - origin.Y x_new = x * math.cos(angle_rad) - y * math.sin(angle_rad) y_new = x * math.sin(angle_rad) + y * math.cos(angle_rad) return arcpy.Point(x_new + origin.X, y_new + origin.Y) # Function to rotate a geometry around its centroid def rotate_geometry(geometry, angle): centroid = geometry.centroid if geometry.type == 'polygon': new_parts = [] for part in geometry: new_part = [rotate_point(p, angle, centroid) for p in part if p] new_parts.append(arcpy.Array(new_part)) return arcpy.Polygon(arcpy.Array(new_parts), geometry.spatialReference) else: return geometry # Random rotation of simplified polygons with arcpy.da.UpdateCursor(simplified_fc, ["SHAPE@"]) as cursor: for row in cursor: angle = random.uniform(0, 360) # Random rotation angle rotated_geometry = rotate_geometry(row[0], angle) row[0] = rotated_geometry cursor.updateRow(row)
Results
Adjustments
Manual Rotation Functions:
- Defined rotate_point to rotate a point around a given origin.
- Defined rotate_geometry to rotate the entire geometry around its centroid.
Applying Manual Rotation:
Used these functions to manually rotate each polygon in the SimplifiedPolygons (final output fc) feature class.
These methods ensure that the shared dataset contains no traceable information about the original polygons. The specified fields have been removed from the attribute table, the polygons are simplified correctly, and the rotation is applied manually.
We hope that this article has been helpful! If you have any feedback or questions, please feel free to send us an email or connect with us for a chat. The NTGISC team is here to assist you further!