318 lines
14 KiB
Swift
318 lines
14 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
|
|
import Shogun
|
|
|
|
class SegnalazioniMapViewController: EQNBaseMapViewController {
|
|
|
|
struct MapCircle {
|
|
let color: UIColor
|
|
let circle: MKCircle
|
|
}
|
|
|
|
override var isCloseButtonVisible: Bool {
|
|
false
|
|
}
|
|
|
|
private let appPreferences = AppPreferences.shared
|
|
/// 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: - UI
|
|
|
|
private lazy var magnitudeLegendView: UIView = {
|
|
let view = UIView()
|
|
view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
|
let stackView = UIStackView()
|
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
|
stackView.axis = .horizontal
|
|
stackView.distribution = .fillEqually
|
|
[20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120].forEach { magnitude in
|
|
let label = UILabel()
|
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
label.text = (magnitude / 10).romanNumber()
|
|
label.backgroundColor = UIColor(named: "Mercalli \(magnitude)")
|
|
label.textAlignment = .center
|
|
label.font = .preferredFont(forTextStyle: .callout)
|
|
label.textColor = magnitude >= 100 ? .white : .black
|
|
stackView.addArrangedSubview(label)
|
|
}
|
|
|
|
view.addSubview(stackView)
|
|
stackView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
|
|
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
|
|
stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
|
|
stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
|
|
|
|
return view
|
|
}()
|
|
|
|
// MARK: - View Lifecycle
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
}
|
|
|
|
// MARK: - Public
|
|
|
|
override func extraUI() {
|
|
view.addSubview(magnitudeLegendView)
|
|
|
|
magnitudeLegendView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
|
|
magnitudeLegendView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
|
|
magnitudeLegendView.heightAnchor.constraint(equalToConstant: 25.0).isActive = true
|
|
magnitudeLegendView.topAnchor.constraint(equalTo: mapView.topAnchor).isActive = true
|
|
}
|
|
|
|
override func configureUI() {
|
|
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(onTapCloseButton(_:)))
|
|
navigationItem.rightBarButtonItems = [
|
|
UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), style: .plain, target: self, action: #selector(onTapScreenshotButton(_:))),
|
|
UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), style: .plain, target: self, action: #selector(onTapMapDetailStyleButton(_:)))
|
|
]
|
|
}
|
|
|
|
override func registerMapAnnotationViews() {
|
|
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SingleLineIdentifier)
|
|
mapView.register(EQNCustomAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNCustomAnnotationView.SmallIdentifier)
|
|
}
|
|
|
|
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(EQNSettingUserReportNotification.shared.distanzaMassima),
|
|
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)
|
|
}
|
|
}
|
|
|
|
// MARK: - Actions
|
|
|
|
@objc private func onTapCloseButton(_ sender: Any) {
|
|
dismiss(animated: true)
|
|
}
|
|
|
|
@objc private func onTapMapDetailStyleButton(_ sender: Any) {
|
|
appPreferences.userReportExpandedView.toggle()
|
|
reloadMap()
|
|
}
|
|
|
|
@objc private func onTapScreenshotButton(_ sender: Any) {
|
|
let screenshot = createSnapshot {
|
|
// nascondiamo la legenda
|
|
magnitudeLegendView.isHidden = true
|
|
} restore: {
|
|
// ri-visualizziamo la legenda
|
|
magnitudeLegendView.isHidden = false
|
|
}
|
|
|
|
let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: [])
|
|
present(controller, animated: true)
|
|
}
|
|
|
|
// 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 }
|
|
|
|
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 = EQNUtility.getDeltaMinute(vector_date[i])
|
|
if vector_cluster[i] == 0 && deltaMinute_i <= minutes {
|
|
for j in 0..<vector_latitude.count {
|
|
let deltaMinute_j = EQNUtility.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
|
|
let color: UIColor = AppTheme.Colors.darkGray
|
|
|
|
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)
|
|
}
|
|
|
|
// MARK: - Map
|
|
|
|
override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? {
|
|
guard let annotation = annotation as? EQNMapAnnotationUserReport else {
|
|
return nil
|
|
}
|
|
|
|
let identifier = appPreferences.userReportExpandedView ? EQNCustomAnnotationView.SingleLineIdentifier : EQNCustomAnnotationView.SmallIdentifier
|
|
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: annotation) as! EQNCustomAnnotationView
|
|
|
|
let size = appPreferences.userReportExpandedView ? EQNCustomAnnotationView.SingleLineImageHeight : EQNCustomAnnotationView.SmallViewImageHeight
|
|
annotationView.image = annotation.image(with: size)
|
|
annotationView.title = annotation.timeDifference
|
|
annotationView.canShowCallout = true
|
|
// Psizioniamo più in alto le segnalazioni con intensità maggiore.
|
|
// Valori maggiori di anchorPointZ mettono la view più in basso,
|
|
// quindi invertiamo il valore dell'intensità
|
|
annotationView.layer.anchorPointZ = (1000 - CGFloat(annotation.report?.intensity ?? 0))
|
|
|
|
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)
|
|
}
|
|
}
|