285 lines
9.5 KiB
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)
|
|
}
|
|
}
|
|
}
|