168 lines
No EOL
5.8 KiB
Python
168 lines
No EOL
5.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
GPX to iOS Simulator simctl location command
|
|
|
|
Converts a GPX file to simctl location start command for realistic iOS location simulation.
|
|
|
|
Tested with CoMaps exported tracks
|
|
|
|
Usage:
|
|
python gpx_to_simctl.py test_route.gpx
|
|
"""
|
|
|
|
import argparse
|
|
import xml.etree.ElementTree as ET
|
|
from pathlib import Path
|
|
import sys
|
|
import subprocess
|
|
|
|
def extract_track_points_from_gpx(gpx_file: Path):
|
|
"""Extract track points from GPX file."""
|
|
tree = ET.parse(gpx_file)
|
|
root = tree.getroot()
|
|
|
|
points = []
|
|
# Find all elements with lat/lon attributes
|
|
for elem in root.findall('.//*[@lat][@lon]'):
|
|
lat = float(elem.get('lat'))
|
|
lon = float(elem.get('lon'))
|
|
points.append((lat, lon))
|
|
|
|
return points
|
|
|
|
def generate_simctl_command(points, speed_kmh=60, interval=0.1, distance=None, device="booted"):
|
|
"""Generate simctl location start command."""
|
|
if len(points) < 2:
|
|
raise ValueError("Need at least 2 waypoints for simctl location start")
|
|
|
|
# Convert km/h to m/s
|
|
speed_mps = speed_kmh / 3.6
|
|
|
|
# Format waypoints as lat,lon pairs
|
|
waypoint_strings = [f"{lat:.6f},{lon:.6f}" for lat, lon in points]
|
|
|
|
# Build command
|
|
cmd = ["xcrun", "simctl", "location", device, "start"]
|
|
cmd.append(f"--speed={speed_mps:.2f}")
|
|
|
|
if distance:
|
|
cmd.append(f"--distance={distance}")
|
|
else:
|
|
cmd.append(f"--interval={interval}")
|
|
|
|
cmd.extend(waypoint_strings)
|
|
|
|
return cmd
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert GPX file to simctl location start command",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python gpx_to_simctl.py test_route.gpx --speed 60 --interval 0.1
|
|
python gpx_to_simctl.py test_route.gpx --speed 80 --distance 10 --clear-first
|
|
python gpx_to_simctl.py test_route.gpx --speed 50 --dry-run
|
|
"""
|
|
)
|
|
|
|
parser.add_argument('gpx_file', help='Input GPX file')
|
|
parser.add_argument('--speed', type=float, default=60,
|
|
help='Speed in km/h (default: 60)')
|
|
parser.add_argument('--interval', type=float, default=0.1,
|
|
help='Update interval in seconds (default: 0.1)')
|
|
parser.add_argument('--distance', type=float,
|
|
help='Update distance in meters (overrides --interval)')
|
|
parser.add_argument('--device', default='booted',
|
|
help='Target device (default: booted)')
|
|
parser.add_argument('--dry-run', action='store_true',
|
|
help='Show command without executing (default: execute)')
|
|
parser.add_argument('--clear-first', action='store_true',
|
|
help='Clear existing location before starting')
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate input file
|
|
gpx_file = Path(args.gpx_file)
|
|
if not gpx_file.exists():
|
|
print(f"Error: GPX file '{gpx_file}' not found", file=sys.stderr)
|
|
return 1
|
|
|
|
try:
|
|
# Extract waypoints
|
|
points = extract_track_points_from_gpx(gpx_file)
|
|
print(f"Extracted {len(points)} waypoints from {gpx_file}")
|
|
|
|
if len(points) < 2:
|
|
print("Error: Need at least 2 waypoints for location simulation", file=sys.stderr)
|
|
return 1
|
|
|
|
# Generate command
|
|
cmd = generate_simctl_command(
|
|
points,
|
|
speed_kmh=args.speed,
|
|
interval=args.interval,
|
|
distance=args.distance,
|
|
device=args.device
|
|
)
|
|
|
|
# Show command
|
|
print(f"\nGenerated simctl command:")
|
|
print(" ".join(cmd))
|
|
|
|
# Calculate simulation info
|
|
speed_mps = args.speed / 3.6
|
|
total_distance = 0
|
|
for i in range(1, len(points)):
|
|
lat1, lon1 = points[i-1]
|
|
lat2, lon2 = points[i]
|
|
# Simple distance approximation
|
|
total_distance += ((lat2-lat1)**2 + (lon2-lon1)**2)**0.5 * 111000 # rough conversion to meters
|
|
|
|
duration = total_distance / speed_mps
|
|
print(f"\nSimulation info:")
|
|
print(f" Speed: {args.speed} km/h ({speed_mps:.1f} m/s)")
|
|
print(f" Waypoints: {len(points)}")
|
|
print(f" Estimated distance: {total_distance/1000:.2f} km")
|
|
print(f" Estimated duration: {duration:.0f} seconds ({duration/60:.1f} minutes)")
|
|
if args.distance:
|
|
print(f" Update distance: {args.distance}m")
|
|
else:
|
|
print(f" Update interval: {args.interval}s")
|
|
|
|
# Execute by default unless dry-run
|
|
if args.dry_run:
|
|
print(f"\n[DRY RUN] Command that would be executed:")
|
|
print(f" {' '.join(cmd)}")
|
|
if args.clear_first:
|
|
clear_cmd = ["xcrun", "simctl", "location", args.device, "clear"]
|
|
print(f" (would clear location first: {' '.join(clear_cmd)})")
|
|
else:
|
|
print(f"\nExecuting command...")
|
|
|
|
# Clear location first if requested
|
|
if args.clear_first:
|
|
clear_cmd = ["xcrun", "simctl", "location", args.device, "clear"]
|
|
print("Clearing existing location...")
|
|
subprocess.run(clear_cmd, check=True)
|
|
|
|
# Execute the start command
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
|
|
if result.returncode == 0:
|
|
print("✅ Location simulation started successfully!")
|
|
if result.stdout.strip():
|
|
print(result.stdout.strip())
|
|
else:
|
|
print(f"❌ Error executing command:")
|
|
print(result.stderr.strip())
|
|
return 1
|
|
|
|
return 0
|
|
|
|
except Exception as e:
|
|
print(f"Error: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main()) |