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.. 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 , und als Punkte; /// Track-Name aus innerhalb des ersten . 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 = "" } }