Files
eqn.ios/Sources/Earthquake Network/Controllers/Realtime Alert/RealtimeAlertViewController.swift
T
2022-06-17 10:09:04 +02:00

214 lines
8.0 KiB
Swift

//
// 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
}
}