GPX-Import via Teilen-Menü und 'Öffnen mit'
Andere Apps können jetzt GPX-Tracks zu Ban Yaro Go schicken (Komoot,
Outdooractive, GPSies, AllTrails, Files-App, Mail-Anhänge, AirDrop).
- Info.plist:
- UTImportedTypeDeclarations: com.topografix.gpx (conforms to
public.xml/data/content, ext gpx, MIME application/gpx+xml)
- CFBundleDocumentTypes registriert die UTI als Viewer (LSHandlerRank
Alternate, damit wir nicht die Default-App werden)
- LSSupportsOpeningDocumentsInPlace=true
- Support/GPXParser.swift: schlanker XMLParser/SAX-Reader für
<trkpt>/<wpt>/<rtept>, Track-Name aus <trk><name>, ele + ISO8601 time
- Views/GPXImportSheet.swift: Sheet mit Map(MapPolyline)+Start/Ziel-Pins,
Distanz/Punkte/Dauer-Karte, zwei Aktionen:
1. 'Als Tour übernehmen' — Name editierbar, Hunde-Picker (FlowDogs),
öffentlich-Toggle → POST /api/routes
2. 'Nur ansehen' — Startpunkt in Apple Maps
- BanYaroGoApp.swift: .onOpenURL prüft .gpx, security-scoped resource,
parst und triggert das Sheet via TrackBox-Wrapper
This commit is contained in:
parent
3d229d42ce
commit
7848817cbe
4 changed files with 470 additions and 0 deletions
138
BanYaroGo/Support/GPXParser.swift
Normal file
138
BanYaroGo/Support/GPXParser.swift
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import Foundation
|
||||
import CoreLocation
|
||||
|
||||
struct GPXTrack {
|
||||
let name: String?
|
||||
let points: [GPXPoint]
|
||||
|
||||
var coordinates: [CLLocationCoordinate2D] {
|
||||
points.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lon) }
|
||||
}
|
||||
|
||||
/// Total length in meters along the track using great-circle distance.
|
||||
var distanceMeters: Double {
|
||||
guard points.count > 1 else { return 0 }
|
||||
var sum: Double = 0
|
||||
for i in 1..<points.count {
|
||||
let a = CLLocation(latitude: points[i-1].lat, longitude: points[i-1].lon)
|
||||
let b = CLLocation(latitude: points[i].lat, longitude: points[i].lon)
|
||||
sum += b.distance(from: a)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
/// Duration in seconds derived from first/last <time> if available.
|
||||
var durationSeconds: TimeInterval? {
|
||||
guard let first = points.first?.time, let last = points.last?.time else { return nil }
|
||||
let delta = last.timeIntervalSince(first)
|
||||
return delta > 0 ? delta : nil
|
||||
}
|
||||
}
|
||||
|
||||
struct GPXPoint {
|
||||
let lat: Double
|
||||
let lon: Double
|
||||
let ele: Double?
|
||||
let time: Date?
|
||||
}
|
||||
|
||||
enum GPXParseError: Error, LocalizedError {
|
||||
case noTrack
|
||||
case invalid(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .noTrack: return "Keine Trackpunkte gefunden."
|
||||
case .invalid(let why): return "GPX ungültig: \(why)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Minimaler SAX-Parser für GPX. Liest <trkpt>, <wpt> und <rtept> als Punkte;
|
||||
/// Track-Name aus <name> innerhalb des ersten <trk>.
|
||||
final class GPXParser: NSObject, XMLParserDelegate {
|
||||
private var points: [GPXPoint] = []
|
||||
private var trackName: String?
|
||||
|
||||
private var currentLat: Double?
|
||||
private var currentLon: Double?
|
||||
private var currentEle: Double?
|
||||
private var currentTime: Date?
|
||||
private var currentElement: String?
|
||||
private var inTrk = false
|
||||
private var trkNameCaptured = false
|
||||
private var textBuffer = ""
|
||||
|
||||
private static let isoFormatter: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
||||
return f
|
||||
}()
|
||||
private static let isoFormatterNoFraction: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
return f
|
||||
}()
|
||||
|
||||
static func parse(data: Data) throws -> GPXTrack {
|
||||
let parser = GPXParser()
|
||||
let xml = XMLParser(data: data)
|
||||
xml.delegate = parser
|
||||
if !xml.parse() {
|
||||
if let err = xml.parserError {
|
||||
throw GPXParseError.invalid(err.localizedDescription)
|
||||
}
|
||||
}
|
||||
guard !parser.points.isEmpty else { throw GPXParseError.noTrack }
|
||||
return GPXTrack(name: parser.trackName, points: parser.points)
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, didStartElement elementName: String,
|
||||
namespaceURI: String?, qualifiedName qName: String?,
|
||||
attributes attributeDict: [String : String] = [:]) {
|
||||
currentElement = elementName
|
||||
textBuffer = ""
|
||||
switch elementName {
|
||||
case "trk":
|
||||
inTrk = true
|
||||
trkNameCaptured = false
|
||||
case "trkpt", "wpt", "rtept":
|
||||
currentLat = attributeDict["lat"].flatMap { Double($0) }
|
||||
currentLon = attributeDict["lon"].flatMap { Double($0) }
|
||||
currentEle = nil
|
||||
currentTime = nil
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, foundCharacters string: String) {
|
||||
textBuffer += string
|
||||
}
|
||||
|
||||
func parser(_ parser: XMLParser, didEndElement elementName: String,
|
||||
namespaceURI: String?, qualifiedName qName: String?) {
|
||||
let text = textBuffer.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
switch elementName {
|
||||
case "ele":
|
||||
currentEle = Double(text)
|
||||
case "time":
|
||||
currentTime = Self.isoFormatter.date(from: text)
|
||||
?? Self.isoFormatterNoFraction.date(from: text)
|
||||
case "name":
|
||||
if inTrk && !trkNameCaptured {
|
||||
trackName = text
|
||||
trkNameCaptured = true
|
||||
}
|
||||
case "trkpt", "wpt", "rtept":
|
||||
if let lat = currentLat, let lon = currentLon {
|
||||
points.append(GPXPoint(lat: lat, lon: lon, ele: currentEle, time: currentTime))
|
||||
}
|
||||
case "trk":
|
||||
inTrk = false
|
||||
default:
|
||||
break
|
||||
}
|
||||
textBuffer = ""
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue