1
#!/usr/bin/env python3
"""
Route Buffer Coverage Generator
This script takes an existing GPX route file and generates a return path with a zig-zag pattern
that ensures coverage of a specified buffer zone around the original route. This is useful for
forcing navigation apps to download all map tiles in the vicinity of a planned route.
Usage:
python route_buffer_coverage.py input_gpx buffer_width spacing output_gpx
Arguments:
input_gpx: Path to the input GPX file containing the original route
buffer_width: Width of the buffer zone to cover on each side of the route (in meters)
spacing: Distance between zig-zag lines in meters (default: 200)
output_gpx: Path to save the output GPX file
"""
import argparse
import math
import xml.etree.ElementTree as ET
import numpy as np
from datetime import datetime
# Define GPX namespaces
NAMESPACES = {
'gpx': 'http://www.topografix.com/GPX/1/1',
'xsi': 'http://www.w3.org/2001/XMLSchema-instance'
}
def haversine_distance(lat1, lon1, lat2, lon2):
"""Calculate the great-circle distance between two points in meters."""
R = 6371000 # Earth radius in meters
# Convert decimal degrees to radians
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
# Haversine formula
dlat = lat2 - lat1
dlon = lon2 - lon1
a = math.sin(dlat/2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon/2)**2
c = 2 * math.asin(math.sqrt(a))
return R * c
def calculate_bearing(lat1, lon1, lat2, lon2):
"""Calculate the bearing from point 1 to point 2 in degrees."""
# Convert to radians
lat1, lon1, lat2, lon2 = map(math.radians, [lat1, lon1, lat2, lon2])
# Calculate bearing
y = math.sin(lon2 - lon1) * math.cos(lat2)
x = math.cos(lat1) * math.sin(lat2) - math.sin(lat1) * math.cos(lat2) * math.cos(lon2 - lon1)
bearing = math.atan2(y, x)
# Convert to degrees
bearing = math.degrees(bearing)
bearing = (bearing + 360) % 360
return bearing
def calculate_perpendicular_bearing(bearing):
"""Calculate perpendicular bearings (90 degrees to left and right)."""
left_bearing = (bearing - 90) % 360
right_bearing = (bearing + 90) % 360
return left_bearing, right_bearing
def calculate_new_point(lat, lon, bearing, distance):
"""Calculate new GPS coordinates given a starting point, bearing (in degrees), and distance (in meters)."""
R = 6371000 # Earth radius in meters
# Convert to radians
lat1 = math.radians(lat)
lon1 = math.radians(lon)
bearing = math.radians(bearing)
# Calculate angular distance
angular_distance = distance / R
# Calculate new latitude
lat2 = math.asin(math.sin(lat1) * math.cos(angular_distance) +
math.cos(lat1) * math.sin(angular_distance) * math.cos(bearing))
# Calculate new longitude
lon2 = lon1 + math.atan2(math.sin(bearing) * math.sin(angular_distance) * math.cos(lat1),
math.cos(angular_distance) - math.sin(lat1) * math.sin(lat2))
# Convert back to degrees
lat2 = math.degrees(lat2)
lon2 = math.degrees(lon2)
return lat2, lon2
def parse_gpx_route(gpx_file):
"""Parse the input GPX file and extract route points."""
try:
tree = ET.parse(gpx_file)
root = tree.getroot()
waypoints = []
# Try to find trackpoints
for trkpt in root.findall('.//gpx:trkpt', NAMESPACES):
lat = float(trkpt.get('lat'))
lon = float(trkpt.get('lon'))
waypoints.append((lat, lon))
# If no trackpoints found, try routepoints
if not waypoints:
for rtept in root.findall('.//gpx:rtept', NAMESPACES):
lat = float(rtept.get('lat'))
lon = float(rtept.get('lon'))
waypoints.append((lat, lon))
return waypoints
except Exception as e:
print(f"Error parsing GPX file: {e}")
return []
def generate_buffer_zigzag(route_points, buffer_width, spacing):
"""Generate a zig-zag pattern to cover the buffer zone around the route."""
if len(route_points) < 2:
print("Error: Route must contain at least 2 points.")
return []
# Start with the original route
result_points = route_points.copy()
zigzag_points = []
# Calculate accumulated distance along the route
accumulated_distances = [0]
for i in range(1, len(route_points)):
prev_lat, prev_lon = route_points[i-1]
curr_lat, curr_lon = route_points[i]
segment_distance = haversine_distance(prev_lat, prev_lon, curr_lat, curr_lon)
accumulated_distances.append(accumulated_distances[-1] + segment_distance)
total_route_length = accumulated_distances[-1]
# Calculate the interval for perpendicular crossings
# Use buffer_width as a basis for the interval to ensure good coverage
interval = buffer_width
# Number of perpendicular crossings
num_crossings = max(3, int(total_route_length / interval))
# Create perpendicular lines at regular intervals
for i in range(num_crossings + 1):
# Calculate distance along the route for this crossing
target_distance = (i * total_route_length) / num_crossings
# Find the segment containing this distance
segment_idx = 0
while segment_idx < len(accumulated_distances) - 1 and accumulated_distances[segment_idx + 1] < target_distance:
segment_idx += 1
# If we're at the end of the route
if segment_idx >= len(route_points) - 1:
segment_idx = len(route_points) - 2
# Calculate interpolation factor within this segment
segment_start_dist = accumulated_distances[segment_idx]
segment_end_dist = accumulated_distances[segment_idx + 1]
segment_length = segment_end_dist - segment_start_dist
# Avoid division by zero for very short segments
if segment_length < 0.1:
interpolation_factor = 0
else:
interpolation_factor = (target_distance - segment_start_dist) / segment_length
# Interpolate to find the exact crossing point
start_lat, start_lon = route_points[segment_idx]
end_lat, end_lon = route_points[segment_idx + 1]
crossing_lat = start_lat + interpolation_factor * (end_lat - start_lat)
crossing_lon = start_lon + interpolation_factor * (end_lon - start_lon)
# Calculate bearing of the route at this point
bearing = calculate_bearing(start_lat, start_lon, end_lat, end_lon)
# Calculate perpendicular bearings
left_bearing, right_bearing = calculate_perpendicular_bearing(bearing)
# Create perpendicular line crossing the route at this point
crossing_points = []
# Center point (on the route)
crossing_points.append((crossing_lat, crossing_lon))
# Left side points (multiple points to ensure full coverage)
for dist in range(0, int(buffer_width) + 1, int(spacing)):
new_lat, new_lon = calculate_new_point(crossing_lat, crossing_lon, left_bearing, dist)
crossing_points.append((new_lat, new_lon))
# Right side points (multiple points to ensure full coverage)
for dist in range(0, int(buffer_width) + 1, int(spacing)):
new_lat, new_lon = calculate_new_point(crossing_lat, crossing_lon, right_bearing, dist)
crossing_points.append((new_lat, new_lon))
# Add this set of crossing points to our zigzag collection
zigzag_points.extend(crossing_points)
# Create a route back to the start using the zigzag points
# First, get the first and last points of the original route
start_point = route_points[0]
# Add a path back to start from the end of the zigzags
result_points.extend(zigzag_points)
result_points.append(start_point) # Close the loop
return result_points
def write_gpx(points, output_file):
"""Write the points to a GPX file."""
root = ET.Element('gpx')
root.set('version', '1.1')
root.set('creator', 'Route Buffer Coverage Generator')
root.set('xmlns', 'http://www.topografix.com/GPX/1/1')
# Add metadata
metadata = ET.SubElement(root, 'metadata')
name = ET.SubElement(metadata, 'name')
name.text = 'Route with Buffer Coverage'
time = ET.SubElement(metadata, 'time')
time.text = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
# Create track
trk = ET.SubElement(root, 'trk')
trk_name = ET.SubElement(trk, 'name')
trk_name.text = 'Combined Route'
trkseg = ET.SubElement(trk, 'trkseg')
# Add track points
for lat, lon in points:
trkpt = ET.SubElement(trkseg, 'trkpt')
trkpt.set('lat', str(lat))
trkpt.set('lon', str(lon))
# Write to file
tree = ET.ElementTree(root)
ET.indent(tree, space=" ") # Pretty print XML (Python 3.9+)
tree.write(output_file, encoding='utf-8', xml_declaration=True)
def main():
parser = argparse.ArgumentParser(description="Generate a return path with zig-zags to cover a buffer zone around an existing route.")
parser.add_argument("input_gpx", help="Path to the input GPX file containing the original route")
parser.add_argument("buffer_width", type=float, help="Width of the buffer zone to cover on each side of the route (in meters)")
parser.add_argument("-s", "--spacing", type=float, default=200.0, help="Spacing between points along perpendicular lines (default: 200)")
parser.add_argument("-i", "--interval", type=float, default=None, help="Distance between perpendicular crossings in meters (default: equal to buffer_width)")
parser.add_argument("output_gpx", help="Path to save the output GPX file")
args = parser.parse_args()
# Parse input GPX
route_points = parse_gpx_route(args.input_gpx)
if not route_points:
print("Error: No route points found in the input GPX file.")
return
print(f"Parsed {len(route_points)} points from input route.")
# Set interval if not specified
interval = args.interval if args.interval else args.buffer_width
# Generate buffer coverage
result_points = generate_buffer_zigzag(route_points, args.buffer_width, args.spacing)
# Write output GPX
write_gpx(result_points, args.output_gpx)
print(f"Generated route with buffer coverage ({len(result_points)} points).")
print(f"Buffer width: {args.buffer_width}m")
print(f"Crossing interval: {interval}m")
print(f"Output written to: {args.output_gpx}")
if __name__ == "__main__":
main()
For immediate assistance, please email our customer support: [email protected]