Files
eqn.ios/Sources/Earthquake Network/Models/EQNRealtimePushNotification.swift

285 lines
9.5 KiB
Swift

//
// EQNRealtimePushNotification.swift
// Earthquake Network
//
// Created by Andrea Busi on 13/07/23.
// Copyright © 2023 Earthquake Network. All rights reserved.
//
import Foundation
import UIKit
import CoreLocation
import Shogun
@objc
class EQNRealtimePushNotification: NSObject, Codable {
/// Tempo (in secondi) entro cui vengono mostrate allerte in tempo reale ricevute
private static let RealtimeAlertExpiration: TimeInterval = 28_800 // 8 ore
private enum CodingKeys: String, CodingKey {
case type
case intensity
case magnitude
case latitude
case longitude
case counter
case dateTime
case waveSpeed
case impactTimestamp
case peak
case title
case displayTitle
case displayBody
}
let type: String
/// Earthquake intensity
let intensity: Int
let magnitude: Int
/// Earthquake coordinate
let latitude: Double
let longitude: Double
/// Number of smartphones that report the earthquake
let counter: Int
let dateTime: Date?
/// Earthquake wave speed
let waveSpeed: Double
/// Calculated timestamp for earthquake on user position
let impactTimestamp: Date?
let peak: Double?
// Title received inside `aps.alert`
let title: String
// Title and body elaborated in NotificationService
let displayTitle: String
let displayBody: String
var coordinate: CLLocation {
.init(latitude: latitude, longitude: longitude)
}
// MARK: - Init
init(
type: String,
intensity: Int,
magnitude: Int,
latitude: Double,
longitude: Double,
counter: Int,
dateTime: Date?,
waveSpeed: Double,
impactTimestamp: Date?,
peak: Double?,
title: String,
displayTitle: String,
displayBody: String
) {
self.type = type
self.intensity = intensity
self.magnitude = magnitude
self.latitude = latitude
self.longitude = longitude
self.counter = counter
self.dateTime = dateTime
self.waveSpeed = waveSpeed
self.impactTimestamp = impactTimestamp
self.peak = peak
self.title = title
self.displayTitle = displayTitle
self.displayBody = displayBody
}
func distanceFromUser() -> CLLocationDistance {
// Usiamo la posizione salvata, che coincide con quella presnete in EQNUser.
// In questo modo non abbiamo dipendenze e possiamo includere questa classe
// anche nel target NotificationService
EQNUserData.shared.lastLocation?.distance(from: coordinate) ?? 0.0
}
/// Remaining time before earthquake gets user position
func currentCountdown() -> Int {
guard let impactTimestamp else { return 0 }
let now = Date()
let difference = lround(max(impactTimestamp.timeIntervalSince(now), 0))
return difference
}
@objc
func isCountdownExpired() -> Bool {
currentCountdown() <= 0
}
/// Intensity on user location
func relativeIntensity() -> Double {
guard distanceFromUser() > 0, let peak else { return 0 }
let distanceKm = distanceFromUser() / 1_000 // get in km
let relativeIntensity = peak * exp(-distanceKm/peak/250)
return relativeIntensity
}
// MARK: - Class
/// Remove any saved notification
@objc(removeStoredNotification)
static func removeStored() {
UserDefaults.standard.removeObject(forKey: UserDefaults.RealTimeAlertPayload)
UserDefaults.standard.removeObject(forKey: UserDefaults.RealTimeAlertDate)
}
@objc(storedNotification)
static func stored() -> EQNRealtimePushNotification? {
guard let date = UserDefaults.standard.object(forKey: UserDefaults.RealTimeAlertDate) as? Date else {
return nil
}
guard date.isBeforeInterval(Self.RealtimeAlertExpiration) else {
print("[EQNRealtimePushNotification] Saved notification expired")
return nil
}
guard let data = UserDefaults.standard.object(forKey: UserDefaults.RealTimeAlertPayload) as? Data else {
print("[EQNRealtimePushNotification] No notification saved for key '\(UserDefaults.RealTimeAlertPayload)'")
return nil
}
guard let notification = try? JSONDecoder().decode(EQNRealtimePushNotification.self, from: data) else {
print("[EQNRealtimePushNotification] Unable to decode given notification")
return nil
}
return notification
}
/// Convert and store a push notification payload.
/// Expected payload has the following structure:
/// ```
/// {
/// "title": "Allerta sismica in tempo reale",
/// "body": "Previsto uno scuotimento forte",
/// "userInfo": {
/// "datetime" : "2023-07-13 12:24:04",
/// ...
/// "aps": {
/// "alert" : {
/// "loc-key" : "Rilevato sisma forte a",
/// "title-loc-key" : "Allerta sismica in tempo reale",
/// "loc-args" : [
/// "150 km (Test)"
/// ]
/// },
/// }
/// }
/// }
/// ```
/// - Parameter payload: Notification payload
/// - Returns: `true` if save succeed, `false` otherwise
@objc(storeNotificationWithPayload:)
@discardableResult
static func store(payload: [String: Any]) -> Bool {
guard let notification = from(payload: payload) else {
print("[EQNRealtimePushNotification] Unable to convert received notification")
return false
}
guard let data = try? JSONEncoder().encode(notification) else {
print("[EQNRealtimePushNotification] Unable to encode given notification")
return false
}
UserDefaults.standard.set(data, forKey: UserDefaults.RealTimeAlertPayload)
UserDefaults.standard.set(Date(), forKey: UserDefaults.RealTimeAlertDate)
return true
}
@objc
private static func from(payload: [String: Any]) -> EQNRealtimePushNotification? {
guard let userInfo = payload["userInfo"] as? [String: Any],
let aps = userInfo["aps"] as? [String: Any],
let alert = aps["alert"] as? [String: Any] else {
print("[EQNRealtimePushNotification] Missing required info to parse push notification")
return nil
}
var title: String = ""
if let titleKey = alert["loc-key"] as? String, let args = alert["loc-args"] as? [String], let arg = args.first {
title = String(format: NSLocalizedString(titleKey, comment: ""), arg)
}
let displayTitle = payload.string(forKey: "title", orDefault: "")
let displayBody = payload.string(forKey: "body", orDefault: "")
return from(
userInfo: userInfo,
title: title,
displayTitle: displayTitle,
displayBody: displayBody
)
}
static func from(
userInfo: [AnyHashable: Any],
title: String,
displayTitle: String,
displayBody: String
) -> EQNRealtimePushNotification? {
guard let latitude = userInfo.double(forKey: "latitude"),
let longitude = userInfo.double(forKey: "longitude") else {
print("[EQNRealtimePushNotification] Unable to get coordinate from push notification")
return nil
}
let type = userInfo.string(forKey: "type", orDefault: "")
let intensity = userInfo.integer(forKey: "intensity", orDefault: 0)
let magnitude = userInfo.integer(forKey: "magnitude", orDefault: 0)
let counter = userInfo.integer(forKey: "counter", orDefault: 0)
var dateTime: Date?
if let dateString = userInfo.string(forKey: "datetime"), let date = EQNUtility.getDateFrom(dateString) {
dateTime = date
}
let waveSpeed = userInfo.double(forKey: "wave_speed", orDefault: 0.0) * 1000 // m/s
var impactTimestamp: Date?
if let timestamp = EQNUtility.calculateUserSeismicTimestamp(fromUserInfo: userInfo) {
impactTimestamp = timestamp
}
let peak = userInfo.double(forKey: "peak")
return .init(
type: type,
intensity: intensity,
magnitude: magnitude,
latitude: latitude,
longitude: longitude,
counter: counter,
dateTime: dateTime,
waveSpeed: waveSpeed,
impactTimestamp: impactTimestamp,
peak: peak,
title: title,
displayTitle: displayTitle,
displayBody: displayBody
)
}
}
extension EQNRealtimePushNotification {
/// Returns the color based on relative intensity
var relativeIntensityColor: UIColor {
let intensity = relativeIntensity()
switch intensity {
case _ where intensity < 0.004:
return UIColor(red: 90.0/255.0, green: 90.0/255.0, blue: 90.0/255.0, alpha: 1.0)
case _ where intensity < 0.30:
return UIColor(red: 38.0/255.0, green: 100.0/255.0, blue: 38.0/255.0, alpha: 1.0)
case _ where intensity < 0.70:
return UIColor(red: 255.0/255.0, green: 140.0/255.0, blue: 0.0, alpha: 1.0)
default:
return UIColor(red: 215.0/255.0, green: 0.0, blue: 0.0, alpha: 1.0)
}
}
}