Calculating Geometric Offsets Based on Target Area in GIS

Introduction

Land subdivision sometimes involves offsetting a line segment between two non-parallel boundaries to achieve a specific polygon area.

The Scenario:
A user initiates a tool and defines:

  1. Line 1: Defined by fixed vertex A and direction point B.
  2. Line 2: Defined by fixed vertex C and direction point D.
  3. Target Area: A specific area value to be contained within the generated trapezoid.

The Goal:
Calculate the offset distance and the coordinates of the new segment such that the area of the polygon equals the Target Area.


The Geometric Concept: The “Virtual Triangle”

Since lines and are not parallel, they will eventually intersect at a specific point. Let us call this intersection point .

  1. The Base Triangle: The segments and form a triangle (the “Virtual Triangle”).
  2. The Scaled Triangle: When we offset the line to a new position , we create a new, larger triangle .
  3. Similarity: Because the offset lines lie on the original vectors, is mathematically similar to .

The area of the desired trapezoid () is simply the difference between the new triangle area and the original virtual triangle area.

The Scaling Law

For similar triangles, the ratio of their areas is equal to the square of the ratio of their corresponding dimensions (side lengths or heights).

If we let be the area of and be the area of , the scaling factor is:

Once is found, the new coordinates and can be found by simply scaling the vectors starting from the intersection point .


The Derivation

To solve this programmatically, we need to derive the formula for the offset distance .

Let:

  • = Area of the virtual triangle .
  • = Length of segment .
  • = Altitude (height) of from to base .
  • = The user-input target area.

From triangle geometry, we know:

The relationship between the new height () and the old height () follows the scaling law:

Solving for :

Substituting :


The Algorithm

To implement this in a GIS tool (like a QGIS plugin or ArcPy script), follow this logic:

  1. Find Intersection ():
    Compute the intersection point of the infinite lines defined by vectors and . (Note: Use a tolerance variable like to handle floating-point inaccuracies when determining if lines are parallel).

  2. Calculate Initial Virtual Area ():
    Calculate the area of the triangle formed by .

  3. Determine Direction (Add or Subtract Area):
    We must determine if the user wants to expand the triangle (offset away from ) or shrink it (offset toward ).

    • Calculate the dot product of vector and input direction vector .
    • If Dot > 0: The lines are diverging. .
    • If Dot < 0: The lines are converging. .
  4. Calculate Scale Factor ():

  5. Compute New Coordinates:
    Scale the vectors from the origin :

  6. Compute Height ():
    Calculate the perpendicular distance from point to the line segment .


Python Implementation

Below is a simply python function to do such calculation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import math

def calculate_offset_geometry(ptA, ptB, ptC, ptD, target_area, epsilon=1e-8):
"""
Calculates the offset distance h and new coordinates A', C' based on area.

Args:
ptA, ptB: (x, y) tuples defining Line 1 (A is fixed vertex, B is direction).
ptC, ptD: (x, y) tuples defining Line 2 (C is fixed vertex, D is direction).
target_area: Float, the area required for polygon AA'C'C.
epsilon: Float tolerance used to determine if lines are parallel.

Returns:
dict: {'h': distance, 'A_prime': (x,y), 'C_prime': (x,y)}
"""

# --- Helper Functions ---
def get_line_intersection(p1, p2, p3, p4):
# Standard determinant method for intersection
x1, y1 = p1
x2, y2 = p2
x3, y3 = p3
x4, y4 = p4

denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)

# Use epsilon for floating-point safety instead of strict equality
if abs(denom) < epsilon:
return None # Lines are practically parallel

ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom
return (x1 + ua * (x2 - x1), y1 + ua * (y2 - y1))

def get_dist(p1, p2):
return math.sqrt((p2[0]-p1[0])**2 + (p2[1]-p1[1])**2)

def get_triangle_area(p1, p2, p3):
# Shoelace formula for triangle area
return 0.5 * abs(p1[0]*(p2[1]-p3[1]) + p2[0]*(p3[1]-p1[1]) + p3[0]*(p1[1]-p2[1]))

# --- Main Logic ---

# 1. Find Intersection O
O = get_line_intersection(ptA, ptB, ptC, ptD)

# Handle Parallel Case (Fallback)
if O is None:
L = get_dist(ptA, ptC)
h_parallel = target_area / L
# Simple translation for parallel lines
# (Implementation of parallel shift coordinates omitted for brevity)
return {'h': h_parallel, 'note': 'Lines are parallel'}

# 2. Virtual Triangle Area S0
S0 = get_triangle_area(O, ptA, ptC)

# 3. Determine Expansion vs Contraction
# Check if vector A->B goes away from O or towards O
vec_OA = (ptA[0] - O[0], ptA[1] - O[1])
vec_AB = (ptB[0] - ptA[0], ptB[1] - ptA[1])

# Dot product
dot_prod = vec_OA[0] * vec_AB[0] + vec_OA[1] * vec_AB[1]

if dot_prod > 0:
# Diverging (Expanding)
S_new = S0 + target_area
else:
# Converging (Shrinking)
S_new = S0 - target_area
# Use epsilon to prevent floating-point errors from triggering ValueError
if S_new < -epsilon:
raise ValueError("Target area exceeds the size of the converging triangle tip.")
S_new = max(0, S_new) # Clamp to 0 if it is a microscopic negative number

# 4. Scale Factor
k = math.sqrt(S_new / S0)

# 5. Calculate New Coordinates A' and C'
# Formula: NewPoint = O + (OldPoint - O) * k
Ax_prime = O[0] + (ptA[0] - O[0]) * k
Ay_prime = O[1] + (ptA[1] - O[1]) * k
Cx_prime = O[0] + (ptC[0] - O[0]) * k
Cy_prime = O[1] + (ptC[1] - O[1]) * k

A_prime = (Ax_prime, Ay_prime)
C_prime = (Cx_prime, Cy_prime)

# 6. Calculate h (Perpendicular distance from A' to Line AC)
# Distance from point A' to line defined by A and C.
numerator = abs((ptC[0]-ptA[0])*(ptA[1]-Ay_prime) - (ptA[0]-Ax_prime)*(ptC[1]-ptA[1]))
denominator = get_dist(ptA, ptC)

h = numerator / denominator

return {
'h': h,
'A_prime': A_prime,
'C_prime': C_prime
}