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.

My output feature class "Lummi_OffsetPolygons"

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.

The offset between the two polygons is 32.04 ft at that location.

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.


The fields "INTPTLAT" and "INTPTLON" have been removed from the attribute table.

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

The Lummi polygon has been simplified with a 5 feet tolerance and rotated by a random angle using the manual rotation functions.
The fields "INTPTLAT" and "INTPTLON" have been removed from the attribute table.

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!

Did this answer your question? Thanks for the feedback There was a problem submitting your feedback. Please try again later.

Still need help? Contact Us Contact Us