// // Copyright Amazon.com Inc. or its affiliates. // All Rights Reserved. // // SPDX-License-Identifier: Apache-2.0 // import SwiftUI import Mapbox extension _MGLMapViewWrapper { /// Coordinator class for `_MGLMapViewWrapper` that manages MGLMapViewDelegate methods. final class Coordinator: NSObject, MGLMapViewDelegate { var control: _MGLMapViewWrapper init(_ control: _MGLMapViewWrapper) { self.control = control } func mapView(_ mapView: MGLMapView, regionDidChangeWith reason: MGLCameraChangeReason, animated: Bool) { DispatchQueue.main.async { self.control.zoomLevel = mapView.zoomLevel self.control.bounds = mapView.visibleCoordinateBounds self.control.center = mapView.centerCoordinate self.control.heading = mapView.camera.heading } } func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) { control.userLocation = userLocation?.coordinate } func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) { if let source = style.sources.first as? MGLVectorTileSource { let attribution = source.attributionInfos.first control.attribution = attribution?.title.string ?? "" } setupRenderingLayers(for: style) setTapRecognizer() } func mapView(_ mapView: MGLMapView, regionWillChangeAnimated animated: Bool) { if let calloutViewToRemove = mapView.subviews.first(where: { $0.tag == 42 }) { mapView.willRemoveSubview(calloutViewToRemove) calloutViewToRemove.removeFromSuperview() } } } } // MARK: Private Configuration Methods extension _MGLMapViewWrapper.Coordinator { /// Add rendering layers to `MGLStyle`. /// /// - FeatureLayer: Features representing a point on the map. /// Sourced through `features` property of `_MGLMapViewWrapper`. /// - Identifier: `"aml_feature_style_layer"` /// - Uses source identifier: `"aml_location_source"` /// - Rendered image identifier: `"aml_feature"` /// - ClusterCircleLayer: Cluster Features representing a cluster of features on the map. /// Clustering behavior determined by `ClusteringBehavior`. /// - Identifier: `"aml_cluster_circle_layer"` /// - Uses source identifier: `"aml_location_source"` /// - ClusterNumberLayer: Renders numbers representing the amount of features within a feature cluster. /// - Identifier: `"aml_cluster_number_layer"` /// - Parameter style: The `MGLStyle` that the layers will be added to. private func setupRenderingLayers(for style: MGLStyle) { let locationSource = makeLocationSource() style.addSource(locationSource) let defaultImage = UIImage( named: "AMLFeatureView", in: Bundle.module, compatibleWith: nil )! style.setImage(defaultImage, forName: "aml_feature") let featureLayer = makeFeatureLayer(for: locationSource) style.addLayer(featureLayer) let clusterCircleLayer = makeClusterCircleLayer(for: locationSource) style.addLayer(clusterCircleLayer) let clusterNumberLayer = makeClusterNumberLayer(for: locationSource) style.addLayer(clusterNumberLayer) } /// Create the location source used to source features in the subsequent layers. /// - Returns: A `MGLShapeSource` with defined clustering options. private func makeLocationSource() -> MGLShapeSource { MGLShapeSource.init( identifier: "aml_location_source", shape: nil, options: [ .clustered: control.clusteringBehavior.shouldCluster, .maximumZoomLevelForClustering: control.clusteringBehavior.maximumZoomLevel, .clusterRadius: control.clusteringBehavior.clusterRadius ] ) } /// Create the layer that renders features on the map. /// - Parameter source: The layer's source. /// - Returns: A `MGLSymbolStyleLayer` that will render features provided through the `source`. private func makeFeatureLayer(for source: MGLSource) -> MGLSymbolStyleLayer { let featureLayer = MGLSymbolStyleLayer(identifier: "aml_feature_style_layer", source: source) featureLayer.iconImageName = NSExpression(forConstantValue: "aml_feature") featureLayer.iconIgnoresPlacement = NSExpression(forConstantValue: true) featureLayer.iconAllowsOverlap = NSExpression(forConstantValue: true) featureLayer.predicate = NSPredicate(format: "cluster != YES") return featureLayer } /// Create the layer that renders feature clusters on the map. /// - Parameter source: The layer's source. Generally the same as the feature layer's `source`. /// - Returns: A `MGLCircleStyleLayer` that will render feature clusters provided through the `source`. /// Clustering behavior defined through `ClusteringBehavior`. private func makeClusterCircleLayer(for source: MGLSource) -> MGLCircleStyleLayer { let clusterCircleLayer = MGLCircleStyleLayer(identifier: "aml_cluster_circle_layer", source: source) clusterCircleLayer.circleRadius = NSExpression( format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'exponential', 10, %@)", [ control.clusteringBehavior.maximumZoomLevel + 2: 20, control.clusteringBehavior.maximumZoomLevel + 4: 30, control.clusteringBehavior.maximumZoomLevel + 6: 40, control.clusteringBehavior.maximumZoomLevel + 8: 50, control.clusteringBehavior.maximumZoomLevel + 9: 60 ] ) clusterCircleLayer.circleStrokeColor = NSExpression(forConstantValue: UIColor.white) clusterCircleLayer.circleStrokeWidth = NSExpression(forConstantValue: 4) clusterCircleLayer.circleColor = NSExpression( format: "mgl_step:from:stops:(point_count, %@, %@)", control.clusteringBehavior.clusterColor, control.clusteringBehavior.clusterColorSteps ) clusterCircleLayer.predicate = NSPredicate(format: "cluster == YES") return clusterCircleLayer } /// Create the layer that renders numbers on the feature clusters on the map. /// - Parameter source: The layer's source. Generally the same as the cluster circle layer's `source`. /// - Returns: A `MGLSymbolStyleLayer` that renders numbers on feature clusters provided through the `source`. private func makeClusterNumberLayer(for source: MGLSource) -> MGLSymbolStyleLayer { let clusterNumberLayer = MGLSymbolStyleLayer(identifier: "aml_cluster_number_layer", source: source) clusterNumberLayer.text = NSExpression(format: "CAST(point_count, 'NSString')") clusterNumberLayer.textColor = NSExpression(forConstantValue: control.clusteringBehavior.clusterNumberColor) clusterNumberLayer.textFontNames = NSExpression(forConstantValue: ["Arial Bold"]) clusterNumberLayer.predicate = NSPredicate(format: "cluster == YES") return clusterNumberLayer } /// Set a tap gesture recognizer whose action calls the implemention of `featureTapped` or `clusterTapped`. private func setTapRecognizer() { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(handleTap(sender:))) control.mapView.addGestureRecognizer(tapGestureRecognizer) } /// Called by the `UITapGestureRecognizer`, this determines if a feature or feature cluster was tapped. /// /// If so, it will call the provided implementation of `featureTapped` or `clusterTapped`. /// /// - Important: You should never have to call this from your code. /// The action is called by the tap gesture recognizer. /// - Parameter sender: The `UITapGestureRecognizer`. @objc func handleTap(sender: UITapGestureRecognizer) { let location = sender.location(in: control.mapView) guard let tappedFeature = control.mapView.visibleFeatures( at: location, styleLayerIdentifiers: [ "aml_feature_style_layer", "aml_cluster_circle_layer" ] ).first else { return } if let tappedCluster = tappedFeature as? MGLPointFeatureCluster { control.proxyDelegate.clusterTapped(control.mapView, tappedCluster) } else if let tappedFeature = tappedFeature as? MGLPointFeature { control.proxyDelegate.featureTapped(control.mapView, tappedFeature) } } }