214 lines
8.0 KiB
Swift
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
|
|
}
|
|
}
|