// // 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 private let containerView = RealtimeAlertContainerView() private var notificationView: RealtimeAlertView { containerView.alertView } /// Alert to display private let realtimeAlert: EQNRealtimePushNotification /// 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? // MARK: - Init @objc init(notification: EQNRealtimePushNotification) { self.realtimeAlert = notification super.init(nibName: nil, bundle: nil) self.waveAnimationCurrentRadius = currentWavePosition() self.waveAnimationVelocity = evaluateWaveAnimationVelocity() } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { // importante togliere il delegato, altrimenti causa crash notificationView.mapView.delegate = nil } // MARK: - View Lifecycle override func loadView() { view = containerView } override func viewDidLoad() { super.viewDidLoad() configureUI() updateUI() startCountdown() startWaveAnimation() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) containerView.startBackgroundAnimation() } // MARK: - Private private func configureUI() { notificationView.closeButton.addTarget(self, action: #selector(onTapClose(_:)), for: .touchUpInside) // configure color for animation containerView.animationColor = realtimeAlert.relativeIntensityColor } private func updateUI() { notificationView.descriptionLabel.text = realtimeAlert.title // update title with distance from earthquake let distanceRound = Int(round(realtimeAlert.distanceFromUser() / 1_000)) notificationView.descriptionLabel.text = (notificationView.descriptionLabel.text ?? "") + ".\n" + String(format: NSLocalizedString("official_distance", comment: ""), distanceRound) notificationView.intensityLabel.text = realtimeAlert.displayBody notificationView.intensityLabel.textColor = realtimeAlert.relativeIntensityColor // center map on the earthquake coordinate let span = MKCoordinateSpan(latitudeDelta: 10.5, longitudeDelta: 10.5) let region = MKCoordinateRegion(center: realtimeAlert.coordinate.coordinate, span: span) notificationView.mapView.setCenter(realtimeAlert.coordinate.coordinate, animated: false) notificationView.mapView.setRegion(region, animated: true) // aggiungiamo annotation con epicentro sisma notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity) // simuliamo animazione dell'onda sismica notificationView.addMapCircle(center: realtimeAlert.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: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate]) } } private func startCountdown() { // show countdown only if time is less than 300 seconds if realtimeAlert.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) { // invalidiamo i timer, altri countdownTimer?.invalidate() countdownTimer = nil waveAnimationTimer?.invalidate() waveAnimationTimer = nil onClose() dismiss(animated: true) } // MARK: - Timer @objc private func countdownTimerFired(_ sender: Timer) { let countdown = realtimeAlert.currentCountdown() notificationView.waveTimeLabel.text = String.localizedStringWithFormat(NSLocalizedString("alert_wave", comment: ""), countdown) notificationView.waveTimeLabel.textColor = waveTimeTextColor(for: countdown) if countdown <= 0 { // stop the countdown countdownTimer?.invalidate() countdownTimer = nil } } @objc private func mapWaveAnimationFired(_ sender: Timer) { waveAnimationCurrentRadius += waveAnimationVelocity notificationView.addMapCircle(center: realtimeAlert.coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation") } // MARK: - Helpers /// 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 { // distanza tra utente e terremoto let distance = realtimeAlert.distanceFromUser() // calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo let remainingDistance = realtimeAlert.waveSpeed * Double(realtimeAlert.currentCountdown()) return distance - remainingDistance } /// Evaluate wave velocity based on push notification data /// - Returns: Wave velocity, used for animation private func evaluateWaveAnimationVelocity() -> Double { let velocity = realtimeAlert.waveSpeed return velocity * waveAnimationRefreshRate } /// Returns the text color based on impact countdown private func waveTimeTextColor(for countdown: Int) -> UIColor { switch countdown { case _ where countdown > 15: return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0) case _ where countdown > 5: return UIColor(red: 255.0/255.0, green: 100.0/255.0, blue: 0.0, alpha: 1.0) default: return UIColor(red: 255.0/255.0, green: 0.0/255.0, blue: 0.0, alpha: 1.0) } } }