Files
eqn.ios/Sources/Earthquake Network/Controllers/Reports/SegnalazioniMapViewController.swift
T

293 lines
12 KiB
Swift

//
// SegnalazioniMapViewController.swift
// Earthquake Network
//
// Created by Andrea Busi on 06/03/21.
// Copyright © 2021 Earthquake Network. All rights reserved.
//
import Foundation
import MapKit
class SegnalazioniMapViewController: EQNBaseMapViewController {
struct MapCircle {
let color: UIColor
let circle: MKCircle
}
/// Contains circles and related colors to draw overlays on the map
private var mapCircles = [MapCircle]()
/// Reports currently showned on the map
private var filteredReports = [EQNSegnalazione]()
// MARK: - View Lifecycle
override func viewDidLoad() {
super.viewDidLoad()
}
// MARK: - Public
override func registerMapAnnotationViews() {
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
}
override func loadDataSource() {
guard let list = EQNManager.manager().elencoSelagnazioniManuali else { return }
// set the base filter
setDefaultFilter(for: list)
// filter report based on selected filter
let filterDate = filter.date
filteredReports = list.filter { $0.date > filterDate }
// create annotations to display on the map
let annotations = filteredReports.compactMap { EQNMapAnnotationUserReport(report: $0) }
// create circles to group cluster of reports and show on the map
let mapCircles = elaborateCircles(for: filteredReports)
addMapCircles(mapCircles)
// update map and center
updateMap(with: annotations)
}
override func elaborateMapCenter() {
var centerLocation: CLLocation?
// Se c'è un cluster distante dall'utente meno del raggio di notifica sulle segnalazioni,
// allora mostro all'utente quel cluster
if let userPosition = CLLocationManager().location {
let nearestCluser = mapCircles
.map { CLLocation(latitude: $0.circle.coordinate.latitude, longitude: $0.circle.coordinate.longitude) }
.sorted(by: { abs(userPosition.distance(from: $0)) < abs(userPosition.distance(from: $1)) })
.first
// controlliamo che sia inferiore al raggio impostato per le notifiche
if let radius = Double(EQNNotificheSegnalazioniUtente.shared().distanzaPosizione),
let nearestCluser = nearestCluser,
abs(nearestCluser.distance(from: userPosition)) < radius {
centerLocation = nearestCluser
}
}
// altrimenti mostro il cluster più recente
if centerLocation == nil, let newestReport = filteredReports.sorted(by: { $0.date > $1.date }).first {
// cerco il cerchio che contiene la segnalazione più recente
// tra i cerchi trovati, prendo quello più piccolo
let newestCircle = mapCircles
.map { $0.circle }
.filter { (circle) -> Bool in
let location = CLLocation(latitude: circle.coordinate.latitude, longitude: circle.coordinate.longitude)
let distance = abs(newestReport.coordinate.distance(from: location))
return distance < circle.radius
}
.sorted(by: { $0.radius < $1.radius })
.first
if let newestCircle = newestCircle {
centerLocation = CLLocation(latitude: newestCircle.coordinate.latitude, longitude: newestCircle.coordinate.longitude)
}
}
if let centerLocation = centerLocation {
setMapCenter(for: centerLocation)
}
}
override func didTapAnnotation(_ annotation: MKAnnotation) {
guard let annotation = annotation as? EQNMapAnnotationUserReport, let report = annotation.report else {
return
}
let difference = Int(Date().timeIntervalSince(report.date) / 60.0)
let title = EQNUtility.formattedString(forTimeDifference: difference) + " - \(report.intensity.description)"
var message = ""
+ "🏢 " + report.address
+ "\n" + EQNUtility.formattedDate(from: report.date) + " \(NSLocalizedString("share_yourtime", comment: ""))"
if !report.message.isEmpty {
message += "\n💬 \(report.message)"
}
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: NSLocalizedString("main_share", comment: ""), style: .default, handler: { [unowned self] _ in
self.openShareActivity(for: report)
}))
alert.addAction(UIAlertAction(title: NSLocalizedString("official_close", comment: ""), style: .cancel, handler: nil))
present(alert, animated: true, completion: nil)
}
// MARK: - Private
private func elaborateCircles(for reports: [EQNSegnalazione]) -> [MapCircle] {
let vector_latitude = reports.map { $0.coordinate.coordinate.latitude }
let vector_longitude = reports.map { $0.coordinate.coordinate.longitude }
let vector_date = reports.map { $0.date }
let vector_state = reports.map { $0.intensity.rawValue }
let minutes: TimeInterval = filter.minutes
var cluster_code = 0
var vector_cluster = [Int](repeating: 0, count: vector_latitude.count)
for i in 0..<vector_latitude.count {
let deltaMinute_i = getDeltaMinute(vector_date[i])
if vector_cluster[i] == 0 && deltaMinute_i <= minutes {
for j in 0..<vector_latitude.count {
let deltaMinute_j = getDeltaMinute(vector_date[j])
if i != j && deltaMinute_j <= minutes {
if abs(vector_latitude[i] - vector_latitude[j]) < 4 && abs(vector_longitude[i] - vector_longitude[j]) < 4 && abs(deltaMinute_i - deltaMinute_j) <= 20 {
if vector_cluster[j] > 0 {
vector_cluster[i] = vector_cluster[j]
} else {
if vector_cluster[i] > 0 {
vector_cluster[j] = vector_cluster[i]
} else {
cluster_code += 1
vector_cluster[i] = cluster_code
vector_cluster[j] = cluster_code
}
}
}
}
}
}
}
//calcola i centri dei cluster e l'intensità di ciascun cluster
var lat_centre = [Double](repeating: 0, count: cluster_code)
var lon_centre = [Double](repeating: 0, count: cluster_code)
var cluster_freq = [Int](repeating: 0, count: cluster_code)
var cluster_intensity = [Double](repeating: 0, count: cluster_code)
for k in 0..<cluster_code {
lat_centre[k] = 0
lon_centre[k] = 0
cluster_freq[k] = 0
cluster_intensity[k] = 0
for i in 0..<vector_latitude.count {
if vector_cluster[i] == k+1 {
lat_centre[k] = lat_centre[k] + vector_latitude[i]
lon_centre[k] = lon_centre[k] + vector_longitude[i]
cluster_freq[k] = cluster_freq[k] + 1
cluster_intensity[k] = cluster_intensity[k] + Double(vector_state[i])
}
}
if cluster_freq[k] > 0 {
lat_centre[k] = lat_centre[k]/Double(cluster_freq[k])
lon_centre[k] = lon_centre[k]/Double(cluster_freq[k])
cluster_intensity[k] = cluster_intensity[k]/Double(cluster_freq[k])
}
}
var lat_farest = [Double](repeating: 0, count: cluster_code)
var lon_farest = [Double](repeating: 0, count: cluster_code)
var max_distance = [Double](repeating: 0, count: cluster_code)
//per ogni cluster calcola il punto più lontano dal centro
for k in 0..<cluster_code {
max_distance[k] = 0
for i in 0..<vector_latitude.count {
if vector_cluster[i] == k+1 {
let distance = abs(lat_centre[k] - vector_latitude[i]) + abs(lon_centre[k] - vector_longitude[i])
if distance >= max_distance[k] {
lat_farest[k] = vector_latitude[i]
lon_farest[k] = vector_longitude[i]
max_distance[k] = distance
}
}
}
}
let circles = Array(0..<cluster_code).map { (i) -> MapCircle in
var value_distance = max_distance[i] / 20.0
if value_distance > 1.0 {
value_distance = 1.0
}
let value_intensity = (cluster_intensity[i]-1.0) / 2.0
let value_reference = max(value_distance, value_intensity)
let color: UIColor
if value_reference <= 0.5 {
let red = round(value_reference * 510)
color = UIColor(red: CGFloat(red / 255.0), green: 230.0/255.0, blue: 0.0, alpha: 1.0)
} else {
let green = round(230 - (value_reference - 0.5) * 460)
color = UIColor(red: 255.0, green: CGFloat(green / 255.0), blue: 0.0, alpha: 1.0)
}
let centre = CLLocation(latitude: lat_centre[i], longitude: lon_centre[i])
let farest = CLLocation(latitude: lat_farest[i], longitude: lon_farest[i])
let radius: CLLocationDistance = centre.distance(from: farest) + 4000
let circle = MKCircle(center: centre.coordinate, radius: radius)
return MapCircle(color: color, circle: circle)
}
return circles
}
private func addMapCircles(_ circles: [MapCircle]) {
// elimino vecchie circonferenze
let previousCircles = mapCircles.map { $0.circle }
mapView.removeOverlays(previousCircles)
// !!note: is important to assign here the circles
// otherwise `addOverlays` will not work
mapCircles = circles
// creo nuovi cerchi
let overlays = circles.map { $0.circle }
mapView.addOverlays(overlays)
}
private func getDeltaMinute(_ date: Date) -> TimeInterval {
Date().timeIntervalSince(date) / 60.0
}
private func openShareActivity(for report: EQNSegnalazione) {
// create message to share
let intensity = report.intensity.description
let difference = Int(Date().timeIntervalSince(report.date) / 60.0)
let time = EQNUtility.formattedString(forTimeDifference: difference)
let message = [
NSLocalizedString("share_hashtag", comment: ""),
intensity,
NSLocalizedString("share_felt", comment: ""),
report.address,
time + ".",
NSLocalizedString("share_notified", comment: "")
].joined(separator: " ")
let controller = UIActivityViewController(activityItems: [message], applicationActivities: [])
present(controller, animated: true)
}
// MARK: - Map
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
guard let annotation = annotation as? EQNMapAnnotationUserReport else {
return nil
}
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: EQNCustomAnnotationView.SingleLineIdentifier, for: annotation) as! EQNCustomAnnotationView
annotationView.image = annotation.image
annotationView.title = annotation.title
return annotationView
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let overlay = overlay as? MKCircle, let mapCircle = mapCircles.first(where: { $0.circle == overlay }) {
let circle = MKCircleRenderer(overlay: overlay)
circle.strokeColor = mapCircle.color
circle.fillColor = mapCircle.color.withAlphaComponent(0.1)
circle.lineWidth = 2.0
return circle
}
return MKOverlayRenderer(overlay: overlay)
}
}