Created
June 5, 2020 13:08
-
-
Save KoCMoHaBTa/d92ea94515fcfdb11e975f9bf63b09f1 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// GMSMapView+ClusterKit.swift | |
// ClusterKit | |
// | |
// Created by Milen Halachev on 5.06.20. | |
// Copyright © 2020 Elders. All rights reserved. | |
// | |
import Foundation | |
import GoogleMaps | |
import ClusterKit | |
/** | |
The GMSMapViewDataSource protocol is adopted by an object that mediates the GMSMapView’s data. The data source provides the markers that represent clusters on map. | |
*/ | |
@objc protocol GMSMapViewDataSource: NSObjectProtocol { | |
/** | |
Asks the data source for a marker that represent the given cluster. | |
@param mapView A map view object requesting the marker. | |
@param cluster The cluster to represent. | |
@return An object inheriting from GMSMarker that the map view can use for the specified cluster. | |
*/ | |
@objc optional func mapView(_ mapView: GMSMapView, markerFor cluster: CKCluster) -> GMSMarker | |
} | |
/** | |
GMSMarker category adopting the CKAnnotation protocol. | |
*/ | |
extension GMSMarker { | |
/** | |
The cluster that the marker is related to. | |
*/ | |
private static var clusterKey = "" | |
weak var cluster: CKCluster? { | |
get { objc_getAssociatedObject(self, &Self.clusterKey) as? CKCluster } | |
set { objc_setAssociatedObject(self, &Self.clusterKey, newValue, .OBJC_ASSOCIATION_ASSIGN) } | |
} | |
} | |
/** | |
GMSMapView category adopting the CKMap protocol. | |
*/ | |
extension GMSMapView: CKMap { | |
private static var clusterManagerKey = "" | |
public var clusterManager: CKClusterManager { | |
var clusterManager = objc_getAssociatedObject(self, &Self.clusterManagerKey) as? CKClusterManager | |
if clusterManager == nil { | |
clusterManager = .init() | |
clusterManager?.map = self | |
objc_setAssociatedObject(self, &Self.clusterManagerKey, clusterManager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
} | |
return clusterManager! | |
} | |
private static var dataSourceKey = "" | |
/** | |
Data source instance that adopt the GMSMapViewDataSource. | |
*/ | |
weak var dataSource: GMSMapViewDataSource? { | |
get { objc_getAssociatedObject(self, &Self.dataSourceKey) as? GMSMapViewDataSource } | |
set { objc_setAssociatedObject(self, &Self.dataSourceKey, newValue, .OBJC_ASSOCIATION_ASSIGN) } | |
} | |
private static var markersKey = "" | |
private var markers: NSMapTable<CKCluster, GMSMarker> { | |
var markers = objc_getAssociatedObject(self, &Self.markersKey) as? NSMapTable<CKCluster, GMSMarker> | |
if markers == nil { | |
markers = .strongToStrongObjects() | |
objc_setAssociatedObject(self, &Self.markersKey, markers, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
} | |
return markers! | |
} | |
/** | |
Returns the marker representing the given cluster. | |
@param cluster The cluster for which to return the corresponding marker. | |
@return The value associated with cluster, or nil if no value is associated with cluster. | |
*/ | |
func marker(for cluster: CKCluster) -> GMSMarker? { | |
return self.markers.object(forKey: cluster) | |
} | |
public var visibleMapRect: MKMapRect { | |
let bounds = GMSCoordinateBounds(region: self.projection.visibleRegion()) | |
let sw = MKMapPoint(bounds.southWest) | |
let ne = MKMapPoint(bounds.northEast); | |
let x = sw.x | |
let y = ne.y | |
var width = ne.x - sw.x | |
var height = sw.y - ne.y | |
// Handle antimeridian crossing | |
if width < 0 { | |
width = ne.x + MKMapSize.world.width - sw.x | |
} | |
if height < 0 { | |
height = sw.y + MKMapSize.world.height - ne.y | |
} | |
return .init(x: x, y: y, width: width, height: height) | |
} | |
public var zoom: Double { | |
let bounds = GMSCoordinateBounds(region: self.projection.visibleRegion()) | |
var longitudeDelta = bounds.northEast.longitude - bounds.southWest.longitude | |
// Handle antimeridian crossing | |
if longitudeDelta < 0 { | |
longitudeDelta = 360 + bounds.northEast.longitude - bounds.southWest.longitude | |
} | |
return log2(360 * Double(self.frame.size.width) / (256 * longitudeDelta)) | |
} | |
private func add(_ cluster: CKCluster) { | |
var marker = self.dataSource?.mapView?(self, markerFor: cluster) | |
if marker == nil { | |
marker = .init(position: cluster.coordinate) | |
if cluster.count > 1 { | |
marker?.icon = GMSMarker.markerImage(with: .purple) | |
} | |
} | |
marker?.cluster = cluster; | |
marker?.zIndex = 1; | |
marker?.map = self; | |
self.markers.setObject(marker, forKey: cluster) | |
} | |
private func remove(_ cluster: CKCluster) { | |
let marker = self.markers.object(forKey: cluster) | |
marker?.map = nil | |
self.markers.removeObject(forKey: cluster) | |
} | |
public func add(_ clusters: [CKCluster]) { | |
clusters.forEach({ self.add($0) }) | |
} | |
public func remove(_ clusters: [CKCluster]) { | |
clusters.forEach({ self.remove($0) }) | |
} | |
public func perform(_ animations: [CKClusterAnimation], completion: ((Bool) -> Void)? = nil) { | |
var animationsBlock: () -> Void = {} | |
var completionBlock: (Bool) -> Void = { finished in | |
completion?(finished) | |
} | |
for animation in animations { | |
let marker = self.markers.object(forKey: animation.cluster) | |
marker?.zIndex = 0 | |
marker?.position = animation.from | |
let previousAnimationsBlock = animationsBlock | |
animationsBlock = { | |
previousAnimationsBlock() | |
marker?.layer.latitude = animation.to.latitude; | |
marker?.layer.longitude = animation.to.longitude; | |
} | |
let previousCompletionBlock = completionBlock | |
completionBlock = { finished in | |
marker?.zIndex = 1 | |
previousCompletionBlock(finished) | |
} | |
} | |
if self.clusterManager.delegate?.clusterManager?(self.clusterManager, performAnimations: animationsBlock, completion: completionBlock) == nil { | |
let curve: CAMediaTimingFunction | |
switch (self.clusterManager.animationOptions) { | |
case .curveEaseInOut: | |
curve = .init(name: .easeInEaseOut) | |
break; | |
case .curveEaseIn: | |
curve = .init(name: .easeIn) | |
break; | |
case .curveEaseOut: | |
curve = .init(name: .easeOut) | |
break; | |
case .curveLinear: | |
curve = .init(name: .linear) | |
break; | |
default: | |
curve = .init(name: .default) | |
} | |
CATransaction.begin() | |
CATransaction.setAnimationDuration(CFTimeInterval(self.clusterManager.animationDuration)) | |
CATransaction.setAnimationTimingFunction(curve) | |
CATransaction.setCompletionBlock { | |
completionBlock(true) | |
} | |
animationsBlock() | |
CATransaction.commit() | |
} | |
} | |
public func select(_ cluster: CKCluster, animated: Bool) { | |
let marker = self.markers.object(forKey: cluster) | |
if marker !== self.selectedMarker { | |
marker?.map = self | |
self.selectedMarker = marker | |
} | |
} | |
public func deselect(_ cluster: CKCluster, animated: Bool) { | |
let marker = self.markers.object(forKey: cluster) | |
if marker === self.selectedMarker { | |
self.selectedMarker = nil | |
} | |
} | |
} | |
/** | |
GMSCameraUpdate for modifying the camera to show the content of a cluster. | |
*/ | |
extension GMSCameraUpdate { | |
/** | |
Returns a GMSCameraUpdate that transforms the camera such that the specified cluster are centered on screen at the greatest possible zoom level. The bounds will have a default padding of 64 points. | |
The returned camera update will set the camera's bearing and tilt to their default zero values (i.e., facing north and looking directly at the Earth). | |
@param cluster The cluster to fit. | |
@return The camera update that fit the given cluster. | |
*/ | |
static func fit(_ cluster: CKCluster) -> GMSCameraUpdate { | |
return self.fit(cluster, withPadding: 64) | |
} | |
/** | |
This is similar to fitCluster: but allows specifying the padding (in points) in order to inset the bounding box from the view's edges. | |
@param cluster The cluster to fit. | |
@param padding The padding that inset the bounding box. If the requested padding is larger than the view size in either the vertical or horizontal direction the map will be maximally zoomed out. | |
@return The camera update that fit the given cluster. | |
*/ | |
static func fit(_ cluster: CKCluster, withPadding padding: CGFloat) -> GMSCameraUpdate { | |
let edgeInsets = UIEdgeInsets(top: padding, left: padding, bottom: padding, right: padding) | |
return self.fit(cluster, with: edgeInsets) | |
} | |
/** | |
This is similar to fitCluster: but allows specifying edge insets in order to inset the bounding box from the view's edges. | |
@param cluster The cluster to fit. | |
@param edgeInsets The edge insets of the bounding box. If the requested edge insets are larger than the view size in either the vertical or horizontal direction the map will be maximally zoomed out. | |
@return The camera update that fit the given cluster. | |
*/ | |
static func fit(_ cluster: CKCluster, with edgeInsets: UIEdgeInsets) -> GMSCameraUpdate { | |
let bounds = GMSCoordinateBounds(coordinate: cluster.coordinate, coordinate: cluster.coordinate) | |
for i in 0..<cluster.count { | |
let marker = cluster[i] | |
bounds.includingCoordinate(marker.coordinate) | |
} | |
return self.fit(bounds, with: edgeInsets) | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment