refactor: Create a model for the realtime alert

This commit is contained in:
Andrea Busi
2022-06-17 12:49:57 +02:00
parent 2877dff23c
commit 63592e6cfb
4 changed files with 120 additions and 80 deletions
@@ -140,6 +140,7 @@
65DBFB7425E2BBF20041CBA6 /* GADTMediumTemplateView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 65DBFB6F25E2BBF20041CBA6 /* GADTMediumTemplateView.xib */; };
65DBFB7525E2BBF20041CBA6 /* GADTMediumTemplateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65DBFB7125E2BBF20041CBA6 /* GADTMediumTemplateView.m */; };
65DBFB7625E2BBF20041CBA6 /* GADTTemplateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 65DBFB7225E2BBF20041CBA6 /* GADTTemplateView.m */; };
65E1226C285C92AA000E294A /* EQNRealtimeAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E1226B285C92AA000E294A /* EQNRealtimeAlert.swift */; };
65E1B19B260F980600A0ACBA /* Dictionary+EQNExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65E1B19A260F980600A0ACBA /* Dictionary+EQNExtensions.swift */; };
8C10B0B92281FE7F00125C9F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8C10B0BD2281FE7F00125C9F /* Localizable.strings */; };
8C10B0BA2281FE7F00125C9F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 8C10B0BD2281FE7F00125C9F /* Localizable.strings */; };
@@ -430,6 +431,7 @@
65DBFB7125E2BBF20041CBA6 /* GADTMediumTemplateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GADTMediumTemplateView.m; sourceTree = "<group>"; };
65DBFB7225E2BBF20041CBA6 /* GADTTemplateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GADTTemplateView.m; sourceTree = "<group>"; };
65DBFB7325E2BBF20041CBA6 /* GADTMediumTemplateView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GADTMediumTemplateView.h; sourceTree = "<group>"; };
65E1226B285C92AA000E294A /* EQNRealtimeAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EQNRealtimeAlert.swift; sourceTree = "<group>"; };
65E1B19A260F980600A0ACBA /* Dictionary+EQNExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+EQNExtensions.swift"; sourceTree = "<group>"; };
8C10B0BC2281FE7F00125C9F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = "<group>"; };
8C10B0BE2281FE9E00125C9F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
@@ -1084,6 +1086,7 @@
8C593E89217BA2470008B260 /* EQNSegnalazione.m */,
8CF4F4D9216D44930057110B /* EQNPastquakes.h */,
8CF4F4DA216D44930057110B /* EQNPastquakes.m */,
65E1226B285C92AA000E294A /* EQNRealtimeAlert.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1592,6 +1595,7 @@
DCA5B6E7252E4BD8002AEC96 /* EQNBaseTableViewCell.swift in Sources */,
8CF66058214C566B009F4314 /* ServerRequest.m in Sources */,
DCF9E14D24F6D1AA002B6B1D /* EQNData.swift in Sources */,
65E1226C285C92AA000E294A /* EQNRealtimeAlert.swift in Sources */,
DC52B8A524FCCD6900ABEBA6 /* AppTheme.swift in Sources */,
653C680425F3DF8A00FE52AC /* EQNBaseMapViewController.swift in Sources */,
658BC02B2859A4D3009EECAA /* RealtimeAlertView.swift in Sources */,
@@ -120,8 +120,9 @@ typedef NS_ENUM(NSInteger, AllerteTableRow) {
if (date && [EQNUtility getDifferenceMinute:date] < TEMPO_VISUALIZZAZIONE_NOTIFICA) {
self.isNotificaAttiva = YES;
NSDictionary *info = [EQNUtility loadDictionaryFromUserDefaultsForKey:NOTIFICHE_RETE_SMARTPHONE_DIZIONARIO_NOTIFICA];
RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithNotification:info];
if (controller) {
EQNRealtimeAlert *alert = [[EQNRealtimeAlert alloc] initWithNotification:info];
if (alert) {
RealtimeAlertViewController *controller = [[RealtimeAlertViewController alloc] initWithAlert:alert];
if (@available(iOS 13.0, *)) {
controller.modalInPresentation = YES;
}
@@ -16,17 +16,9 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
// 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
/// Alert to display
private let realtimeAlert: EQNRealtimeAlert
/// Timer to constantly update countdown label
private var countdownTimer: Timer?
/// Refresh time for wave animation
@@ -37,30 +29,15 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
/// 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
init(alert: EQNRealtimeAlert) {
self.realtimeAlert = alert
super.init(nibName: nil, bundle: nil)
self.waveAnimationCurrentRadius = currentWavePosition()
self.waveAnimationVelocity = evaluateWaveVelocity()
self.waveAnimationVelocity = evaluateWaveAnimationVelocity()
}
required init?(coder: NSCoder) {
@@ -90,39 +67,35 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
}
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)
}
notificationView.descriptionLabel.text = realtimeAlert.title
// update title with distance from earthquake
let distance = EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0
let distanceRound = Int(round(distance / 1_000))
let distanceRound = Int(round(realtimeAlert.distanceFromUser() / 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)
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
let intensity = notification.eqn_intValue(for: "intensity") ?? 0
notificationView.addMapAnnotation(center: coordinate.coordinate, intensity: intensity)
notificationView.addMapAnnotation(center: realtimeAlert.coordinate.coordinate, intensity: realtimeAlert.intensity)
// simuliamo animazione dell'onda sismica
notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
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: [coordinate.coordinate, lastPosition.coordinate])
notificationView.addMapLine(coordinates: [realtimeAlert.coordinate.coordinate, lastPosition.coordinate])
}
}
private func startCountdown() {
// show countdown only if time is less than 300 seconds
if currentCountdown < 300 {
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)
@@ -146,9 +119,10 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
// MARK: - Timer
@objc private func countdownTimerFired(_ sender: Timer) {
notificationView.waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), currentCountdown)
let countdown = realtimeAlert.currentCountdown()
notificationView.waveTimeLabel.text = String(format: NSLocalizedString("alert_wave", comment: ""), countdown)
if currentCountdown <= 0 {
if countdown <= 0 {
// stop the countdown
countdownTimer?.invalidate()
countdownTimer = nil
@@ -157,57 +131,27 @@ class RealtimeAlertViewController: UIViewController, MKMapViewDelegate {
@objc private func mapWaveAnimationFired(_ sender: Timer) {
waveAnimationCurrentRadius += waveAnimationVelocity
notificationView.addMapCircle(center: coordinate.coordinate, radius: waveAnimationCurrentRadius, overlayId: "wave_animation")
notificationView.addMapCircle(center: realtimeAlert.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
let distance = realtimeAlert.distanceFromUser()
// calcoliamo la distanza rimanente da mostrare, perchè la schermata potrebbe anche essere aperta in ritardo
let remainingDistance = waveSpeed * Double(currentCountdown)
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 evaluateWaveVelocity() -> Double {
let waveSpeed = notification.eqn_doubleValue(for: "wave_speed") ?? 0 // km/s
let velocity = waveSpeed * 1000
private func evaluateWaveAnimationVelocity() -> Double {
let velocity = realtimeAlert.waveSpeed
return velocity * waveAnimationRefreshRate
}
}
@@ -0,0 +1,91 @@
//
// EQNRealtimeAlert.swift
// Earthquake Network
//
// Created by Andrea Busi on 17/06/22.
// Copyright © 2022 Earthquake Network. All rights reserved.
//
import Foundation
import CoreLocation
@objc
class EQNRealtimeAlert: NSObject {
typealias NotificationPayload = [String: Any]
/// Title to display
var title: String = ""
/// Earthquake coordinate
let coordinate: CLLocation
/// Earthquake intensity
let intensity: Int
/// Earthquake wave speed
let waveSpeed: Double
/// Calculated timestamp for earthquake on user position
let impactTimestamp: Date
// MARK: - Init
@objc
init?(notification: [String: Any]) {
guard let alert = Self.getPushAlertPayload(from: notification),
let coordinate = Self.getCoordinate(from: notification),
let impactTimestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: notification) else {
return nil
}
self.coordinate = coordinate
self.impactTimestamp = impactTimestamp
if let title = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
self.title = String(format: NSLocalizedString(title, comment: ""), arg)
}
self.intensity = notification.eqn_intValue(for: "intensity") ?? 0
self.waveSpeed = (notification.eqn_doubleValue(for: "wave_speed") ?? 0) * 1000 // m/s
}
func distanceFromUser() -> CLLocationDistance {
EQNUser.default().lastPosition?.distance(from: coordinate) ?? 0.0
}
/// Remaining time before earthquake gets user position
func currentCountdown() -> Int {
let now = Date()
let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0))
return difference
}
@objc
func isCountdownExpired() -> Bool {
currentCountdown() <= 0
}
// MARK: - Helpers
/// 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
}
/// 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)
}
}