Skyquant-LOS

SkyQuant LOS

A native iOS app (SwiftUI + MapKit + Swift Charts, iOS 17+) for terrain line-of-sight. It is a client of the SkyQuant Weather server’s LOS API — the same engine behind the web app’s 📐 Line of Sight panel — and does no terrain math of its own beyond preparing the profile chart.

What it does

  1. Map | Satellite | Globe — the top control switches one MKMapView between a flat standard map, flat satellite imagery with road and place labels (to place a point on an exact building; switching between Map and Satellite keeps the camera where it is) and a hybrid map with realistic elevation, which iOS 17 draws as a 3-D globe when zoomed out (switching to Globe flies the camera out to 20,000 km over the current centre; back to Map or Satellite returns to a regional view). The choice is remembered.
  2. Tap to place the origin (the observer). A bottom sheet opens:
    • Origin altitude — feet, MSL or AGL (default 33 ft AGL);
    • LOS altitude — the target/aircraft altitude, feet, MSL or AGL (default 30,000 ft MSL; AGL follows the terrain);
    • Max range — km, clamped to 5–500 (default 350);
    • Earth model — True Earth (geometric horizon) or 4/3 Earth (standard radio refraction);
    • origin latitude/longitude (5 decimals, editable). Values persist across launches.
  3. Compute viewshed — POST /api/los/viewshed. A cold server can take several minutes (terrain tiles download first); the app waits up to 11 minutes and shows elapsed time with a Cancel button. The visible area is drawn in green with a dashed range ring; stats show ground elevation, observer MSL and the range swept. The viewshed colour is user-selectable in Settings → Viewshed color (picker, preview, Reset to default); it applies to the map at once, with no Save.
  4. Tap anywhere to check a path — POST /api/los for that target. The path is drawn along the great circle (blue = clear, red dashed = blocked) with a verdict and a terrain-profile chart; drag across the chart to read distance, terrain, LOS altitude and clearance. Tap again to move the target. Edit opens the parameter form over the drawn viewshed: Recompute viewshed reruns it for the same origin and replaces the result, Back to results returns without touching it (a tap on the map while editing moves the origin, which drops the viewshed). Clear drops the viewshed (keeps the origin); New origin starts over.

The app always runs in dark mode (UIUserInterfaceStyle = Dark in the generated Info.plist, plus .preferredColorScheme(.dark) at the root), so the map, sheets and chart ignore the system appearance.

App icon

The icon is the “LOS” wordmark with the O as a viewshed ring (lit wedge toward the target) and a sight line from the cyan origin to the amber target. Source: design/AppIcon.svg (1024×1024, Inter ExtraBold); the shipped PNG is SkyQuantLOS/Assets.xcassets/AppIcon.appiconset/AppIcon.png. Re-render the PNG from the SVG at 1024 px after any edit (an iOS icon must be opaque).

Server requirement

The app connects to the server address built into it (below) — nothing to type. Settings (gear) can override it with another server (written by Save; empty = the built-in server, and Use built-in server drops a stored override). Either way the server must be a SkyQuant Weather build whose /api/los and /api/los/viewshed accept obs_msl_m (absolute observer altitude) — needed for an MSL origin altitude. AGL origins work on any server with the LOS routes.

Use https: App Transport Security blocks plain http to a public host.

Wire contract (metres on the wire, feet in the UI)

Request Fields
POST /api/los/viewshed obs_lat, obs_lon, ONE of obs_agl_m / obs_msl_m, ONE of tgt_agl_m / tgt_msl_m, max_range_km, az_step: 1, range_step_m: 250
POST /api/los the same obs_* / tgt_* choice, tgt_lat, tgt_lon, obs_terrain_m (from the viewshed, so both share one observer ground), n_samples: 500

Errors come back as { "error": "…" } and are shown in a red banner, as are HTTP failures, timeouts and malformed JSON.

Opening and running

cp Config/Local.xcconfig.example Config/Local.xcconfig   # then edit the URL
open SkyQuantLOS.xcodeproj

Config/Local.xcconfig is where your server’s address goes; it is gitignored and is the only place in the checkout that may contain it. The committed Config/Server.xcconfig (the app target’s base configuration) sets SKYQUANT_BASE_URL empty and #include?s the local file; Config/Info.plist bakes the value into the built app’s Info.plist as SkyQuantBaseURL, so the app connects on launch with nothing to type.

Mind the xcconfig comment trap: // starts a comment anywhere on a line, so write the URL as https:/$()/your-server.example ($() expands to nothing), or give a bare host (your-server.example — the app adds https://). Config/Local.xcconfig.example explains it.

Built without Local.xcconfig, the app has no built-in address and opens Settings on launch to ask for one. In either build, a URL typed in Settings is an override and wins while it is stored; a broken override is reported, never silently replaced by the built-in. The server hostname is never committed to this repository — only the gitignored Config/Local.xcconfig and, for an override, the device’s UserDefaults hold one. Config/Local.xcconfig must carry the production address before archiving — a Release build made without it ships an app with no address that asks the user to type one.

Xcode 16 or newer (the project uses file-system-synchronized folders, project format 77). To run on a device put your Apple Developer Team ID in the same Config/Local.xcconfig (DEVELOPMENT_TEAM = ABCDE12345 — the project file says $(inherited) for every target, so do NOT pick a team under Signing & Capabilities: Xcode would write it into project.pbxproj and block the next git pull), then Run. ⌘U runs the unit tests (SkyQuantLOSTests: request bodies, response decoding, viewshed geometry, profile math, viewshed colour, server resolution).

The project file was written by hand. If it ever misbehaves, regenerate it from the equivalent XcodeGen spec:

brew install xcodegen
xcodegen generate

Layout

SkyQuantLOS/
  SkyQuantLOSApp.swift
  Models/       AltitudeReference, Units, LOSRequests, LOSResponses, ProfileMath,
                ViewshedStyle
  Geo/          ViewshedGeometry (GeoJSON → MKPolygon, ring, geodesic path)
  Networking/   APIClient (+ LOSError, AppSettings)
  State/        LOSSession (the app's state machine)
  Views/        ContentView, LOSMapView, SetupSheet, ResultPanel,
                ProfileChartView, SettingsView
SkyQuantLOSTests/
Config/         Server.xcconfig (committed, empty default), Info.plist
                (SkyQuantBaseURL), Local.xcconfig.example; Local.xcconfig is
                gitignored and holds the real address
design/         AppIcon.svg