// // RealtimeAlertViewController.swift // Earthquake Network // // Created by Andrea Busi on 15/06/22. // Copyright © 2022 Earthquake Network. All rights reserved. // import UIKit import MapKit class RealtimeAlertViewController: UIViewController, MKMapViewDelegate { @objc var onClose: () -> Void = {} // MARK: - Internal typealias NotificationPayload = [String: Any] private let notificationView = RealtimeAlertView() // complete push payload private let notification: NotificationPayload // aps.alert field of given push notification private let alert: NotificationPayload /// Coordinate of the earthquake private let coordinate: CLLocation /// Calculated timestamp for earthquake on user position private let impactTimestamp: Date /// Timer to constantly update countdown label private var countdownTimer: Timer? /// Refresh time for wave animation private let waveAnimationRefreshRate = 0.1 /// Current radius of the wave animation on the map private var waveAnimationCurrentRadius: CLLocationDistance = 0 private var waveAnimationVelocity: Double = 1_000 /// Timer to simulate animation for the wave private var waveAnimationTimer: Timer? private var currentCountdown: Int { let now = Date() let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0)) return difference } // MARK: - Init @objc init?(notification: NotificationPayload) { guard let alert = Self.getPushAlertPayload(from: notification), let coordinate = Self.getCoordinate(from: notification), let impactTimestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: notification) else { return nil } self.notification = notification self.alert = alert self.coordinate = coordinate self.impactTimestamp = impactTimestamp super.init(nibName: nil, bundle: nil) self.waveAnimationCurrentRadius = currentWavePosition() self.waveAnimationVelocity = evaluateWaveVelocity() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: - View Lifecycle override func loadView() { view = notificationView } override func viewDidLoad() { super.viewDidLoad() configureUI() updateUI() startCountdown() startWaveAnimation() } // MARK: - Private private func configureUI() { notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside) } private func updateUI() { if let title = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first { notificationView.descriptionLabel.text = String(format: NSLocalizedString(title, comment: ""), arg) } // update title with distance from earthquake let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0 let distanceRound = Int(round(distance / 1_000)) notificationView.descriptionLabel.text = (notificationView.descriptionLabel.text ?? "") + ".\n" + String(format: NSLocalizedString("timer_message2_other", comment: ""), distanceRound) // center map on the earthquake coordinate let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5) let region = MKCoordinateRegion(center: coordinate.coordinate, span: span) notificationView.mapView.setCenter(coordinate.coordinate, animated: false) notificationView.mapView.setRegion(region, animated: true) // aggiungiamo annotation con epicentro sisma let intensity = notification.eqn_intValue(for: "intensity") ?? 0 notificationView.addMapAnnotation(center: coordinate.coordinate, intensity: intensity) // simuliamo animazione dell'onda sismica notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation") // aggiungiamo un segmento tra la posizione del sisma e quella dell'utente if let lastPosition = EQNUser.default().lastPosition { notificationView.addMapLine(coordinates: [coordinate.coordinate, lastPosition.coordinate]) } } private func startCountdown() { // show countdown only if time is less than 300 seconds if currentCountdown < 300 { // start a timer for the countdown label notificationView.waveTimeLabel.isHidden = false countdownTimer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(countdownTimerFired(_:)), userInfo: nil, repeats: true) countdownTimer?.fire() } } private func startWaveAnimation() { waveAnimationTimer = Timer.scheduledTimer(timeInterval: waveAnimationRefreshRate, target: self, selector: #selector(mapWaveAnimationFired(_:)), userInfo: nil, repeats: true) waveAnimationTimer?.fire() } // MARK: - Action @objc private func onTapClose(_ sender: UIButton) { onClose() dismiss(animated: true) } // MARK: - Timer @objc private func countdownTimerFired(_ sender: Timer) { notificationView.waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), currentCountdown) if currentCountdown <= 0 { // stop the countdown countdownTimer?.invalidate() countdownTimer = nil } } @objc private func mapWaveAnimationFired(_ sender: Timer) { waveAnimationCurrentRadius += waveAnimationVelocity notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation") } // MARK: - Helpers /// Retrieve coordinate of earthquake from the notification payload /// - Parameter notification: Notification payload /// - Returns: Coordinate if found, nil otherwise static func getCoordinate( from notification: NotificationPayload ) -> CLLocation? { guard let latitude = notification.eqn_doubleValue(for: "latitude"), let longitude = notification.eqn_doubleValue(for: "longitude") else { return nil } return CLLocation(latitude: latitude, longitude: longitude) } /// Get `aps.alert` object inside a given notification payload /// - Parameter notification: Notification payload /// - Returns: `aps.alert` object if found, nil otherwise static func getPushAlertPayload( from notification: NotificationPayload ) -> NotificationPayload? { guard let aps = notification["aps"] as? [String: Any], let alert = aps["alert"] as? [String: Any] else { return nil } return alert } /// Evaluate current position for the wave /// Used to define initial position for the wave circle /// - Returns: Distance of the wave from the original earthquake point private func currentWavePosition() -> Double { // velocità onda let waveSpeed = (notification.eqn_doubleValue(for: "wave_speed") ?? 0) * 1000 // m/s // distanza tra utente e terremoto let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0 // m // calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo let remainingDistance = waveSpeed * Double(currentCountdown) return distance - remainingDistance } /// Evaluate wave velocity based on push notification data /// - Returns: Wave velocity, used for animation private func evaluateWaveVelocity() -> Double { let waveSpeed = notification.eqn_doubleValue(for: "wave_speed") ?? 0 // km/s let velocity = waveSpeed * 1000 return velocity * waveAnimationRefreshRate } }