Assume that all coordinates are expressed in a suitable projected Cartesian coordinate system, so Euclidean lengths and planar areas are meaningful. Land subdivision sometimes involves moving a line segment between two non-parallel boundaries to produce a polygon with a specified area.
The Scenario: A user initiates a tool and defines:
Line 1: A fixed vertex A and a direction point B.
Line 2: A fixed vertex C and a direction point D.
Target Area: The required area of the generated trapezoid, expressed in the square units of the projected coordinate system.
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”
Because lines and are not parallel, their infinite extensions intersect at a unique point. Let us call this intersection point .
The Base Triangle: The segments and form a triangle (the “Virtual Triangle”).
The Scaled Triangle: Moving to creates another triangle, , which may be larger or smaller than .
Similarity: Multiplying both vectors from by the same scale factor makes parallel to . Therefore, is similar to .
The area of the desired trapezoid () is the absolute difference between the areas of the two triangles.
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 non-negative 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:
be the area of the virtual triangle .
be the length of segment .
be the altitude of from to base .
be the user-specified target area.
From triangle geometry, we know:
For an outward subdivision, and the new height is . The scaling law gives
Solving for :
Substituting :
For an inward subdivision, and the new height is . Therefore,
The Algorithm
To implement this in a GIS tool, such as a QGIS plugin or an ArcPy script, follow this logic:
Find Intersection (): Compute the intersection point of the infinite lines defined by vectors and . Use a scale-aware tolerance to identify parallel or nearly parallel lines. Parallel boundaries require a separate construction and are outside the non-parallel scenario considered here.
Calculate Initial Virtual Area (): Calculate the area of the triangle formed by .
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 both and .
Both dot products must have the same sign; otherwise, the two direction points select inconsistent branches from .
If both are positive: The boundaries point away from , so .
If both are negative: The boundaries point toward , so .
Calculate Scale Factor ():
Compute New Coordinates: Scale the vectors from the origin :
Compute Height (): Calculate the perpendicular distance from to the infinite line through and .
Python Implementation
Below is a simple Python function that performs this calculation.
defcalculate_offset_geometry( ptA, ptB, ptC, ptD, target_area, epsilon=1e-8 ): """ Calculate offset distance h and new coordinates A', C' from a target area. All points must use the same projected Cartesian coordinate system. Args: ptA, ptB: (x, y) tuples defining line 1. ptC, ptD: (x, y) tuples defining line 2. target_area: Required area of polygon AA'C'C. epsilon: Relative tolerance for geometric comparisons. Returns: A dictionary containing h, A_prime, and C_prime. """ if epsilon <= 0: raise ValueError("epsilon must be positive.") ifnot math.isfinite(target_area) or target_area < 0: raise ValueError("target_area must be a finite, non-negative number.")
coordinates = (*ptA, *ptB, *ptC, *ptD) iflen(coordinates) != 8ornotall(map(math.isfinite, coordinates)): raise ValueError("Each point must contain two finite coordinates.")
if length_ab <= length_tolerance or length_cd <= length_tolerance: raise ValueError("Each boundary line requires two distinct points.") if base_length <= length_tolerance: raise ValueError("A and C must be distinct points.")
denominator = cross(direction_ab, direction_cd) ifabs(denominator) <= epsilon * length_ab * length_cd: raise ValueError("Boundary lines are parallel or nearly parallel.")
# A + parameter * AB is the intersection O. parameter = cross(vector(ptA, ptC), direction_cd) / denominator O = ( ptA[0] + parameter * direction_ab[0], ptA[1] + parameter * direction_ab[1], )
ifabs(direction_a) <= ambiguous_a orabs(direction_c) <= ambiguous_c: raise ValueError("A boundary direction is numerically ambiguous.") if (direction_a > 0) != (direction_c > 0): raise ValueError("AB and CD select inconsistent branches from O.")
initial_area = twice_initial_area / 2.0 if direction_a > 0: new_area = initial_area + target_area else: if target_area > initial_area andnot math.isclose( target_area, initial_area, rel_tol=epsilon ): raise ValueError( "Target area exceeds the size of the converging triangle tip." ) new_area = max(0.0, initial_area - target_area)
Another Perspective: Determinants and Vector Projection
The virtual-triangle method can also be described entirely using linear algebra. This perspective treats triangle area as a determinant and the offset distance as the length of a vector component. It provides a compact alternative formulation and another way to verify the geometry. In this section, is the same scale factor as above, and is the same target area.
Translate the coordinate system so that the intersection point is the origin. Define
The determinant of is the signed area of the parallelogram formed by and . Therefore, the area of the virtual triangle is
Scale both vectors by the same factor :
The segment joining the scaled endpoints is therefore parallel to .
If , then
For an outward subdivision with target area ,
Substituting gives
For an inward subdivision, the new triangle is smaller, so
Finally, translate the scaled vectors back to the original coordinate system:
Finding the offset distance by projection
Let the displacement from to be
and let the original base direction be
The component of parallel to is
Subtracting this projection leaves the perpendicular component. Its norm is the offset distance:
NumPy implementation
The following function applies this formulation after the intersection point has been found. Passing outward=False selects the inward solution.
defsubdivision_by_linear_algebra( ptO, ptA, ptC, target_area, outward=True, epsilon=1e-12 ): if epsilon <= 0: raise ValueError("epsilon must be positive.") ifnot math.isfinite(target_area) or target_area < 0: raise ValueError("target_area must be a finite, non-negative number.")
O = np.asarray(ptO, dtype=float) A = np.asarray(ptA, dtype=float) C = np.asarray(ptC, dtype=float)
points = (O, A, C) ifany(point.shape != (2,) for point in points): raise ValueError("Each point must contain exactly two coordinates.") ifnotall(np.all(np.isfinite(point)) for point in points): raise ValueError("All coordinates must be finite.")
a = A - O c = C - O base = C - A length_a = np.linalg.norm(a) length_c = np.linalg.norm(c) base_length = np.linalg.norm(base) geometry_scale = max(1.0, length_a, length_c, base_length) length_tolerance = epsilon * geometry_scale matrix = np.column_stack((a, c)) twice_initial_area = abs(np.linalg.det(matrix))
if ( length_a <= length_tolerance or length_c <= length_tolerance or twice_initial_area <= epsilon * length_a * length_c ): raise ValueError("The virtual triangle is degenerate.")
if scale_squared < -epsilon: raise ValueError("The inward target area exceeds the virtual triangle.") scale = math.sqrt(max(0.0, scale_squared))
A_prime = O + scale * a C_prime = O + scale * c
displacement = A_prime - A base_squared = np.dot(base, base) if base_squared <= length_tolerance**2: raise ValueError("A and C must be distinct points.")
parallel_component = np.dot(displacement, base) / base_squared * base perpendicular_component = displacement - parallel_component h = np.linalg.norm(perpendicular_component)