feat: add smooth CameraUpdate actions for supported maps

This commit is contained in:
Ivan Smolin 2022-05-17 17:28:56 +03:00
parent 23b9e8ac7a
commit f61bb8ef12
41 changed files with 843 additions and 84 deletions

View File

@ -1,5 +1,9 @@
# Changelog
### 1.17.0
- **Add**: add smooth CameraUpdate actions for supported maps
### 1.16.2
- **Update**: `DefaultRecoverableJsonNetworkService` now supports error forwarding from its error handlers to initial requests.

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "LeadKit"
s.version = "1.16.2"
s.version = "1.17.0"
s.summary = "iOS framework with a bunch of tools for rapid development"
s.homepage = "https://github.com/TouchInstinct/LeadKit"
s.license = "Apache License, Version 2.0"

View File

@ -25,7 +25,7 @@ import TIMapUtils
import MapKit
import UIKit
open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, [ApplePlacemarkManager<Model>], MKCoordinateRegion>, MKMapViewDelegate {
open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotationView, [ApplePlacemarkManager<Model>], MKMapRect>, MKMapViewDelegate {
public weak var mapViewDelegate: MKMapViewDelegate?
private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self)
@ -139,20 +139,3 @@ open class AppleClusterPlacemarkManager<Model>: BasePlacemarkManager<MKAnnotatio
return mapViewDelegate
}
}
private extension MKCoordinateRegion {
static func from(coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion {
guard !coordinates.isEmpty else {
return MKCoordinateRegion()
}
let bbox = CoordinateBounds.from(coordinates: coordinates)
let span = MKCoordinateSpan(latitudeDelta: bbox.northEast.latitude - bbox.southWest.latitude,
longitudeDelta: bbox.northEast.longitude - bbox.southWest.longitude)
let center = CLLocationCoordinate2D(latitude: bbox.northEast.latitude - (span.latitudeDelta / 2),
longitude: bbox.southWest.longitude + (span.longitudeDelta / 2))
return MKCoordinateRegion(center: center, span: span)
}
}

View File

@ -0,0 +1,74 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import MapKit
import TIMapUtils
open class MKCameraUpdate: CameraUpdate, CameraUpdateFactory {
private let action: CameraUpdateAction<CLLocationCoordinate2D, MKMapRect>
public init(action: CameraUpdateAction<CLLocationCoordinate2D, MKMapRect>) {
self.action = action
}
// MARK: - CameraUpdateFactory
public static func update(for action: CameraUpdateAction<CLLocationCoordinate2D, MKMapRect>) -> MKCameraUpdate {
.init(action: action)
}
// MARK: - CameraUpdate
private func update(map: MKMapView, animated: Bool = true) {
switch action {
case let .focus(target, zoom):
map.set(zoomLevel: zoom,
at: target,
animated: animated)
case let .fit(bounds, insets):
map.setVisibleMapRect(bounds,
edgePadding: insets,
animated: animated)
case .zoomIn:
map.set(zoomLevel: map.zoomLevel + 1,
at: map.centerCoordinate,
animated: animated)
case .zoomOut:
map.set(zoomLevel: map.zoomLevel - 1,
at: map.centerCoordinate,
animated: animated)
}
}
public func update(map: MKMapView) {
update(map: map, animated: false)
}
public func update(map: MKMapView, animationDuration: TimeInterval) {
UIView.animate(withDuration: animationDuration) {
self.update(map: map, animated: true)
}
}
}

View File

@ -0,0 +1,43 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import MapKit
import TIMapUtils
public extension MKMapRect {
static func from<C: Collection>(coordinates: C) -> MKMapRect where C.Element == CLLocationCoordinate2D {
guard let bbox = CLCoordinateBounds.from(coordinates: coordinates) else {
return MKMapRect.null
}
let southWest = MKMapPoint(bbox.southWest)
let northEast = MKMapPoint(bbox.northEast)
let origin = MKMapPoint(x: southWest.x,
y: northEast.y)
let size = MKMapSize(width: northEast.x - southWest.x,
height: southWest.y - northEast.y)
return MKMapRect(origin: origin, size: size)
}
}

View File

@ -0,0 +1,42 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import MapKit
// https://stackoverflow.com/a/15020534
public extension MKMapView {
var zoomLevel: Float {
Float(log2(360 * ((frame.size.width / .expectedMapViewTileSize) / region.span.longitudeDelta))) + 1
}
func set(zoomLevel: Float, at centerCoordinate: CLLocationCoordinate2D, animated: Bool = true) {
let span = MKCoordinateSpan(latitudeDelta: 0,
longitudeDelta: 360 / pow(2, Double(zoomLevel)) * frame.size.width / .expectedMapViewTileSize)
setRegion(MKCoordinateRegion(center: centerCoordinate, span: span), animated: animated)
}
}
private extension CGFloat {
static var expectedMapViewTileSize: CGFloat {
256
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIAppleMapUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Apple MapKit.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIFoundationUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for Foundation framework classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,57 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import GoogleMapsUtils
import TIMapUtils
extension GMSCameraUpdate: CameraUpdateFactory, CameraUpdate {
// MARK: - CameraUpdateFactory
public static func update(for action: CameraUpdateAction<CLLocationCoordinate2D, GMSCoordinateBounds>) -> GMSCameraUpdate {
switch action {
case let .focus(target, zoom):
return .setTarget(target, zoom: zoom)
case let .fit(bounds, insets):
return .fit(bounds, with: insets)
case .zoomIn:
return .zoomIn()
case .zoomOut:
return .zoomOut()
}
}
// MARK: - CameraUpdate
public func update(map: GMSMapView) {
map.moveCamera(self)
}
public func update(map: GMSMapView, animationDuration: TimeInterval) {
CATransaction.begin()
CATransaction.setValue(animationDuration, forKey: kCATransactionAnimationDuration)
map.animate(with: self)
CATransaction.commit()
}
}

View File

@ -110,11 +110,7 @@ open class GoogleClusterPlacemarkManager<Model>: BasePlacemarkManager<GMSMarker,
return false
}
let bounds = placemarkManagers.reduce(GMSCoordinateBounds()) {
$0.includingCoordinate($1.position)
}
return tapHandler?(placemarkManagers, bounds) ?? false
return tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.position }) ?? .init()) ?? false
}
open func clusterManager(_ clusterManager: GMUClusterManager, didTap clusterItem: GMUClusterItem) -> Bool {
@ -131,3 +127,9 @@ open class GoogleClusterPlacemarkManager<Model>: BasePlacemarkManager<GMSMarker,
nil // icon generation will be performed in GMUClusterRendererDelegate callback
}
}
extension GMSCoordinateBounds: CoordinateBounds {
public static func of(southWest: CLLocationCoordinate2D, northEast: CLLocationCoordinate2D) -> Self {
.init(coordinate: southWest, coordinate: northEast)
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIGoogleMapUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Google Maps SDK.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIKeychainUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for Keychain classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import Foundation
public protocol CameraUpdate {
associatedtype Map
func update(map: Map)
func update(map: Map, animationDuration: TimeInterval)
}

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit.UIGeometry
public enum CameraUpdateAction<Location, BoundingBox> {
case focus(target: Location, zoom: Float)
case fit(bounds: BoundingBox, insets: UIEdgeInsets)
case zoomIn
case zoomOut
}

View File

@ -0,0 +1,29 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
public protocol CameraUpdateFactory {
associatedtype Position
associatedtype BoundingBox
associatedtype Update: CameraUpdate
static func update(for action: CameraUpdateAction<Position, BoundingBox>) -> Update
}

View File

@ -0,0 +1,35 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import CoreLocation
extension CLLocationCoordinate2D: Hashable {
public static func == (lhs: CLLocationCoordinate2D, rhs: CLLocationCoordinate2D) -> Bool {
rhs.latitude == lhs.latitude &&
rhs.longitude == lhs.longitude
}
public func hash(into hasher: inout Hasher) {
hasher.combine(latitude)
hasher.combine(longitude)
}
}

View File

@ -0,0 +1,35 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// https://wiki.openstreetmap.org/wiki/Zoom_levels
public enum CommonZoomLevels: UInt {
case largestCountry = 3 // whole Russia (in horizontal orientation)
case largeMetropolianArea = 9 // Moscow with suburb
case metropolianArea = 10 // Saint-Petersburg with suburb
case city = 11 // regional city or metropolian center
case street = 16
case building = 18
public var floatValue: Float {
Float(rawValue)
}
}

View File

@ -22,7 +22,7 @@
import CoreLocation
public struct CoordinateBounds {
public struct CLCoordinateBounds: CoordinateBounds {
public let southWest: CLLocationCoordinate2D
public let northEast: CLLocationCoordinate2D
@ -30,22 +30,10 @@ public struct CoordinateBounds {
self.southWest = southWest
self.northEast = northEast
}
}
public extension CoordinateBounds {
static func from<C: Collection>(coordinates: C) -> CoordinateBounds where C.Element == CLLocationCoordinate2D {
guard let first = coordinates.first else {
return .init(southWest: CLLocationCoordinate2D(),
northEast: CLLocationCoordinate2D())
}
let initialBox = CoordinateBounds(southWest: first, northEast: first)
return coordinates.dropFirst().reduce(initialBox) {
CoordinateBounds(southWest: CLLocationCoordinate2D(latitude: min($0.southWest.latitude, $1.latitude),
longitude: min($0.southWest.longitude, $1.longitude)),
northEast: CLLocationCoordinate2D(latitude: max($0.northEast.latitude, $1.latitude),
longitude: max($0.northEast.longitude, $1.longitude)))
}
public static func of(southWest: CLLocationCoordinate2D, northEast: CLLocationCoordinate2D) -> CLCoordinateBounds {
.init(southWest: southWest, northEast: northEast)
}
}
extension CLLocationCoordinate2D: LocationCoordinate {}

View File

@ -0,0 +1,52 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import CoreLocation
public extension CoordinateBounds {
static func from<C: Collection>(coordinates: C) -> Self? where C.Element == Coordinate {
guard !coordinates.isEmpty else {
return nil
}
var (northEastLon, northEastLat) = (-CLLocationDegrees.infinity, -CLLocationDegrees.infinity)
var (southWestLon, southWestLat) = (CLLocationDegrees.infinity, CLLocationDegrees.infinity)
for coordinate in coordinates {
southWestLon = min(southWestLon, coordinate.longitude)
northEastLon = max(northEastLon, coordinate.longitude)
southWestLat = min(southWestLat, coordinate.latitude)
northEastLat = max(northEastLat, coordinate.latitude)
}
return .of(southWest: Coordinate(latitude: southWestLat, longitude: southWestLon),
northEast: Coordinate(latitude: northEastLat, longitude: northEastLon))
}
var topLeft: Coordinate {
Coordinate(latitude: northEast.latitude, longitude: southWest.longitude)
}
var bottomRight: Coordinate {
Coordinate(latitude: southWest.latitude, longitude: northEast.longitude)
}
}

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
public protocol CoordinateBounds {
associatedtype Coordinate: LocationCoordinate
var southWest: Coordinate { get }
var northEast: Coordinate { get }
static func of(southWest: Coordinate, northEast: Coordinate) -> Self
}

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import CoreLocation
public protocol LocationCoordinate {
var latitude: CLLocationDegrees { get }
var longitude: CLLocationDegrees { get }
init(latitude: CLLocationDegrees, longitude: CLLocationDegrees)
}

View File

@ -22,8 +22,8 @@
import UIKit
open class BasePlacemarkManager<Placemark, Model, Position>: NSObject, PlacemarkManager {
public typealias TapHandlerClosure = (Model, Position) -> Bool
open class BasePlacemarkManager<Placemark, Model, Coordinate>: NSObject, PlacemarkManager {
public typealias TapHandlerClosure = (Model, Coordinate) -> Bool
public typealias IconProviderClosure = (Model) -> UIImage
public var tapHandler: TapHandlerClosure?

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMapUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for map objects clustering and interacting.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIMoyaNetworking'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Moya + Swagger network service.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworking'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Swagger-frendly networking layer helpers.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TINetworkingCache'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Caching results of EndpointRequests.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIPagination'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Generic pagination component.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TISwiftUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Bunch of useful helpers for Swift development.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITableKitUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for TableKit classes.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TITransitions'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of custom transitions to present controller. '
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIElements'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Bunch of useful protocols and views.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIUIKitCore'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Core UI elements: protocols, views and helpers.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }

View File

@ -0,0 +1,30 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import CoreLocation
import YandexMapsMobile
public extension CLLocationCoordinate2D {
init(ymkPoint: YMKPoint) {
self.init(latitude: ymkPoint.latitude, longitude: ymkPoint.longitude)
}
}

View File

@ -0,0 +1,39 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import UIKit.UIGeometry
public extension UIEdgeInsets {
static func +(lhs: UIEdgeInsets, rhs: UIEdgeInsets) -> UIEdgeInsets {
UIEdgeInsets(top: lhs.top + rhs.top,
left: lhs.left + rhs.left,
bottom: lhs.bottom + rhs.bottom,
right: lhs.right + rhs.right)
}
static func *(lhs: UIEdgeInsets, rhs: CGFloat) -> UIEdgeInsets {
UIEdgeInsets(top: lhs.top * rhs,
left: lhs.left * rhs,
bottom: lhs.bottom * rhs,
right: lhs.right * rhs)
}
}

View File

@ -0,0 +1,32 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import YandexMapsMobile
import TIMapUtils
extension YMKPoint: LocationCoordinate {}
extension YMKBoundingBox: CoordinateBounds {
public static func of(southWest: YMKPoint, northEast: YMKPoint) -> Self {
.init(southWest: southWest, northEast: northEast)
}
}

View File

@ -0,0 +1,75 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import YandexMapsMobile
public extension YMKScreenRect {
func cameraPosition(in mapView: YMKMapView, insets: UIEdgeInsets) -> YMKCameraPosition? {
let mapWindow: YMKMapWindow = mapView.mapWindow // mapWindow is IUO
let center = YMKScreenPoint(x: (topLeft.x + bottomRight.x) / 2,
y: (topLeft.y + bottomRight.y) / 2)
guard let coordinatePoint = mapWindow.screenToWorld(with: center) else {
return nil
}
let mapInsets: UIEdgeInsets
if #available(iOS 11.0, *) {
mapInsets = (insets + mapView.safeAreaInsets) * CGFloat(mapWindow.scaleFactor)
} else {
mapInsets = insets * CGFloat(mapWindow.scaleFactor)
}
let size = CGSize(width: mapWindow.width(), height: mapWindow.height())
guard let minScale = minScale(for: size, insets: mapInsets) else {
return nil
}
let map = mapWindow.map
let newZoom = map.cameraPosition.zoom + log2(minScale)
let minZoom = map.getMinZoom()
let maxZoom = map.getMaxZoom()
return YMKCameraPosition(target: coordinatePoint,
zoom: min(max(newZoom, minZoom), maxZoom),
azimuth: 0,
tilt: 0)
}
private func minScale(for mapSize: CGSize, insets: UIEdgeInsets) -> Float? {
let width = CGFloat(abs(bottomRight.x - topLeft.x))
let height = CGFloat(abs(topLeft.y - bottomRight.y))
if width > 0 || height > 0 {
let scaleX = mapSize.width / width - (insets.left + insets.right) / width
let scaleY = mapSize.height / height - (insets.top + insets.bottom) / height
return Float(min(scaleX, scaleY))
}
return nil
}
}

View File

@ -0,0 +1,42 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import YandexMapsMobile
public extension YMKScreenRect {
static func from<C: Collection>(coordinates: C,
in mapWindow: YMKMapWindow) -> YMKScreenRect? where C.Element == YMKPoint {
guard let bbox = YMKBoundingBox.from(coordinates: coordinates) else {
return nil
}
let toScreenPoint = mapWindow.worldToScreen
guard let topLeft = toScreenPoint(bbox.topLeft),
let bottomRight = toScreenPoint(bbox.bottomRight) else {
return nil
}
return YMKScreenRect(topLeft: topLeft, bottomRight: bottomRight)
}
}

View File

@ -0,0 +1,82 @@
//
// Copyright (c) 2022 Touch Instinct
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the Software), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED AS IS, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
import YandexMapsMobile
import TIMapUtils
open class YMKCameraUpdate: CameraUpdate, CameraUpdateFactory {
private let action: CameraUpdateAction<YMKPoint, [YMKPoint]>
public var animationType = YMKAnimation(type: .smooth, duration: 0.5)
public init(action: CameraUpdateAction<YMKPoint, [YMKPoint]>) {
self.action = action
}
// MARK: - CameraUpdateFactory
public static func update(for action: CameraUpdateAction<YMKPoint, [YMKPoint]>) -> YMKCameraUpdate {
.init(action: action)
}
open func cameraPosition(for action: CameraUpdateAction<YMKPoint, [YMKPoint]>,
in mapView: YMKMapView) -> YMKCameraPosition {
let cameraPosition = mapView.mapWindow.map.cameraPosition
switch action {
case let .focus(target, zoom):
return YMKCameraPosition(target: target,
zoom: zoom,
azimuth: 0,
tilt: 0)
case let .fit(bounds, insets):
return YMKScreenRect.from(coordinates: bounds, in: mapView.mapWindow)?
.cameraPosition(in: mapView, insets: insets) ?? cameraPosition
case .zoomIn:
return YMKCameraPosition(target: cameraPosition.target,
zoom: cameraPosition.zoom + 1,
azimuth: cameraPosition.azimuth,
tilt: cameraPosition.tilt)
case .zoomOut:
return YMKCameraPosition(target: cameraPosition.target,
zoom: cameraPosition.zoom - 1,
azimuth: cameraPosition.azimuth,
tilt: cameraPosition.tilt)
}
}
// MARK: - CameraUpdate
public func update(map: YMKMapView) {
map.mapWindow.map.move(with: cameraPosition(for: action, in: map))
}
public func update(map: YMKMapView, animationDuration: TimeInterval) {
map.mapWindow.map.move(with: cameraPosition(for: action, in: map),
animationType: YMKAnimation(type: .smooth, duration: Float(animationDuration)),
cameraCallback: nil)
}
}

View File

@ -23,9 +23,11 @@
import TIMapUtils
import YandexMapsMobile
import UIKit
import CoreLocation
open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster, [YandexPlacemarkManager<Model>], YMKBoundingBox>, YMKClusterListener, YMKClusterTapListener {
public var placemarksMapping: Zip2Sequence<[YMKPlacemarkMapObject], [YandexPlacemarkManager<Model>]>?
open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster, [YandexPlacemarkManager<Model>], [YMKPoint]>, YMKClusterListener, YMKClusterTapListener {
public var placemarksMapping: [CLLocationCoordinate2D: [YandexPlacemarkManager<Model>]]?
public init(placemarkManagers: [YandexPlacemarkManager<Model>],
iconProvider: @escaping IconProviderClosure,
@ -45,14 +47,23 @@ open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster
tapHandler: tapHandler)
}
open func addMarkers(to map: YMKMap, clusterRadius: Double = 60, minZoom: UInt = 15) {
open func addMarkers(to map: YMKMap,
clusterRadius: Double = 60,
minZoom: UInt = CommonZoomLevels.street.rawValue) {
let clusterizedPlacemarkCollection = map.mapObjects.addClusterizedPlacemarkCollection(with: self)
let emptyPlacemarks = clusterizedPlacemarkCollection.addEmptyPlacemarks(with: dataModel.map { $0.position })
self.placemarksMapping = zip(emptyPlacemarks, dataModel)
let placemarksZip = zip(emptyPlacemarks, dataModel)
placemarksMapping?.forEach { (placemark, manager) in
// [(coordinate, placemarkManager)]
let mappingSequence = placemarksZip.map { (CLLocationCoordinate2D(ymkPoint: $0.0.geometry), $0.1) }
// [coordinate: [placemarkManager]]
self.placemarksMapping = Dictionary(grouping: mappingSequence) { $0.0 }.mapValues { $0.map { $0.1 } }
placemarksZip.forEach { (placemark, manager) in
manager.configure(placemark: placemark)
}
@ -73,12 +84,14 @@ open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster
return false
}
return tapHandler(managers(in: cluster), .from(coordinates: managers(in: cluster).map { $0.position }))
let managersInCluster = managers(in: cluster)
return tapHandler(managersInCluster, managersInCluster.map { $0.position })
}
open func managers(in cluster: YMKCluster) -> [YandexPlacemarkManager<Model>] {
cluster.placemarks.compactMap { placemark in
placemarksMapping?.first { $0.0 == placemark }?.1
cluster.placemarks.flatMap {
placemarksMapping?[CLLocationCoordinate2D(ymkPoint: $0.geometry)] ?? []
}
}
@ -89,21 +102,3 @@ open class YandexClusterPlacemarkManager<Model>: BasePlacemarkManager<YMKCluster
placemark.appearance.setIconWith(iconProvider(managers(in: placemark)))
}
}
private extension YMKBoundingBox {
static func from(coordinates: [YMKPoint]) -> YMKBoundingBox {
guard let first = coordinates.first else {
return YMKBoundingBox()
}
let initialBox = YMKBoundingBox(southWest: first, northEast: first)
return coordinates.dropFirst().reduce(initialBox) {
YMKBoundingBox(southWest: YMKPoint(latitude: min($0.southWest.latitude, $1.latitude),
longitude: min($0.southWest.longitude, $1.longitude)),
northEast: YMKPoint(latitude: max($0.northEast.latitude, $1.latitude),
longitude: max($0.northEast.longitude, $1.longitude)))
}
}
}

View File

@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = 'TIYandexMapUtils'
s.version = '1.16.2'
s.version = '1.17.0'
s.summary = 'Set of helpers for map objects clustering and interacting using Yandex Maps SDK.'
s.homepage = 'https://github.com/TouchInstinct/LeadKit/tree/' + s.version.to_s + '/' + s.name
s.license = { :type => 'MIT', :file => 'LICENSE' }