import SwiftUI import MapKit import UIKit /// Renders a sharable card with map snapshot + stats for a route. @MainActor enum RouteShareImage { static func render(detail: RouteDetail, size: CGFloat = 1200) async -> UIImage? { let mapSize = CGSize(width: size, height: size * 0.75) guard let mapImage = await mapSnapshot(track: detail.gpsTrack, size: mapSize) else { return nil } let card = ShareCard(detail: detail, mapImage: mapImage, width: size) let renderer = ImageRenderer(content: card) renderer.scale = 2 return renderer.uiImage } private static func mapSnapshot(track: [GPSPoint], size: CGSize) async -> UIImage? { guard track.count >= 2 else { return nil } let coords = track.map { CLLocationCoordinate2D(latitude: $0.lat, longitude: $0.lon) } let options = MKMapSnapshotter.Options() options.size = size options.pointOfInterestFilter = .excludingAll let lats = track.map(\.lat) let lons = track.map(\.lon) options.region = MKCoordinateRegion( center: CLLocationCoordinate2D( latitude: (lats.min()! + lats.max()!) / 2, longitude: (lons.min()! + lons.max()!) / 2 ), span: MKCoordinateSpan( latitudeDelta: max((lats.max()! - lats.min()!) * 1.4, 0.002), longitudeDelta: max((lons.max()! - lons.min()!) * 1.4, 0.002) ) ) do { let snapshot = try await MKMapSnapshotter(options: options).start() let uiRenderer = UIGraphicsImageRenderer(size: size) return uiRenderer.image { ctx in snapshot.image.draw(in: CGRect(origin: .zero, size: size)) ctx.cgContext.setLineWidth(6) ctx.cgContext.setStrokeColor(UIColor(red: 0xC4 / 255.0, green: 0x84 / 255.0, blue: 0x3A / 255.0, alpha: 1).cgColor) ctx.cgContext.setLineJoin(.round) ctx.cgContext.setLineCap(.round) var first = true for coord in coords { let p = snapshot.point(for: coord) if first { ctx.cgContext.move(to: p); first = false } else { ctx.cgContext.addLine(to: p) } } ctx.cgContext.strokePath() } } catch { return nil } } } private struct ShareCard: View { let detail: RouteDetail let mapImage: UIImage let width: CGFloat var body: some View { VStack(spacing: 0) { Image(uiImage: mapImage) .resizable() .scaledToFit() VStack(alignment: .leading, spacing: 16) { Text(detail.name) .font(.system(size: 36, weight: .bold)) .lineLimit(2) HStack(spacing: 28) { stat(value: String(format: "%.2f km", detail.distanzKm ?? 0), label: "Distanz") stat(value: durationLabel, label: "Dauer") stat(value: "\(detail.gpsTrack.count)", label: "Punkte") } HStack { Image(systemName: "pawprint.fill") .foregroundStyle(Color(red: 0xC4 / 255.0, green: 0x84 / 255.0, blue: 0x3A / 255.0)) Text("Ban Yaro Go") .font(.headline) Spacer() } } .padding(28) .background(Color.white) } .frame(width: width) .background(Color.white) } private func stat(value: String, label: String) -> some View { VStack(alignment: .leading, spacing: 4) { Text(value).font(.system(size: 28, weight: .semibold)) Text(label).font(.system(size: 16)).foregroundStyle(.secondary) } } private var durationLabel: String { let mins = detail.dauerMin ?? 0 if mins >= 60 { return "\(mins / 60) h \(mins % 60) min" } return "\(mins) min" } }