SPB Git forge

spb/ka-ios

Public
20commits 1branches 0releases
17.2 MBsize
maindefault branch
28 days agolast push
Swift 100%
10.2 KB · 240 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// MapboxAPI.swift — les services Mapbox de Ka Trajet : Geocoding (recherche3// de lieux, autocomplétion, géocodage inverse) et Directions (itinéraires4// auto/vélo/marche avec alternatives et étapes turn-by-turn).5// Jeton PUBLIC Mapbox (pk.…) partagé avec Lou-Ka (kamaps/config.ts) — conçu6// pour être exposé côté client ; restrictions gérées au tableau de bord Mapbox.7import Foundation8import CoreLocation910enum MapboxConfig {11    static let token = "pk.eyJ1Ijoic3Bib3VjaGVyIiwiYSI6ImNtc3Fyb3k4djAwOTgyenB3dWt6NHBjc2kifQ.poqLf0ADy3lIh28O-pFI2Q"12}1314// MARK: - Modèles1516struct Place: Identifiable, Equatable {17    let id: String18    let name: String        // ex. « Château Frontenac »19    let address: String     // ex. « 1 rue des Carrières, Québec »20    let coordinate: CLLocationCoordinate2D2122    static func == (l: Place, r: Place) -> Bool { l.id == r.id }23}2425enum TransportMode: String, CaseIterable, Identifiable {26    case auto, velo, marche27    var id: String { rawValue }28    var profile: String {29        switch self {30        case .auto: return "driving-traffic"31        case .velo: return "cycling"32        case .marche: return "walking"33        }34    }35    var icon: String {36        switch self {37        case .auto: return "car.fill"38        case .velo: return "bicycle"39        case .marche: return "figure.walk"40        }41    }42    var label: String {43        switch self {44        case .auto: return "Auto"45        case .velo: return "Vélo"46        case .marche: return "Marche"47        }48    }49}5051struct RouteStep: Identifiable {52    let id = UUID()53    let instruction: String54    let distance: Double     // mètres55    let icon: String         // symbole SF selon la manœuvre56    let coordinate: CLLocationCoordinate2D   // où se fait la manœuvre57}5859struct Route: Identifiable {60    let id = UUID()61    let coordinates: [CLLocationCoordinate2D]62    let distance: Double     // mètres63    let duration: Double     // secondes64    let steps: [RouteStep]6566    var durationText: String {67        let m = Int((duration / 60).rounded())68        return m >= 60 ? "\(m / 60) h \(m % 60 == 0 ? "" : "\(m % 60) min")".trimmingCharacters(in: .whitespaces)69                       : "\(max(m, 1)) min"70    }71    var distanceText: String { Route.format(meters: distance) }7273    static func format(meters: Double) -> String {74        meters >= 1000 ? String(format: "%.1f km", meters / 1000) : "\(Int(meters.rounded())) m"75    }76}7778// MARK: - Client HTTP7980enum MapboxAPI {81    // Recherche de lieux (autocomplétion), en français, biaisée vers la82    // position de l'utilisateur quand elle est connue.83    static func search(_ query: String, near: CLLocationCoordinate2D?) async throws -> [Place] {84        let q = query.trimmingCharacters(in: .whitespacesAndNewlines)85        guard !q.isEmpty else { return [] }86        var comps = URLComponents(string: "https://api.mapbox.com/geocoding/v5/mapbox.places/\(q.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? q).json")!87        var items = [88            URLQueryItem(name: "access_token", value: MapboxConfig.token),89            URLQueryItem(name: "language", value: "fr"),90            URLQueryItem(name: "limit", value: "7"),91            URLQueryItem(name: "autocomplete", value: "true"),92        ]93        if let near {94            items.append(URLQueryItem(name: "proximity", value: "\(near.longitude),\(near.latitude)"))95        }96        comps.queryItems = items97        let (data, _) = try await URLSession.shared.data(from: comps.url!)98        let decoded = try JSONDecoder().decode(GeocodeResponse.self, from: data)99        return decoded.features.map { f in100            Place(id: f.id,101                  name: f.text ?? f.place_name,102                  address: f.place_name,103                  coordinate: .init(latitude: f.center[1], longitude: f.center[0]))104        }105    }106107    // Géocodage inverse : nomme un point déposé sur la carte.108    static func reverse(_ c: CLLocationCoordinate2D) async -> Place {109        let fallback = Place(id: "pin-\(c.latitude)-\(c.longitude)",110                             name: "Repère sur la carte",111                             address: String(format: "%.5f, %.5f", c.latitude, c.longitude),112                             coordinate: c)113        var comps = URLComponents(string: "https://api.mapbox.com/geocoding/v5/mapbox.places/\(c.longitude),\(c.latitude).json")!114        comps.queryItems = [115            URLQueryItem(name: "access_token", value: MapboxConfig.token),116            URLQueryItem(name: "language", value: "fr"),117            URLQueryItem(name: "limit", value: "1"),118        ]119        guard let (data, _) = try? await URLSession.shared.data(from: comps.url!),120              let decoded = try? JSONDecoder().decode(GeocodeResponse.self, from: data),121              let f = decoded.features.first else { return fallback }122        return Place(id: fallback.id, name: f.text ?? f.place_name, address: f.place_name, coordinate: c)123    }124125    // Ville au point donné (pour les APIs Ka filtrées par ville).126    static func cityName(at c: CLLocationCoordinate2D) async -> String? {127        var comps = URLComponents(string: "https://api.mapbox.com/geocoding/v5/mapbox.places/\(c.longitude),\(c.latitude).json")!128        comps.queryItems = [129            URLQueryItem(name: "access_token", value: MapboxConfig.token),130            URLQueryItem(name: "types", value: "place"),131            URLQueryItem(name: "language", value: "fr"),132            URLQueryItem(name: "limit", value: "1"),133        ]134        guard let (data, _) = try? await URLSession.shared.data(from: comps.url!),135              let decoded = try? JSONDecoder().decode(GeocodeResponse.self, from: data)136        else { return nil }137        return decoded.features.first?.text138    }139140    // Coordonnées d'une ville québécoise (géocodage direct, pour les sources141    // Ka sans lat/lng comme les boutiques Fabri-Ka).142    static func geocodeCity(_ name: String) async -> CLLocationCoordinate2D? {143        let q = "\(name), Québec, Canada"144        var comps = URLComponents(string: "https://api.mapbox.com/geocoding/v5/mapbox.places/\(q.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? q).json")!145        comps.queryItems = [146            URLQueryItem(name: "access_token", value: MapboxConfig.token),147            URLQueryItem(name: "types", value: "place,locality,neighborhood"),148            URLQueryItem(name: "language", value: "fr"),149            URLQueryItem(name: "limit", value: "1"),150        ]151        guard let (data, _) = try? await URLSession.shared.data(from: comps.url!),152              let decoded = try? JSONDecoder().decode(GeocodeResponse.self, from: data),153              let f = decoded.features.first, f.center.count >= 2 else { return nil }154        return .init(latitude: f.center[1], longitude: f.center[0])155    }156157    // Itinéraires avec alternatives + étapes, en français.158    static func directions(from: CLLocationCoordinate2D, to: CLLocationCoordinate2D,159                           mode: TransportMode) async throws -> [Route] {160        let path = "\(from.longitude),\(from.latitude);\(to.longitude),\(to.latitude)"161        var comps = URLComponents(string: "https://api.mapbox.com/directions/v5/mapbox/\(mode.profile)/\(path)")!162        comps.queryItems = [163            URLQueryItem(name: "access_token", value: MapboxConfig.token),164            URLQueryItem(name: "alternatives", value: "true"),165            URLQueryItem(name: "geometries", value: "geojson"),166            URLQueryItem(name: "overview", value: "full"),167            URLQueryItem(name: "steps", value: "true"),168            URLQueryItem(name: "language", value: "fr"),169        ]170        let (data, _) = try await URLSession.shared.data(from: comps.url!)171        let decoded = try JSONDecoder().decode(DirectionsResponse.self, from: data)172        guard decoded.code == "Ok" else { throw MapboxError.api(decoded.message ?? decoded.code) }173        return decoded.routes.map { r in174            Route(coordinates: r.geometry.coordinates.map { .init(latitude: $0[1], longitude: $0[0]) },175                  distance: r.distance,176                  duration: r.duration,177                  steps: r.legs.flatMap(\.steps).map { s in178                      RouteStep(instruction: s.maneuver.instruction,179                                distance: s.distance,180                                icon: Self.icon(for: s.maneuver),181                                coordinate: .init(latitude: s.maneuver.location[1],182                                                  longitude: s.maneuver.location[0]))183                  })184        }185    }186187    private static func icon(for m: DirectionsResponse.Maneuver) -> String {188        switch m.type {189        case "depart": return "location.fill"190        case "arrive": return "mappin.circle.fill"191        case "roundabout", "rotary": return "arrow.triangle.2.circlepath"192        case "merge": return "arrow.triangle.merge"193        case "on ramp", "off ramp": return "arrow.up.right"194        default:195            if let mod = m.modifier {196                if mod.contains("left") { return "arrow.turn.up.left" }197                if mod.contains("right") { return "arrow.turn.up.right" }198                if mod.contains("uturn") { return "arrow.uturn.down" }199            }200            return "arrow.up"201        }202    }203}204205enum MapboxError: LocalizedError {206    case api(String)207    var errorDescription: String? {208        if case .api(let m) = self { return "Mapbox : \(m)" }209        return nil210    }211}212213// MARK: - DTO214215private struct GeocodeResponse: Decodable {216    struct Feature: Decodable {217        let id: String218        let text: String?219        let place_name: String220        let center: [Double]221    }222    let features: [Feature]223}224225private struct DirectionsResponse: Decodable {226    struct Geometry: Decodable { let coordinates: [[Double]] }227    struct Maneuver: Decodable { let type: String; let modifier: String?; let instruction: String; let location: [Double] }228    struct Step: Decodable { let distance: Double; let maneuver: Maneuver }229    struct Leg: Decodable { let steps: [Step] }230    struct RouteDTO: Decodable {231        let geometry: Geometry232        let distance: Double233        let duration: Double234        let legs: [Leg]235    }236    let code: String237    let message: String?238    let routes: [RouteDTO]239}240