// // 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 // app icon and name displayed on the screenshot private lazy var watermarkView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.isHidden = true let logo = UIImageView(image: .init(named: "eq_icon_transparent")) logo.translatesAutoresizingMaskIntoConstraints = false logo.contentMode = .scaleAspectFit view.addSubview(logo) logo.topAnchor.constraint(equalTo: view.topAnchor).isActive = true logo.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true logo.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true logo.widthAnchor.constraint(equalTo: logo.heightAnchor).isActive = true let title = UILabel() title.translatesAutoresizingMaskIntoConstraints = false title.text = NSLocalizedString("app_name", comment: "") + " App" title.textColor = AppTheme.Colors.red title.font = .preferredFont(forTextStyle: .title3, weight: .semibold) view.addSubview(title) title.leadingAnchor.constraint(equalTo: logo.trailingAnchor, constant: 10.0).isActive = true title.centerYAnchor.constraint(equalTo: logo.centerYAnchor).isActive = true title.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true return view }() 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) view.addSubview(watermarkView) 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 watermarkView.topAnchor.constraint(equalTo: mapView.topAnchor, constant: 10.0).isActive = true watermarkView.leadingAnchor.constraint(equalTo: mapView.leadingAnchor, constant: 10.0).isActive = true watermarkView.heightAnchor.constraint(equalToConstant: 40.0).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(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) } } // MARK: - Actions @objc private func onTapCloseButton(_ sender: Any) { dismiss(animated: true) } @objc private func onTapMapDetailStyleButton(_ sender: Any) { appPreferences.userReportExpandedView.toggle() loadDataSource() } @objc private func onTapScreenshotButton(_ sender: Any) { let snapshot = createSnapshot() let controller = UIActivityViewController(activityItems: [snapshot], applicationActivities: []) present(controller, animated: true) } public func createSnapshot() -> UIImage { // mostriamo il watermark e nascondiamo la legenda watermarkView.isHidden = false magnitudeLegendView.isHidden = true // riduciamo la porzione da salvare alla sola mappa (eliminiamo i filtri) let size = CGSize(width: view.bounds.width, height: mapView.bounds.maxY) let renderer = UIGraphicsImageRenderer(size: size) let image = renderer.image { ctx in view.drawHierarchy(in: view.bounds, afterScreenUpdates: true) } // torniamo allo stato originale watermarkView.isHidden = true magnitudeLegendView.isHidden = false return image } // 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.. 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.. 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..= max_distance[k] { lat_farest[k] = vector_latitude[i] lon_farest[k] = vector_longitude[i] max_distance[k] = distance } } } } let circles = Array(0.. 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) } private func getDeltaMinute(_ date: Date) -> TimeInterval { Date().timeIntervalSince(date) / 60.0 } // 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) } }