Jul 21 - Update to support photos' timestamp/location
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// Utils/PhotoCapture.swift
|
||||
// ------------------------
|
||||
// Capture-time metadata for evidence photos.
|
||||
//
|
||||
// The server burns a timestamp + GPS overlay into every photo uploaded through
|
||||
// POST /api/v1/photos/upload (see photo_stamp.py in the Flask app). It resolves
|
||||
// that metadata from the client form fields first, then the image's own EXIF,
|
||||
// then — last resort — server receipt time.
|
||||
//
|
||||
// EXIF is NOT a usable fallback for this app: savePhotoToDisk() re-encodes each
|
||||
// UIImage via jpegData(compressionQuality:), which strips every EXIF tag. So
|
||||
// these client fields are the ONLY source of true capture time and location.
|
||||
// Without them, a photo taken offline at 09:14 and synced at 16:00 is stamped
|
||||
// 16:00 — the wrong time, on evidence.
|
||||
//
|
||||
// Hence the flow: record the moment + fix AT CAPTURE, carry them through
|
||||
// CapturedPhoto -> PendingPhoto -> the upload request.
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import CoreLocation
|
||||
|
||||
// MARK: - CapturedPhoto
|
||||
|
||||
/// A photo the user just took or picked, together with the metadata recorded
|
||||
/// at that instant. Property names match the tuple this replaced
|
||||
/// (`image`, `path`), so existing call sites keep compiling.
|
||||
struct CapturedPhoto {
|
||||
|
||||
let image: UIImage
|
||||
let path: String
|
||||
let capturedAt: Date
|
||||
let latitude: Double?
|
||||
let longitude: Double?
|
||||
|
||||
/// Stamps "now" plus the freshest GPS fix available at the moment of capture.
|
||||
init(image: UIImage, path: String) {
|
||||
let fix = PhotoLocationProvider.shared.lastLocation
|
||||
self.image = image
|
||||
self.path = path
|
||||
self.capturedAt = Date()
|
||||
self.latitude = fix?.coordinate.latitude
|
||||
self.longitude = fix?.coordinate.longitude
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - PhotoLocationProvider
|
||||
|
||||
/// Shared, always-warm location source for photo capture.
|
||||
///
|
||||
/// One long-lived CLLocationManager: views call `start()` in `onAppear` so a
|
||||
/// fix already exists the instant the shutter fires. Kept separate from
|
||||
/// ExecuteInspectionView's `InspectionLocationManager` (phase 25 submit GPS),
|
||||
/// which is per-view and would start cold on every photo screen.
|
||||
final class PhotoLocationProvider: NSObject, CLLocationManagerDelegate {
|
||||
|
||||
static let shared = PhotoLocationProvider()
|
||||
|
||||
private let manager = CLLocationManager()
|
||||
|
||||
/// Most recent fix, or nil if unavailable/denied.
|
||||
private(set) var lastLocation: CLLocation?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters
|
||||
manager.distanceFilter = 10
|
||||
}
|
||||
|
||||
/// Request permission if needed and begin updating. Safe to call repeatedly.
|
||||
/// Requires NSLocationWhenInUseUsageDescription in the target's Info settings.
|
||||
func start() {
|
||||
switch manager.authorizationStatus {
|
||||
case .notDetermined:
|
||||
manager.requestWhenInUseAuthorization()
|
||||
// locationManagerDidChangeAuthorization starts updates once granted.
|
||||
case .authorizedWhenInUse, .authorizedAlways:
|
||||
manager.startUpdatingLocation()
|
||||
default:
|
||||
break // denied/restricted — lat/lng stay nil; the timestamp is still stamped
|
||||
}
|
||||
}
|
||||
|
||||
func stop() {
|
||||
manager.stopUpdatingLocation()
|
||||
}
|
||||
|
||||
// CLLocationManagerDelegate
|
||||
|
||||
func locationManager(_ m: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
if let loc = locations.last { lastLocation = loc }
|
||||
}
|
||||
|
||||
func locationManager(_ m: CLLocationManager, didFailWithError error: Error) {
|
||||
// Non-fatal — photos still upload, just without coordinates.
|
||||
print("[JQC] Photo location fix failed: \(error.localizedDescription)")
|
||||
}
|
||||
|
||||
func locationManagerDidChangeAuthorization(_ m: CLLocationManager) {
|
||||
if m.authorizationStatus == .authorizedWhenInUse ||
|
||||
m.authorizationStatus == .authorizedAlways {
|
||||
manager.startUpdatingLocation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Wire format
|
||||
|
||||
enum PhotoCaptureFormat {
|
||||
|
||||
/// ISO-8601 with an explicit offset — the format the server's
|
||||
/// `_parse_client_datetime()` expects. A naive string with no offset would
|
||||
/// be read as Eastern wall time, so the offset must always be present.
|
||||
static let iso8601: ISO8601DateFormatter = {
|
||||
let f = ISO8601DateFormatter()
|
||||
f.formatOptions = [.withInternetDateTime]
|
||||
return f
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user