From f61bb8ef126679ccdfeb4bca8d3e079ad86f33bd Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Tue, 17 May 2022 17:28:56 +0300 Subject: [PATCH] feat: add smooth CameraUpdate actions for supported maps --- CHANGELOG.md | 4 + LeadKit.podspec | 2 +- .../AppleClusterPlacemarkManager.swift | 19 +---- .../Sources/CameraUpdate/MKCameraUpdate.swift | 74 +++++++++++++++++ .../MKMapRect+FromCoordinates.swift | 43 ++++++++++ .../Extensions/MKMapView+ZoomLevel.swift | 42 ++++++++++ .../NSObject+ProtocolInspection.swift | 0 TIAppleMapUtils/TIAppleMapUtils.podspec | 2 +- TIFoundationUtils/TIFoundationUtils.podspec | 2 +- .../GMSCameraUpdate+CameraUpdate.swift | 57 +++++++++++++ .../GoogleClusterPlacemarkManager.swift | 12 +-- TIGoogleMapUtils/TIGoogleMapUtils.podspec | 2 +- TIKeychainUtils/TIKeychainUtils.podspec | 2 +- .../Sources/CameraUpdate/CameraUpdate.swift | 30 +++++++ .../CameraUpdate/CameraUpdateAction.swift | 30 +++++++ .../CameraUpdate/CameraUpdateFactory.swift | 29 +++++++ .../CLLocationCoordinate2D+Hashable.swift | 35 ++++++++ .../Sources/Helpers/CommonZoomLevels.swift | 35 ++++++++ .../CLCoordinateBounds.swift} | 22 ++--- .../CoordinateBounds+Extensions.swift | 52 ++++++++++++ .../CoordinateBounds/CoordinateBounds.swift | 30 +++++++ .../CoordinateBounds/LocationCoordinate.swift | 30 +++++++ .../Managers/BasePlacemarkManager.swift | 4 +- TIMapUtils/TIMapUtils.podspec | 2 +- TIMoyaNetworking/TIMoyaNetworking.podspec | 2 +- TINetworking/TINetworking.podspec | 2 +- TINetworkingCache/TINetworkingCache.podspec | 2 +- TIPagination/TIPagination.podspec | 2 +- TISwiftUtils/TISwiftUtils.podspec | 2 +- TITableKitUtils/TITableKitUtils.podspec | 2 +- TITransitions/TITransitions.podspec | 2 +- TIUIElements/TIUIElements.podspec | 2 +- TIUIKitCore/TIUIKitCore.podspec | 2 +- .../CLLocationCoordinate2D+YMKPoint.swift | 30 +++++++ .../Extensions/UIEdgeInsets+Operators.swift | 39 +++++++++ .../YMKBoundingBox+CoordinateBounds.swift | 32 ++++++++ .../YMKScreenRect+CameraPositioning.swift | 75 +++++++++++++++++ .../YMKScreenRect+FromCoordinates.swift | 42 ++++++++++ .../Sources/YMKCameraUpdate.swift | 82 +++++++++++++++++++ .../YandexClusterPlacemarkManager.swift | 47 +++++------ TIYandexMapUtils/TIYandexMapUtils.podspec | 2 +- 41 files changed, 843 insertions(+), 84 deletions(-) create mode 100644 TIAppleMapUtils/Sources/CameraUpdate/MKCameraUpdate.swift create mode 100644 TIAppleMapUtils/Sources/Extensions/MKMapRect+FromCoordinates.swift create mode 100644 TIAppleMapUtils/Sources/Extensions/MKMapView+ZoomLevel.swift rename TIAppleMapUtils/Sources/{ => Extensions}/NSObject+ProtocolInspection.swift (100%) create mode 100644 TIGoogleMapUtils/Sources/CameraUpdate/GMSCameraUpdate+CameraUpdate.swift create mode 100644 TIMapUtils/Sources/CameraUpdate/CameraUpdate.swift create mode 100644 TIMapUtils/Sources/CameraUpdate/CameraUpdateAction.swift create mode 100644 TIMapUtils/Sources/CameraUpdate/CameraUpdateFactory.swift create mode 100644 TIMapUtils/Sources/Extensions/CLLocationCoordinate2D+Hashable.swift create mode 100644 TIMapUtils/Sources/Helpers/CommonZoomLevels.swift rename TIMapUtils/Sources/Helpers/{CoordinateBounds.swift => CoordinateBounds/CLCoordinateBounds.swift} (59%) create mode 100644 TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds+Extensions.swift create mode 100644 TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds.swift create mode 100644 TIMapUtils/Sources/Helpers/CoordinateBounds/LocationCoordinate.swift create mode 100644 TIYandexMapUtils/Sources/Extensions/CLLocationCoordinate2D+YMKPoint.swift create mode 100644 TIYandexMapUtils/Sources/Extensions/UIEdgeInsets+Operators.swift create mode 100644 TIYandexMapUtils/Sources/Extensions/YMKBoundingBox+CoordinateBounds.swift create mode 100644 TIYandexMapUtils/Sources/Extensions/YMKScreenRect+CameraPositioning.swift create mode 100644 TIYandexMapUtils/Sources/Extensions/YMKScreenRect+FromCoordinates.swift create mode 100644 TIYandexMapUtils/Sources/YMKCameraUpdate.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index 7205a82e..ff83466b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/LeadKit.podspec b/LeadKit.podspec index 9913d47e..24866df6 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -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" diff --git a/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift b/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift index 632de13f..a730f0c3 100644 --- a/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift +++ b/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift @@ -25,7 +25,7 @@ import TIMapUtils import MapKit import UIKit -open class AppleClusterPlacemarkManager: BasePlacemarkManager], MKCoordinateRegion>, MKMapViewDelegate { +open class AppleClusterPlacemarkManager: BasePlacemarkManager], MKMapRect>, MKMapViewDelegate { public weak var mapViewDelegate: MKMapViewDelegate? private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self) @@ -139,20 +139,3 @@ open class AppleClusterPlacemarkManager: BasePlacemarkManager 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) - } -} diff --git a/TIAppleMapUtils/Sources/CameraUpdate/MKCameraUpdate.swift b/TIAppleMapUtils/Sources/CameraUpdate/MKCameraUpdate.swift new file mode 100644 index 00000000..0e151d28 --- /dev/null +++ b/TIAppleMapUtils/Sources/CameraUpdate/MKCameraUpdate.swift @@ -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 + + public init(action: CameraUpdateAction) { + self.action = action + } + + // MARK: - CameraUpdateFactory + + public static func update(for action: CameraUpdateAction) -> 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) + } + } +} diff --git a/TIAppleMapUtils/Sources/Extensions/MKMapRect+FromCoordinates.swift b/TIAppleMapUtils/Sources/Extensions/MKMapRect+FromCoordinates.swift new file mode 100644 index 00000000..f23e520c --- /dev/null +++ b/TIAppleMapUtils/Sources/Extensions/MKMapRect+FromCoordinates.swift @@ -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(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) + } +} diff --git a/TIAppleMapUtils/Sources/Extensions/MKMapView+ZoomLevel.swift b/TIAppleMapUtils/Sources/Extensions/MKMapView+ZoomLevel.swift new file mode 100644 index 00000000..a11311a7 --- /dev/null +++ b/TIAppleMapUtils/Sources/Extensions/MKMapView+ZoomLevel.swift @@ -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 + } +} diff --git a/TIAppleMapUtils/Sources/NSObject+ProtocolInspection.swift b/TIAppleMapUtils/Sources/Extensions/NSObject+ProtocolInspection.swift similarity index 100% rename from TIAppleMapUtils/Sources/NSObject+ProtocolInspection.swift rename to TIAppleMapUtils/Sources/Extensions/NSObject+ProtocolInspection.swift diff --git a/TIAppleMapUtils/TIAppleMapUtils.podspec b/TIAppleMapUtils/TIAppleMapUtils.podspec index 1fd7c643..a7ad5b8b 100644 --- a/TIAppleMapUtils/TIAppleMapUtils.podspec +++ b/TIAppleMapUtils/TIAppleMapUtils.podspec @@ -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' } diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 0cec98ac..88d4998c 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -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' } diff --git a/TIGoogleMapUtils/Sources/CameraUpdate/GMSCameraUpdate+CameraUpdate.swift b/TIGoogleMapUtils/Sources/CameraUpdate/GMSCameraUpdate+CameraUpdate.swift new file mode 100644 index 00000000..af1a175b --- /dev/null +++ b/TIGoogleMapUtils/Sources/CameraUpdate/GMSCameraUpdate+CameraUpdate.swift @@ -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) -> 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() + } +} diff --git a/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift b/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift index 525d77c9..76c91f29 100644 --- a/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift +++ b/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift @@ -110,11 +110,7 @@ open class GoogleClusterPlacemarkManager: BasePlacemarkManager Bool { @@ -131,3 +127,9 @@ open class GoogleClusterPlacemarkManager: BasePlacemarkManager Self { + .init(coordinate: southWest, coordinate: northEast) + } +} diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec index 9ed210ce..bf816134 100644 --- a/TIGoogleMapUtils/TIGoogleMapUtils.podspec +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -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' } diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 2d9d1ea8..59e1c6d3 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -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' } diff --git a/TIMapUtils/Sources/CameraUpdate/CameraUpdate.swift b/TIMapUtils/Sources/CameraUpdate/CameraUpdate.swift new file mode 100644 index 00000000..6870b98e --- /dev/null +++ b/TIMapUtils/Sources/CameraUpdate/CameraUpdate.swift @@ -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) +} diff --git a/TIMapUtils/Sources/CameraUpdate/CameraUpdateAction.swift b/TIMapUtils/Sources/CameraUpdate/CameraUpdateAction.swift new file mode 100644 index 00000000..7c68f911 --- /dev/null +++ b/TIMapUtils/Sources/CameraUpdate/CameraUpdateAction.swift @@ -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 { + case focus(target: Location, zoom: Float) + case fit(bounds: BoundingBox, insets: UIEdgeInsets) + case zoomIn + case zoomOut +} diff --git a/TIMapUtils/Sources/CameraUpdate/CameraUpdateFactory.swift b/TIMapUtils/Sources/CameraUpdate/CameraUpdateFactory.swift new file mode 100644 index 00000000..c9f3a10e --- /dev/null +++ b/TIMapUtils/Sources/CameraUpdate/CameraUpdateFactory.swift @@ -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) -> Update +} diff --git a/TIMapUtils/Sources/Extensions/CLLocationCoordinate2D+Hashable.swift b/TIMapUtils/Sources/Extensions/CLLocationCoordinate2D+Hashable.swift new file mode 100644 index 00000000..f7a0fe8e --- /dev/null +++ b/TIMapUtils/Sources/Extensions/CLLocationCoordinate2D+Hashable.swift @@ -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) + } +} diff --git a/TIMapUtils/Sources/Helpers/CommonZoomLevels.swift b/TIMapUtils/Sources/Helpers/CommonZoomLevels.swift new file mode 100644 index 00000000..99cdef52 --- /dev/null +++ b/TIMapUtils/Sources/Helpers/CommonZoomLevels.swift @@ -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) + } +} diff --git a/TIMapUtils/Sources/Helpers/CoordinateBounds.swift b/TIMapUtils/Sources/Helpers/CoordinateBounds/CLCoordinateBounds.swift similarity index 59% rename from TIMapUtils/Sources/Helpers/CoordinateBounds.swift rename to TIMapUtils/Sources/Helpers/CoordinateBounds/CLCoordinateBounds.swift index cb9bc88e..4f0b9ad1 100644 --- a/TIMapUtils/Sources/Helpers/CoordinateBounds.swift +++ b/TIMapUtils/Sources/Helpers/CoordinateBounds/CLCoordinateBounds.swift @@ -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(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 {} diff --git a/TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds+Extensions.swift b/TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds+Extensions.swift new file mode 100644 index 00000000..26f37aa8 --- /dev/null +++ b/TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds+Extensions.swift @@ -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(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) + } +} diff --git a/TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds.swift b/TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds.swift new file mode 100644 index 00000000..987ae46c --- /dev/null +++ b/TIMapUtils/Sources/Helpers/CoordinateBounds/CoordinateBounds.swift @@ -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 +} diff --git a/TIMapUtils/Sources/Helpers/CoordinateBounds/LocationCoordinate.swift b/TIMapUtils/Sources/Helpers/CoordinateBounds/LocationCoordinate.swift new file mode 100644 index 00000000..6a7824ed --- /dev/null +++ b/TIMapUtils/Sources/Helpers/CoordinateBounds/LocationCoordinate.swift @@ -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) +} diff --git a/TIMapUtils/Sources/Managers/BasePlacemarkManager.swift b/TIMapUtils/Sources/Managers/BasePlacemarkManager.swift index d8fb4aa4..fec2fe65 100644 --- a/TIMapUtils/Sources/Managers/BasePlacemarkManager.swift +++ b/TIMapUtils/Sources/Managers/BasePlacemarkManager.swift @@ -22,8 +22,8 @@ import UIKit -open class BasePlacemarkManager: NSObject, PlacemarkManager { - public typealias TapHandlerClosure = (Model, Position) -> Bool +open class BasePlacemarkManager: NSObject, PlacemarkManager { + public typealias TapHandlerClosure = (Model, Coordinate) -> Bool public typealias IconProviderClosure = (Model) -> UIImage public var tapHandler: TapHandlerClosure? diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec index 24a9a433..1a9a80ca 100644 --- a/TIMapUtils/TIMapUtils.podspec +++ b/TIMapUtils/TIMapUtils.podspec @@ -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' } diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 70683ca6..7ac2397b 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -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' } diff --git a/TINetworking/TINetworking.podspec b/TINetworking/TINetworking.podspec index 90b5df02..fc5d95b1 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -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' } diff --git a/TINetworkingCache/TINetworkingCache.podspec b/TINetworkingCache/TINetworkingCache.podspec index 6e402ab6..14f225d6 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -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' } diff --git a/TIPagination/TIPagination.podspec b/TIPagination/TIPagination.podspec index ce88fa8d..1a56d913 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -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' } diff --git a/TISwiftUtils/TISwiftUtils.podspec b/TISwiftUtils/TISwiftUtils.podspec index 41efca2c..96adeb75 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -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' } diff --git a/TITableKitUtils/TITableKitUtils.podspec b/TITableKitUtils/TITableKitUtils.podspec index a8759346..d194092f 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -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' } diff --git a/TITransitions/TITransitions.podspec b/TITransitions/TITransitions.podspec index 9dda51d9..7419fa20 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -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' } diff --git a/TIUIElements/TIUIElements.podspec b/TIUIElements/TIUIElements.podspec index faa3f4ad..29e5575f 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -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' } diff --git a/TIUIKitCore/TIUIKitCore.podspec b/TIUIKitCore/TIUIKitCore.podspec index edfb31aa..d7e12924 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -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' } diff --git a/TIYandexMapUtils/Sources/Extensions/CLLocationCoordinate2D+YMKPoint.swift b/TIYandexMapUtils/Sources/Extensions/CLLocationCoordinate2D+YMKPoint.swift new file mode 100644 index 00000000..31457443 --- /dev/null +++ b/TIYandexMapUtils/Sources/Extensions/CLLocationCoordinate2D+YMKPoint.swift @@ -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) + } +} diff --git a/TIYandexMapUtils/Sources/Extensions/UIEdgeInsets+Operators.swift b/TIYandexMapUtils/Sources/Extensions/UIEdgeInsets+Operators.swift new file mode 100644 index 00000000..7b5e880b --- /dev/null +++ b/TIYandexMapUtils/Sources/Extensions/UIEdgeInsets+Operators.swift @@ -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) + } +} diff --git a/TIYandexMapUtils/Sources/Extensions/YMKBoundingBox+CoordinateBounds.swift b/TIYandexMapUtils/Sources/Extensions/YMKBoundingBox+CoordinateBounds.swift new file mode 100644 index 00000000..a9c81c09 --- /dev/null +++ b/TIYandexMapUtils/Sources/Extensions/YMKBoundingBox+CoordinateBounds.swift @@ -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) + } +} diff --git a/TIYandexMapUtils/Sources/Extensions/YMKScreenRect+CameraPositioning.swift b/TIYandexMapUtils/Sources/Extensions/YMKScreenRect+CameraPositioning.swift new file mode 100644 index 00000000..c55a3140 --- /dev/null +++ b/TIYandexMapUtils/Sources/Extensions/YMKScreenRect+CameraPositioning.swift @@ -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 + } +} diff --git a/TIYandexMapUtils/Sources/Extensions/YMKScreenRect+FromCoordinates.swift b/TIYandexMapUtils/Sources/Extensions/YMKScreenRect+FromCoordinates.swift new file mode 100644 index 00000000..6bf86fca --- /dev/null +++ b/TIYandexMapUtils/Sources/Extensions/YMKScreenRect+FromCoordinates.swift @@ -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(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) + } +} diff --git a/TIYandexMapUtils/Sources/YMKCameraUpdate.swift b/TIYandexMapUtils/Sources/YMKCameraUpdate.swift new file mode 100644 index 00000000..6c900092 --- /dev/null +++ b/TIYandexMapUtils/Sources/YMKCameraUpdate.swift @@ -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 + + public var animationType = YMKAnimation(type: .smooth, duration: 0.5) + + public init(action: CameraUpdateAction) { + self.action = action + } + + // MARK: - CameraUpdateFactory + + public static func update(for action: CameraUpdateAction) -> YMKCameraUpdate { + .init(action: action) + } + + open func cameraPosition(for action: CameraUpdateAction, + 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) + } +} diff --git a/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift b/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift index af3078f6..5e2ba8e7 100644 --- a/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift +++ b/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift @@ -23,9 +23,11 @@ import TIMapUtils import YandexMapsMobile import UIKit +import CoreLocation -open class YandexClusterPlacemarkManager: BasePlacemarkManager], YMKBoundingBox>, YMKClusterListener, YMKClusterTapListener { - public var placemarksMapping: Zip2Sequence<[YMKPlacemarkMapObject], [YandexPlacemarkManager]>? +open class YandexClusterPlacemarkManager: BasePlacemarkManager], [YMKPoint]>, YMKClusterListener, YMKClusterTapListener { + + public var placemarksMapping: [CLLocationCoordinate2D: [YandexPlacemarkManager]]? public init(placemarkManagers: [YandexPlacemarkManager], iconProvider: @escaping IconProviderClosure, @@ -45,14 +47,23 @@ open class YandexClusterPlacemarkManager: BasePlacemarkManager: BasePlacemarkManager [YandexPlacemarkManager] { - 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: BasePlacemarkManager 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))) - - } - } -} diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec index fad94529..a1f8edf1 100644 --- a/TIYandexMapUtils/TIYandexMapUtils.podspec +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -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' }