// // SeismicNetworksIntensityMapViewController.swift // Earthquake Network // // Created by Andrea Busi on 27/02/25. // Copyright © 2025 Earthquake Network. All rights reserved. // import UIKit import MapKit class SeismicNetworksIntensityMapViewController: EQNBaseMapViewController { private let seismic: EQNSisma private var shakemaps: [EQNShakemap] = [] private var pinStyle: MapPinStyle { get { AppPreferences.shared.mapPinStyle } set { AppPreferences.shared.mapPinStyle = newValue } } override var isFilterViewVisible: Bool { false } override var isCloseButtonVisible: Bool { false } // MARK: - UI lazy var descriptionView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = AppTheme.Colors.pureBlue let descriptionLabel = UILabel() descriptionLabel.translatesAutoresizingMaskIntoConstraints = false descriptionLabel.numberOfLines = 0 descriptionLabel.textColor = .white descriptionLabel.font = .preferredFont(forTextStyle: .subheadline) descriptionLabel.textAlignment = .center descriptionLabel.text = NSLocalizedString("shakemap_description", comment: "") view.addSubview(descriptionLabel) descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 2.0).isActive = true descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -2.0).isActive = true descriptionLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 2.0).isActive = true descriptionLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -2.0).isActive = true return view }() // MARK: - Init init( seismic: EQNSisma ) { self.seismic = seismic super.init() } @MainActor required init?(coder: NSCoder) { fatalError("Plase use init(seismic:) instead") } // MARK: - View Lifecycle override func extraUI() { super.extraUI() view.addSubview(descriptionView) descriptionView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true descriptionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true descriptionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true } override func configureUI() { super.configureUI() navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .done, primaryAction: .init(handler: { [weak self] _ in self?.dismiss(animated: true) })) navigationItem.rightBarButtonItems = [ UIBarButtonItem(image: UIImage(named: "navbar-icon-screenshot"), primaryAction: .init(handler: { [weak self] _ in self?.shareScreenshot() })), UIBarButtonItem(image: UIImage(named: "navbar-icon-pin-arrow"), primaryAction: .init(handler: { [weak self] _ in self?.nextPinStyle() })), ] } override func registerMapAnnotationViews() { mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .full)) mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .light)) mapView.register(EQNSeismicAnnotationView.self, forAnnotationViewWithReuseIdentifier: EQNSeismicAnnotationView.identifier(for: .circle)) } override func loadDataSource() { Task { let result = try await APIService.shared.fetchShakemap(isoCode: seismic.isoCode) elaborateShakemaps(result) } } override func elaborateMapCenter() { setMapCenter(for: seismic.coordinate, span: MKCoordinateSpan(latitudeDelta: 2, longitudeDelta: 2)) } // MARK: - Private private func elaborateShakemaps(_ shakemaps: [EQNShakemap]) { self.shakemaps = shakemaps var shakemapPolyline = [MKPolyline]() var shakemapAnnotations: [MKAnnotation] = [] for shakemap in shakemaps { // create coordinates for current shakemap let coordinates = zip(shakemap.lat, shakemap.lon).map { lat, lon in CLLocationCoordinate2D(latitude: Double(lat) / 10_000.0, longitude: Double(lon) / 10_000.0) } let intensityColors = getColors(for: shakemap.intensity) // create line to show on map let polyline = ShakemapPolyline(coordinates: coordinates, count: coordinates.count) polyline.intensity = shakemap.intensity polyline.intensityColor = intensityColors.lineColor shakemapPolyline.append(polyline) // create annotation to show on top of the line let middlePoint = coordinates[coordinates.count / 2] let annotation = EQNMapAnnotationShakemap(coordinate: middlePoint, shakemap: shakemap) annotation.intensityColor = intensityColors.lineColor annotation.intensityTextColor = intensityColors.textColor shakemapAnnotations.append(annotation) } let seismicAnnotation = EQNMapAnnotationSeismic(seismic: seismic) shakemapAnnotations.append(seismicAnnotation) // draw lines mapView.addOverlays(shakemapPolyline) updateMap(with: shakemapAnnotations) } private func nextPinStyle() { pinStyle.advance() reloadMap() } private func shareScreenshot() { let screenshot = createSnapshot(prepare: { descriptionView.isHidden = true }, restore: { descriptionView.isHidden = false }) let controller = UIActivityViewController(activityItems: [screenshot], applicationActivities: []) present(controller, animated: true) } // MARK: - MKMapViewDelegate override func setupAnnotationView(for annotation: MKAnnotation, on mapView: MKMapView) -> MKAnnotationView? { switch annotation { case let shakemapAnnotation as EQNMapAnnotationShakemap: return shakemapAnnotation.toAnnotationView(mapView: mapView, style: .light) case let seismicAnnotation as EQNMapAnnotationSeismic: return seismicAnnotation.toAnnotationView(mapView: mapView, style: pinStyle) default: return nil } } func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let polyline = overlay as? ShakemapPolyline { let renderer = MKPolylineRenderer(polyline: polyline) renderer.strokeColor = polyline.intensityColor renderer.lineWidth = 6.0 return renderer } return MKOverlayRenderer() } private func getColors(for intensity: Float) -> (textColor: UIColor, lineColor: UIColor) { let shakemapColors: [String] = [ "#3E26A8","#3E27AC","#3F28AF","#3F29B2","#402AB4","#402BB7","#412CBA","#412DBD","#422EBF","#422FC2", "#4330C5","#4331C8","#4332CA","#4433CD","#4434D0","#4535D2","#4537D5","#4538D7","#4639D9","#463ADC", "#463BDE","#463DE0","#473EE1","#473FE3","#4741E5","#4742E6","#4744E8","#4745E9","#4746EB","#4848EC", "#4849ED","#484BEE","#484CF0","#484EF1","#484FF2","#4850F3","#4852F4","#4853F5","#4854F6","#4756F7", "#4757F7","#4759F8","#475AF9","#475BFA","#475DFA","#465EFB","#4660FB","#4661FC","#4562FC","#4564FD", "#4465FD","#4367FD","#4368FE","#426AFE","#416BFE","#406DFE","#3F6EFF","#3E70FF","#3C71FF","#3B73FF", "#3974FF","#3876FE","#3677FE","#3579FD","#337AFD","#327CFC","#317DFC","#307FFB","#2F80FA","#2F82FA", "#2E83F9","#2E84F8","#2E86F8","#2E87F7","#2D88F6","#2D8AF5","#2D8BF4","#2D8CF3","#2D8EF2","#2C8FF1", "#2C90F0","#2B91EF","#2A93EE","#2994ED","#2895EC","#2797EB","#2798EA","#2699E9","#269AE8","#259BE8", "#259CE7","#249EE6","#249FE5","#23A0E5","#23A1E4","#22A2E4","#21A3E3","#20A5E3","#1FA6E2","#1EA7E1", "#1DA8E1","#1DA9E0","#1CAADF","#1BABDE","#1AACDD","#19ADDC","#17AEDA","#16AFD9","#14B0D8","#12B1D6", "#10B2D5","#0EB3D4","#0BB3D2","#08B4D1","#06B5CF","#04B6CE","#02B7CC","#01B7CA","#00B8C9","#00B9C7", "#00BAC6","#01BAC4","#02BBC2","#04BBC1","#06BCBF","#09BDBD","#0DBDBC","#10BEBA","#14BEB8","#17BFB6", "#1AC0B5","#1DC0B3","#20C1B1","#23C1AF","#25C2AE","#27C2AC","#29C3AA","#2BC3A8","#2CC4A6","#2EC4A5", "#2FC5A3","#31C5A1","#32C69F","#33C79D","#35C79B","#36C899","#38C896","#39C994","#3BC992","#3DCA90", "#40CA8D","#42CA8B","#45CB89","#48CB86","#4BCB84","#4ECC81","#51CC7F","#54CC7C","#57CC7A","#5ACC77", "#5ECD74","#61CD72","#64CD6F","#67CD6C","#6BCD69","#6ECD66","#72CD64","#76CC61","#79CC5E","#7DCC5B", "#81CC59","#84CC56","#88CB53","#8BCB51","#8FCB4E","#93CA4B","#96CA48","#9AC946","#9DC943","#A1C840", "#A4C83E","#A7C73B","#ABC739","#AEC637","#B2C635","#B5C533","#B8C431","#BBC42F","#BEC32D","#C2C32C", "#C5C22A","#C8C129","#CBC128","#CEC027","#D0BF27","#D3BF27","#D6BE27","#D9BE28","#DBBD28","#DEBC29", "#E1BC2A","#E3BC2B","#E6BB2D","#E8BB2E","#EABA30","#ECBA32","#EFBA35","#F1BA37","#F3BA39","#F5BA3B", "#F7BA3D","#F9BA3E","#FBBB3E","#FCBC3E","#FEBD3D","#FEBE3C","#FEC03B","#FEC13A","#FEC239","#FEC438", "#FEC537","#FEC735","#FEC834","#FECA33","#FDCB32","#FDCD31","#FDCE31","#FCD030","#FBD22F","#FBD32E", "#FAD52E","#F9D62D","#F9D82C","#F8D92B","#F7DB2A","#F7DD2A","#F6DE29","#F6E028","#F5E128","#F5E327", "#F5E526","#F5E626","#F5E825","#F5E924","#F5EB23","#F5EC22","#F5EE21","#F6EF20","#F6F11F","#F6F21E", "#F7F41C","#F7F51B","#F8F71A","#F8F818","#F9F916","#F9FB15" ] let minIntensity = shakemaps.map { $0.intensity }.min() ?? 0 let maxIntensity = shakemaps.map { $0.intensity }.max() ?? 255 let indexColor = if minIntensity == maxIntensity { 0 } else { Int(round((intensity-minIntensity)/(maxIntensity-minIntensity)*255)) } let lineColor = UIColor(hexString: shakemapColors[indexColor]) ?? .white let textColor: UIColor = indexColor < 65 ? .white : .black return (textColor: textColor, lineColor: lineColor) } } extension EQNMapAnnotationShakemap { func toAnnotationView( mapView: MKMapView, style: MapPinStyle, isUserSelection: Bool = false ) -> MKAnnotationView? { switch style { case .full, .light: let identifier = EQNSeismicAnnotationView.identifier(for: style) let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier, for: self) as! EQNSeismicAnnotationView annotationView.magnitude = intensityString(from: shakemap.intensity) annotationView.magnitudeTextColor = intensityTextColor ?? .black annotationView.magnitudeBackgroundColor = intensityColor annotationView.canShowCallout = true return annotationView case .circle: return nil } } private func intensityString(from intensity: Float) -> String { let intensityRounded = (intensity * 10).rounded() / 10 let intensityFloor = floor(intensityRounded) let romanNumerals: [Int: String] = [ 1: "I", 2: "II", 3: "III", 4: "IV", 5: "V", 6: "VI", 7: "VII", 8: "VIII", 9: "IX", 10: "X", 11: "XI", 12: "XII" ] var result = romanNumerals[Int(intensityFloor)] ?? "" if intensityRounded != intensityFloor { let reminder = Int(((intensityRounded - intensityFloor) * 10).rounded()) result += ".\(reminder)" } return result } } fileprivate class ShakemapPolyline: MKPolyline { var intensity: Float = 0 var intensityColor: UIColor = .white }