From 47ff4d949cccc326af1761e80295ea254ea1ee06 Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Tue, 26 Apr 2022 11:30:44 +0300 Subject: [PATCH] feat: TIMapUtils, TIAppleMapUtils, TIGoogleMapUtils and TIYandexMapUtils modules for map items clustering and interacting with them --- CHANGELOG.md | 4 + LeadKit.podspec | 2 +- Package.swift | 10 + .../AppleClusterPlacemarkManager.swift | 158 ++++++++++++++++ .../Sources/ApplePlacemarkManager.swift | 66 +++++++ .../Sources/NSObject+ProtocolInspection.swift | 42 +++++ TIAppleMapUtils/TIAppleMapUtils.podspec | 16 ++ .../KeyedDecodingContainer+DateDecoding.swift | 14 +- TIFoundationUtils/TIFoundationUtils.podspec | 2 +- .../GoogleClusterPlacemarkManager.swift | 122 ++++++++++++ .../Sources/GooglePlacemarkManager.swift | 60 ++++++ TIGoogleMapUtils/TIGoogleMapUtils.podspec | 21 +++ TIKeychainUtils/TIKeychainUtils.podspec | 2 +- .../Helpers/CGGeometry+Extensions.swift | 43 +++++ .../Drawing/Helpers/CGSize+Resize.swift | 68 +++++++ .../Sources/Drawing/Helpers/ResizeMode.swift | 25 +++ .../Operations/BorderDrawingOperation.swift | 83 +++++++++ .../Operations/CALayerDrawingOperation.swift | 50 +++++ .../Drawing/Operations/DrawingOperation.swift | 28 +++ .../OrientationAwareDrawingOperation.swift | 57 ++++++ .../SolidFillDrawingOperation.swift | 78 ++++++++ .../Operations/TextDrawingOperation.swift | 74 ++++++++ .../TransformDrawingOperation.swift | 75 ++++++++ .../Sources/Helpers/CoordinateBounds.swift | 51 ++++++ .../DefaultCachableMarkerIconFactory.swift | 52 ++++++ .../DefaultClusterIconRenderer.swift | 173 ++++++++++++++++++ .../DefaultMarkerIconFactory.swift | 41 +++++ .../IconProviders/MarkerIconFactory.swift | 29 +++ .../Managers/BasePlacemarkManager.swift | 57 ++++++ .../Sources/Managers/PlacemarkManager.swift | 27 +++ TIMapUtils/TIMapUtils.podspec | 15 ++ 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 +- .../YandexClusterPlacemarkManager.swift | 109 +++++++++++ .../Sources/YandexPlacemarkManager.swift | 64 +++++++ TIYandexMapUtils/TIYandexMapUtils.podspec | 21 +++ project-scripts/push_to_podspecs.sh | 6 +- 44 files changed, 1743 insertions(+), 20 deletions(-) create mode 100644 TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift create mode 100644 TIAppleMapUtils/Sources/ApplePlacemarkManager.swift create mode 100644 TIAppleMapUtils/Sources/NSObject+ProtocolInspection.swift create mode 100644 TIAppleMapUtils/TIAppleMapUtils.podspec create mode 100644 TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift create mode 100644 TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift create mode 100644 TIGoogleMapUtils/TIGoogleMapUtils.podspec create mode 100644 TIMapUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift create mode 100644 TIMapUtils/Sources/Drawing/Helpers/CGSize+Resize.swift create mode 100644 TIMapUtils/Sources/Drawing/Helpers/ResizeMode.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/DrawingOperation.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Helpers/CoordinateBounds.swift create mode 100644 TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift create mode 100644 TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift create mode 100644 TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift create mode 100644 TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift create mode 100644 TIMapUtils/Sources/Managers/BasePlacemarkManager.swift create mode 100644 TIMapUtils/Sources/Managers/PlacemarkManager.swift create mode 100644 TIMapUtils/TIMapUtils.podspec create mode 100644 TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift create mode 100644 TIYandexMapUtils/Sources/YandexPlacemarkManager.swift create mode 100644 TIYandexMapUtils/TIYandexMapUtils.podspec diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d714ec1..7dca2d25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +### 1.16.0 + +- **Add**: `TIMapUtils`, `TIAppleMapUtils`, `TIGoogleMapUtils` and `TIYandexMapUtils` modules for map items clustering and interacting with them. + ### 1.15.0 - **Update**: Network services in TIMoyaNetworking now passes MoyaError in result of EnpointRequest execution. diff --git a/LeadKit.podspec b/LeadKit.podspec index df247480..e7b810d6 100644 --- a/LeadKit.podspec +++ b/LeadKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "LeadKit" - s.version = "1.15.0" + s.version = "1.16.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/Package.swift b/Package.swift index c824c208..7741cb14 100644 --- a/Package.swift +++ b/Package.swift @@ -23,6 +23,11 @@ let package = Package( .library(name: "TINetworking", targets: ["TINetworking"]), .library(name: "TIMoyaNetworking", targets: ["TIMoyaNetworking"]), .library(name: "TINetworkingCache", targets: ["TINetworkingCache"]), + + // MARK: - Maps + + .library(name: "TIMapUtils", targets: ["TIMapUtils"]), + .library(name: "TIAppleMapUtils", targets: ["TIAppleMapUtils"]), // MARK: - Elements .library(name: "OTPSwiftView", targets: ["OTPSwiftView"]), @@ -54,6 +59,11 @@ let package = Package( .target(name: "TINetworking", dependencies: ["TISwiftUtils", "Alamofire"], path: "TINetworking/Sources"), .target(name: "TIMoyaNetworking", dependencies: ["TINetworking", "TIFoundationUtils", "Moya"], path: "TIMoyaNetworking"), .target(name: "TINetworkingCache", dependencies: ["TIFoundationUtils", "TINetworking", "Cache"], path: "TINetworkingCache/Sources"), + + // MARK: - Maps + + .target(name: "TIMapUtils", dependencies: [], path: "TIMapUtils/Sources"), + .target(name: "TIAppleMapUtils", dependencies: ["TIMapUtils"], path: "TIAppleMapUtils/Sources"), // MARK: - Elements diff --git a/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift b/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift new file mode 100644 index 00000000..632de13f --- /dev/null +++ b/TIAppleMapUtils/Sources/AppleClusterPlacemarkManager.swift @@ -0,0 +1,158 @@ +// +// 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 TIMapUtils +import MapKit +import UIKit + +open class AppleClusterPlacemarkManager: BasePlacemarkManager], MKCoordinateRegion>, MKMapViewDelegate { + public weak var mapViewDelegate: MKMapViewDelegate? + + private let mapDelegateSelectors = NSObject.instanceMethodSelectors(of: MKMapViewDelegate.self) + + public init(placemarkManagers: [ApplePlacemarkManager], + mapViewDelegate: MKMapViewDelegate? = nil, + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + self.mapViewDelegate = mapViewDelegate + + super.init(dataModel: placemarkManagers, + iconProvider: iconProvider, + tapHandler: tapHandler) + } + + public convenience init(placemarkManagers: [ApplePlacemarkManager], + mapViewDelegate: MKMapViewDelegate? = nil, + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == [Model] { + + self.init(placemarkManagers: placemarkManagers, + mapViewDelegate: mapViewDelegate, + iconProvider: { iconFactory.markerIcon(for: $0.map { $0.dataModel }) }, + tapHandler: tapHandler) + } + + open func addMarkers(to map: MKMapView) { + map.delegate = self + map.addAnnotations(dataModel) + } + + // MARK: - PlacemarkManager + + override open func configure(placemark: MKAnnotationView) { + guard let clusterAnnotation = placemark.annotation as? MKClusterAnnotation, + let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager] else { + return + } + + placemark.image = iconProvider(placemarkManagers) + } + + // MARK: - MKMapViewDelegate + + open func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + guard !(mapViewDelegate?.responds(to: #selector(mapView(_:viewFor:))) ?? false) else { + return mapViewDelegate?.mapView?(mapView, viewFor: annotation) + } + + switch annotation { + case is MKClusterAnnotation: + let clusterAnnotationView = MKAnnotationView(annotation: annotation, + reuseIdentifier: MKMapViewDefaultClusterAnnotationViewReuseIdentifier) + configure(placemark: clusterAnnotationView) + + return clusterAnnotationView + case let placemarkManager as ApplePlacemarkManager: + let defaultAnnotationView = MKAnnotationView(annotation: annotation, + reuseIdentifier: MKMapViewDefaultAnnotationViewReuseIdentifier) + + placemarkManager.configure(placemark: defaultAnnotationView) + + return defaultAnnotationView + default: + return nil + } + } + + open func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + guard !(mapViewDelegate?.responds(to: #selector(mapView(_:didSelect:))) ?? false) else { + mapViewDelegate?.mapView?(mapView, didSelect: view) + return + } + + switch view.annotation { + case let clusterAnnotation as MKClusterAnnotation: + guard let placemarkManagers = clusterAnnotation.memberAnnotations as? [ApplePlacemarkManager] else { + return + } + + _ = tapHandler?(placemarkManagers, .from(coordinates: placemarkManagers.map { $0.coordinate })) + case let placemarkManager as ApplePlacemarkManager: + _ = placemarkManager.tapHandler?(placemarkManager.dataModel, placemarkManager.coordinate) + default: + return + } + } + + // MARK: - MKMapViewDelegate selectors forwarding + + open override func responds(to aSelector: Selector) -> Bool { + let superResponds = super.responds(to: aSelector) + + guard !superResponds else { + return superResponds + } + + guard mapDelegateSelectors.contains(aSelector) else { + return superResponds + } + + return mapViewDelegate?.responds(to: aSelector) ?? false + } + + open override func forwardingTarget(for aSelector: Selector) -> Any? { + guard mapDelegateSelectors.contains(aSelector) else { + return nil + } + + 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) + } +} diff --git a/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift b/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift new file mode 100644 index 00000000..c9536e5d --- /dev/null +++ b/TIAppleMapUtils/Sources/ApplePlacemarkManager.swift @@ -0,0 +1,66 @@ +// +// 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 TIMapUtils +import MapKit + +open class ApplePlacemarkManager: BasePlacemarkManager, MKAnnotation { + // MARK: - MKAnnotation + + public let coordinate: CLLocationCoordinate2D + + public var clusteringIdentifier: String? + + public init(dataModel: Model, + coordinate: CLLocationCoordinate2D, + clusteringIdentifier: String?, + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + self.coordinate = coordinate + self.clusteringIdentifier = clusteringIdentifier + + super.init(dataModel: dataModel, + iconProvider: iconProvider, + tapHandler: tapHandler) + } + + public convenience init(dataModel: Model, + coordinate: CLLocationCoordinate2D, + clusteringIdentifier: String?, + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == Model { + + self.init(dataModel: dataModel, + coordinate: coordinate, + clusteringIdentifier: clusteringIdentifier, + iconProvider: { iconFactory.markerIcon(for: $0) }, + tapHandler: tapHandler) + } + + // MARK: - PlacemarkManager + + override open func configure(placemark: MKAnnotationView) { + placemark.image = iconProvider(dataModel) + placemark.clusteringIdentifier = clusteringIdentifier + } +} diff --git a/TIAppleMapUtils/Sources/NSObject+ProtocolInspection.swift b/TIAppleMapUtils/Sources/NSObject+ProtocolInspection.swift new file mode 100644 index 00000000..ff8f2d0e --- /dev/null +++ b/TIAppleMapUtils/Sources/NSObject+ProtocolInspection.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 ObjectiveC + +public extension NSObject { + static func instanceMethodSelectors(of protocol: Protocol) -> [Selector] { + var methodsCount: UInt32 = 0 + let methodsList = protocol_copyMethodDescriptionList(`protocol`, false, true, &methodsCount) + + defer { + methodsList?.deallocate() + } + + var runtimeSelectors: [Selector?] = [] + + for offset in 0.. 'MIT', :file => 'LICENSE' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '11.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + + s.dependency 'TIMapUtils', s.version.to_s +end diff --git a/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift b/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift index d791084c..8c55e6e3 100644 --- a/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift +++ b/TIFoundationUtils/DateFormatting/Sources/KeyedDecodingContainer+DateDecoding.swift @@ -32,9 +32,9 @@ public extension KeyedDecodingContainer { using: try userInfo.dateFormatter(for: dateFormat)) } - func decodeDate(forKey key: Key, - userInfo: [CodingUserInfoKey: Any], - dateFormat: Format) throws -> [Date] { + func decodeDates(forKey key: Key, + userInfo: [CodingUserInfoKey: Any], + dateFormat: Format) throws -> [Date] { let dateFormatter = try userInfo.dateFormatter(for: dateFormat) @@ -61,10 +61,10 @@ public extension KeyedDecodingContainer { using: try userInfo.dateFormatter(for: dateFormat)) } - func decodeDate(forKey key: Key, - userInfo: [CodingUserInfoKey: Any], - dateFormat: Format, - required: Bool) throws -> [Date]? { + func decodeDates(forKey key: Key, + userInfo: [CodingUserInfoKey: Any], + dateFormat: Format, + required: Bool) throws -> [Date]? { guard let stringDates = try decode([String]?.self, forKey: key, required: required) else { return nil diff --git a/TIFoundationUtils/TIFoundationUtils.podspec b/TIFoundationUtils/TIFoundationUtils.podspec index 8c9859aa..203d5697 100644 --- a/TIFoundationUtils/TIFoundationUtils.podspec +++ b/TIFoundationUtils/TIFoundationUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIFoundationUtils' - s.version = '1.15.0' + s.version = '1.16.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/GoogleClusterPlacemarkManager.swift b/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift new file mode 100644 index 00000000..f1118db3 --- /dev/null +++ b/TIGoogleMapUtils/Sources/GoogleClusterPlacemarkManager.swift @@ -0,0 +1,122 @@ +// +// 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 TIMapUtils +import GoogleMapsUtils +import GoogleMaps + +open class GoogleClusterPlacemarkManager: BasePlacemarkManager], GMSCoordinateBounds>, GMUClusterRendererDelegate, GMUClusterManagerDelegate, GMUClusterIconGenerator { + public var mapDelegate: GMSMapViewDelegate? { + didSet { + clusterManager?.setMapDelegate(mapDelegate) + } + } + + public private(set) var clusterManager: GMUClusterManager? + + public init(placemarkManagers: [GooglePlacemarkManager], + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + super.init(dataModel: placemarkManagers, + iconProvider: iconProvider, + tapHandler: tapHandler) + } + + public convenience init(placemarkManagers: [GooglePlacemarkManager], + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == [Model] { + + self.init(placemarkManagers: placemarkManagers, + iconProvider: { iconFactory.markerIcon(for: $0.map { $0.dataModel }) }, + tapHandler: tapHandler) + } + + open func addMarkers(to map: GMSMapView) { + let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm() + let renderer = GMUDefaultClusterRenderer(mapView: map, + clusterIconGenerator: self) + + renderer.delegate = self + + clusterManager = GMUClusterManager(map: map, + algorithm: algorithm, + renderer: renderer) + + clusterManager?.setDelegate(self, mapDelegate: mapDelegate) + clusterManager?.add(dataModel) + clusterManager?.cluster() + } + + // MARK: - GMUClusterRendererDelegate + + open func renderer(_ renderer: GMUClusterRenderer, markerFor object: Any) -> GMSMarker? { + nil + } + + open func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) { + switch marker.userData { + case let cluster as GMUCluster: + guard let placemarkManagers = cluster.items as? [GooglePlacemarkManager] else { + return + } + + marker.icon = iconProvider(placemarkManagers) + case let clusterItem as GooglePlacemarkManager: + clusterItem.configure(placemark: marker) + default: + break + } + } + + open func renderer(_ renderer: GMUClusterRenderer, didRenderMarker marker: GMSMarker) { + // nothing + } + + // MARK: - GMUClusterManagerDelegate + + open func clusterManager(_ clusterManager: GMUClusterManager, didTap cluster: GMUCluster) -> Bool { + guard let placemarkManagers = cluster.items as? [GooglePlacemarkManager] else { + return false + } + + let bounds = placemarkManagers.reduce(GMSCoordinateBounds()) { + $0.includingCoordinate($1.position) + } + + return tapHandler?(placemarkManagers, bounds) ?? false + } + + open func clusterManager(_ clusterManager: GMUClusterManager, didTap clusterItem: GMUClusterItem) -> Bool { + guard let placemarkManager = clusterItem as? GooglePlacemarkManager else { + return false + } + + return placemarkManager.tapHandler?(placemarkManager.dataModel, clusterItem.position) ?? false + } + + // MARK: - GMUClusterIconGenerator + + open func icon(forSize size: UInt) -> UIImage? { + nil + } +} diff --git a/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift b/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift new file mode 100644 index 00000000..d8402e41 --- /dev/null +++ b/TIGoogleMapUtils/Sources/GooglePlacemarkManager.swift @@ -0,0 +1,60 @@ +// +// 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 TIMapUtils +import GoogleMaps +import GoogleMapsUtils + +open class GooglePlacemarkManager: BasePlacemarkManager, GMUClusterItem { + // MARK: - GMUClusterItem + + public let position: CLLocationCoordinate2D + + public init(dataModel: Model, + position: CLLocationCoordinate2D, + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + self.position = position + + super.init(dataModel: dataModel, + iconProvider: iconProvider, + tapHandler: tapHandler) + } + + public convenience init(dataModel: Model, + position: CLLocationCoordinate2D, + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == Model { + + self.init(dataModel: dataModel, + position: position, + iconProvider: { iconFactory.markerIcon(for: $0) }, + tapHandler: tapHandler) + } + + // MARK: - PlacemarkManager + + override open func configure(placemark: GMSMarker) { + placemark.icon = iconProvider(dataModel) + } +} diff --git a/TIGoogleMapUtils/TIGoogleMapUtils.podspec b/TIGoogleMapUtils/TIGoogleMapUtils.podspec new file mode 100644 index 00000000..d00ce801 --- /dev/null +++ b/TIGoogleMapUtils/TIGoogleMapUtils.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'TIGoogleMapUtils' + s.version = '1.16.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' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '12.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + + s.static_framework = true + s.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + s.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } + + s.dependency 'TIMapUtils', s.version.to_s + s.dependency 'Google-Maps-iOS-Utils', '~> 4' +end diff --git a/TIKeychainUtils/TIKeychainUtils.podspec b/TIKeychainUtils/TIKeychainUtils.podspec index 7f0c0a34..3a1d9552 100644 --- a/TIKeychainUtils/TIKeychainUtils.podspec +++ b/TIKeychainUtils/TIKeychainUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIKeychainUtils' - s.version = '1.15.0' + s.version = '1.16.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/Drawing/Helpers/CGGeometry+Extensions.swift b/TIMapUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.swift new file mode 100644 index 00000000..16e77135 --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Helpers/CGGeometry+Extensions.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 CoreGraphics.CGGeometry + +public typealias CGContextSize = (width: Int, height: Int) + +public extension CGSize { + var ceiledContextSize: CGContextSize { + (width: Int(ceil(width)), height: Int(ceil(height))) + } +} + +public extension CGPoint { + func horizontallyFlipped() -> Self { + CGPoint(x: x, y: -y) + } +} + +public extension CGRect { + func offset(by point: CGPoint) -> Self { + offsetBy(dx: point.x, dy: point.y) + } +} diff --git a/TIMapUtils/Sources/Drawing/Helpers/CGSize+Resize.swift b/TIMapUtils/Sources/Drawing/Helpers/CGSize+Resize.swift new file mode 100644 index 00000000..145a4c73 --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Helpers/CGSize+Resize.swift @@ -0,0 +1,68 @@ +// +// 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 CoreGraphics.CGGeometry + +public extension CGSize { + func resizeRect(forNewSize newSize: CGSize, resizeMode: ResizeMode) -> CGRect { + let horizontalRatio = newSize.width / width + let verticalRatio = newSize.height / height + + let ratio: CGFloat + + switch resizeMode { + case .scaleToFill: + ratio = 1 + + case .scaleAspectFill: + ratio = max(horizontalRatio, verticalRatio) + + case .scaleAspectFit: + ratio = min(horizontalRatio, verticalRatio) + } + + let newWidth = resizeMode == .scaleToFill ? newSize.width : width * ratio + let newHeight = resizeMode == .scaleToFill ? newSize.height : height * ratio + + let originX: CGFloat + let originY: CGFloat + + if newWidth > newSize.width { + originX = (newSize.width - newWidth) / 2 + } else if newWidth < newSize.width { + originX = newSize.width / 2 - newWidth / 2 + } else { + originX = 0 + } + + if newHeight > newSize.height { + originY = (newSize.height - newHeight) / 2 + } else if newHeight < newSize.height { + originY = newSize.height / 2 - newHeight / 2 + } else { + originY = 0 + } + + return CGRect(origin: CGPoint(x: originX, y: originY), + size: CGSize(width: newWidth, height: newHeight)) + } +} diff --git a/TIMapUtils/Sources/Drawing/Helpers/ResizeMode.swift b/TIMapUtils/Sources/Drawing/Helpers/ResizeMode.swift new file mode 100644 index 00000000..fad93dc9 --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Helpers/ResizeMode.swift @@ -0,0 +1,25 @@ +// +// 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 enum ResizeMode { + case scaleToFill, scaleAspectFit, scaleAspectFill +} diff --git a/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift new file mode 100644 index 00000000..797593fd --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift @@ -0,0 +1,83 @@ +// +// 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 + +public struct BorderDrawingOperation: DrawingOperation { + public var frameableContentSize: CGSize + public var border: CGFloat + public var color: CGColor + public var radius: CGFloat + public var exteriorBorder: Bool + + public init(frameableContentSize: CGSize, + border: CGFloat, + color: CGColor, + radius: CGFloat, + exteriorBorder: Bool) { + + self.frameableContentSize = frameableContentSize + self.border = border + self.color = color + self.radius = radius + self.exteriorBorder = exteriorBorder + } + + // MARK: - DrawingOperation + + public func affectedArea(in context: CGContext?) -> CGRect { + let margin = exteriorBorder ? border : 0 + + let width = frameableContentSize.width + margin * 2 + let height = frameableContentSize.height + margin * 2 + + return CGRect(origin: .zero, + size: CGSize(width: width, height: height)) + } + + public func apply(in context: CGContext) { + let drawArea = affectedArea(in: context) + + let ctxSize = drawArea.size.ceiledContextSize + + let ctxRect = CGRect(origin: .zero, size: CGSize(width: ctxSize.width, height: ctxSize.height)) + + let widthDiff = CGFloat(ctxSize.width) - drawArea.width // difference between context width and real width + let heightDiff = CGFloat(ctxSize.height) - drawArea.height // difference between context height and real height + let inset = ctxRect.insetBy(dx: border / 2 + widthDiff, dy: border / 2 + heightDiff) + + context.setStrokeColor(color) + + if radius != 0 { + context.setLineWidth(border) + + let path = CGPath(roundedRect: inset, + cornerWidth: radius, + cornerHeight: radius, + transform: nil) + context.addPath(path) + context.strokePath() + } else { + context.stroke(inset, width: border) + } + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift new file mode 100644 index 00000000..49200b0f --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/CALayerDrawingOperation.swift @@ -0,0 +1,50 @@ +// +// 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 CoreGraphics +import QuartzCore + +public struct CALayerDrawingOperation: DrawingOperation { + public var layer: CALayer + public var offset: CGPoint + + public init(layer: CALayer, + offset: CGPoint) { + + self.layer = layer + self.offset = offset + } + + // MARK: - DrawingOperation + + public func affectedArea(in context: CGContext? = nil) -> CGRect { + CGRect(origin: offset, size: layer.bounds.size) + } + + public func apply(in context: CGContext) { + let offsetTransform = CGAffineTransform(translationX: offset.x, y: offset.y) + + context.concatenate(offsetTransform) + layer.render(in: context) + offsetTransform.concatenating(offsetTransform.inverted()) + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/DrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/DrawingOperation.swift new file mode 100644 index 00000000..3faace21 --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/DrawingOperation.swift @@ -0,0 +1,28 @@ +// +// 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 CoreGraphics + +public protocol DrawingOperation { + func affectedArea(in context: CGContext?) -> CGRect + func apply(in context: CGContext) +} diff --git a/TIMapUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.swift new file mode 100644 index 00000000..08369a25 --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/OrientationAwareDrawingOperation.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 CoreGraphics + +public protocol OrientationAwareDrawingOperation: DrawingOperation { + var flipHorizontallyDuringDrawing: Bool { get set } +} + +extension OrientationAwareDrawingOperation { + func apply(in context: CGContext, operation: (CGContext) -> Void) { + if flipHorizontallyDuringDrawing { + let flipVertical = CGAffineTransform(a: 1, + b: 0, + c: 0, + d: -1, + tx: 0, + ty: affectedArea(in: context).height) + + context.concatenate(flipVertical) + operation(context) + context.concatenate(flipVertical.inverted()) + } else { + operation(context) + } + } + + func offsetForDrawing(_ offset: CGPoint) -> CGPoint { + flipHorizontallyDuringDrawing ? offset.horizontallyFlipped() : offset + } + + func affectedAreaForDrawing(in context: CGContext?) -> CGRect { + var area = affectedArea(in: context) + area.origin = offsetForDrawing(area.origin) + + return area + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift new file mode 100644 index 00000000..9b92d5ce --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/SolidFillDrawingOperation.swift @@ -0,0 +1,78 @@ +// +// 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 CoreGraphics + +public struct SolidFillDrawingOperation: DrawingOperation { + public enum Shape { + case rect(CGRect) + case ellipse(CGRect) + case path(CGPath) + } + + public var color: CGColor + public var shape: Shape + + public init(color: CGColor, shape: Shape) { + self.color = color + self.shape = shape + } + + public init(color: CGColor, rect: CGRect) { + self.init(color: color, shape: .rect(rect)) + } + + public init(color: CGColor, ellipseRect: CGRect) { + self.init(color: color, shape: .ellipse(ellipseRect)) + } + + public init(color: CGColor, path: CGPath) { + self.init(color: color, shape: .path(path)) + } + + // MARK: - DrawingOperation + + public func affectedArea(in context: CGContext? = nil) -> CGRect { + switch shape { + case let .rect(rect): + return rect + case let .ellipse(rect): + return rect + case let .path(path): + return path.boundingBox + } + } + + public func apply(in context: CGContext) { + context.setFillColor(color) + + switch shape { + case let .rect(rect): + context.fill(rect) + case let .ellipse(rect): + context.fillEllipse(in: rect) + case let .path(path): + context.addPath(path) + context.fillPath() + } + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.swift new file mode 100644 index 00000000..5bd14f3a --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/TextDrawingOperation.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 CoreGraphics +import Foundation.NSAttributedString +import CoreText + +public struct TextDrawingOperation: OrientationAwareDrawingOperation { + public var text: String + public var font: CTFont + public var textColor: CGColor + public var flipHorizontallyDuringDrawing: Bool + public var desiredOffset: CGPoint + + private var line: CTLine { + let textAttributes: [NSAttributedString.Key : Any] = [ + .font: font, + .foregroundColor: textColor + ] + + let attributedString = NSAttributedString(string: text, attributes: textAttributes) + + return CTLineCreateWithAttributedString(attributedString) + } + + public init(text: String, + font: CTFont, + textColor: CGColor, + flipHorizontallyDuringDrawing: Bool = true, + desiredOffset: CGPoint = .zero) { + + self.text = text + self.font = font + self.textColor = textColor + self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing + self.desiredOffset = desiredOffset + } + + public func affectedArea(in context: CGContext? = nil) -> CGRect { + CGRect(origin: desiredOffset, size: CTLineGetImageBounds(line, context).size) + } + + public func apply(in context: CGContext) { + apply(in: context) { + let originForDrawing = offsetForDrawing(CTLineGetImageBounds(line, context).origin) + let desiredOffsetForDrawing = offsetForDrawing(desiredOffset) + let textPosition = CGPoint(x: desiredOffsetForDrawing.x - originForDrawing.x, + y: desiredOffsetForDrawing.y - originForDrawing.y) + + $0.textPosition = textPosition + + CTLineDraw(line, $0) + } + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/TransformDrawingOperation.swift new file mode 100644 index 00000000..a85a738d --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/TransformDrawingOperation.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 CoreGraphics + +public struct TransformDrawingOperation: OrientationAwareDrawingOperation { + public var image: CGImage + public var imageSize: CGSize + public var maxNewSize: CGSize + public var resizeMode: ResizeMode + public var offset: CGPoint + public var flipHorizontallyDuringDrawing: Bool + public var cropToImageBounds: Bool + public var interpolationQuality: CGInterpolationQuality + + private var resizedRect: CGRect { + imageSize.resizeRect(forNewSize: maxNewSize, resizeMode: resizeMode) + } + + public init(image: CGImage, + imageSize: CGSize, + maxNewSize: CGSize, + resizeMode: ResizeMode = .scaleAspectFit, + offset: CGPoint = .zero, + flipHorizontallyDuringDrawing: Bool = true, + cropToImageBounds: Bool = false, + interpolationQuality: CGInterpolationQuality = .default) { + + self.image = image + self.imageSize = imageSize + self.maxNewSize = maxNewSize + self.resizeMode = resizeMode + self.offset = offset + self.flipHorizontallyDuringDrawing = flipHorizontallyDuringDrawing + self.cropToImageBounds = cropToImageBounds + self.interpolationQuality = interpolationQuality + } + + // MARK: - DrawingOperation + + public func affectedArea(in context: CGContext? = nil) -> CGRect { + if cropToImageBounds { + return CGRect(origin: offset, size: resizedRect.size) + } else { + return resizedRect.offset(by: offset) + } + } + + public func apply(in context: CGContext) { + apply(in: context) { + $0.interpolationQuality = interpolationQuality + + $0.draw(image, in: affectedAreaForDrawing(in: $0)) + } + } +} diff --git a/TIMapUtils/Sources/Helpers/CoordinateBounds.swift b/TIMapUtils/Sources/Helpers/CoordinateBounds.swift new file mode 100644 index 00000000..cb9bc88e --- /dev/null +++ b/TIMapUtils/Sources/Helpers/CoordinateBounds.swift @@ -0,0 +1,51 @@ +// +// 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 struct CoordinateBounds { + public let southWest: CLLocationCoordinate2D + public let northEast: CLLocationCoordinate2D + + public init(southWest: CLLocationCoordinate2D, northEast: CLLocationCoordinate2D) { + 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))) + } + } +} diff --git a/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.swift new file mode 100644 index 00000000..10083d31 --- /dev/null +++ b/TIMapUtils/Sources/IconProviders/DefaultCachableMarkerIconFactory.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 UIKit.UIImage + +open class DefaultCachableMarkerIconFactory: DefaultMarkerIconFactory { + public typealias CacheKeyProvider = (M) -> K + + public let cache = NSCache() + + private let cacheKeyProvider: CacheKeyProvider + + public init(createIconClosure: @escaping CreateIconClosure, + cacheKeyProvider: @escaping CacheKeyProvider) { + + self.cacheKeyProvider = cacheKeyProvider + + super.init(createIconClosure: createIconClosure) + } + + open override func markerIcon(for model: M) -> UIImage { + let cacheKey = cacheKeyProvider(model) + + guard let cachedIcon = cache.object(forKey: cacheKey) else { + let icon = super.markerIcon(for: model) + cache.setObject(icon, forKey: cacheKey) + + return icon + } + + return cachedIcon + } +} diff --git a/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift new file mode 100644 index 00000000..5d8a81f0 --- /dev/null +++ b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift @@ -0,0 +1,173 @@ +// +// 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 + +open class DefaultClusterIconRenderer { + public struct TextAttributes { + public var font: UIFont + public var color: UIColor + + public init(font: UIFont, color: UIColor) { + self.font = font + self.color = color + } + } + + public enum Background { + case color(UIColor) + case image(UIImage) + } + + public struct Border { + public var strokeSize: CGFloat + public var color: UIColor + + public init(strokeSize: CGFloat, color: UIColor) { + self.strokeSize = strokeSize + self.color = color + } + } + + public var screenScale: CGFloat + public var textAttributes: TextAttributes + public var marginToText: CGFloat + public var background: Background + public var border: Border + + private var borderWidth: CGFloat { + border.strokeSize + } + + public init(screenScale: CGFloat, + textAttributes: TextAttributes = .init(font: .systemFont(ofSize: 48), + color: .black), + marginToText: CGFloat = 8, + background: Background = .color(.orange), + border: Border = .init(strokeSize: 4, color: .white)) { + + self.screenScale = screenScale + self.textAttributes = textAttributes + self.marginToText = marginToText + self.background = background + self.border = border + } + + open func format(clusterSize: Int) -> String { + String(clusterSize) + } + + open func textDrawingOperation(for text: String) -> TextDrawingOperation { + let ctFont = CTFontCreateWithFontDescriptorAndOptions(textAttributes.font.fontDescriptor, + textAttributes.font.pointSize, + nil, + []) + + return TextDrawingOperation(text: text, + font: ctFont, + textColor: textAttributes.color.cgColor) + } + + open func backgroundDrawingOperation(iconSize: CGSize, + iconSizeWithBorder: CGSize, + cornerRadius: CGFloat) -> DrawingOperation? { + + switch background { + case let .color(color): + let path = CGPath(roundedRect: CGRect(origin: CGPoint(x: borderWidth, y: borderWidth), + size: iconSize), + cornerWidth: cornerRadius, + cornerHeight: cornerRadius, + transform: nil) + + return SolidFillDrawingOperation(color: color.cgColor, + path: path) + case let .image(image): + if let cgImage = image.cgImage { + return TransformDrawingOperation(image: cgImage, + imageSize: image.size, + maxNewSize: iconSize, + flipHorizontallyDuringDrawing: true) + } else { + return nil + } + } + } + + open func borderDrawingOperation(iconSize: CGSize, + cornerRadius: CGFloat) -> DrawingOperation { + + BorderDrawingOperation(frameableContentSize: iconSize, + border: borderWidth, + color: border.color.cgColor, + radius: cornerRadius, + exteriorBorder: true) + } + + open func execute(drawingOperations: [DrawingOperation], inContextWithSize size: CGSize) -> UIImage { + let format = UIGraphicsImageRendererFormat() + format.opaque = false + format.scale = screenScale + + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + return renderer.image { + for operation in drawingOperations { + operation.apply(in: $0.cgContext) + } + } + } + + open func renderCluster(of size: Int) -> UIImage { + let text = format(clusterSize: size) + + var textDrawingOperation = textDrawingOperation(for: text) + + let textSize = textDrawingOperation.affectedArea().size + let textRadius = sqrt(textSize.height * textSize.height + textSize.width * textSize.width) / 2 + let internalRadius = textRadius + marginToText + + let iconSize = CGSize(width: internalRadius * 2, height: internalRadius * 2) + let iconSizeWithBorder = CGSize(width: iconSize.width + borderWidth * 2, + height: iconSize.height + borderWidth * 2) + + let radius = CGFloat(min(iconSizeWithBorder.width, iconSizeWithBorder.height) / 2) + + textDrawingOperation.desiredOffset = CGPoint(x: (iconSizeWithBorder.width - textSize.width) / 2, + y: (iconSizeWithBorder.height - textSize.height) / 2) + + let backgroundDrawingOperation = backgroundDrawingOperation(iconSize: iconSize, + iconSizeWithBorder: iconSizeWithBorder, + cornerRadius: radius) + + let borderDrawindOperation = borderDrawingOperation(iconSize: iconSize, + cornerRadius: radius) + + let operations = [backgroundDrawingOperation, + textDrawingOperation, + borderDrawindOperation] + .compactMap { $0 } + + return execute(drawingOperations: operations, + inContextWithSize: iconSizeWithBorder) + } +} diff --git a/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift new file mode 100644 index 00000000..b4d4b35a --- /dev/null +++ b/TIMapUtils/Sources/IconProviders/DefaultMarkerIconFactory.swift @@ -0,0 +1,41 @@ +// +// 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.UIImage + +open class DefaultMarkerIconFactory: MarkerIconFactory { + public typealias CreateIconClosure = (M) -> UIImage + + private let createIconClosure: CreateIconClosure + + public init(createIconClosure: @escaping CreateIconClosure) { + self.createIconClosure = createIconClosure + } + + open func markerIcon(for model: M) -> UIImage { + postprocess(icon: createIconClosure(model)) + } + + open func postprocess(icon: UIImage) -> UIImage { + icon + } +} diff --git a/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift b/TIMapUtils/Sources/IconProviders/MarkerIconFactory.swift new file mode 100644 index 00000000..405688bb --- /dev/null +++ b/TIMapUtils/Sources/IconProviders/MarkerIconFactory.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. +// + +import UIKit.UIImage + +public protocol MarkerIconFactory { + associatedtype Model + + func markerIcon(for model: Model) -> UIImage +} diff --git a/TIMapUtils/Sources/Managers/BasePlacemarkManager.swift b/TIMapUtils/Sources/Managers/BasePlacemarkManager.swift new file mode 100644 index 00000000..d8fb4aa4 --- /dev/null +++ b/TIMapUtils/Sources/Managers/BasePlacemarkManager.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 UIKit + +open class BasePlacemarkManager: NSObject, PlacemarkManager { + public typealias TapHandlerClosure = (Model, Position) -> Bool + public typealias IconProviderClosure = (Model) -> UIImage + + public var tapHandler: TapHandlerClosure? + public var iconProvider: IconProviderClosure + + public let dataModel: Model + + public init(dataModel: Model, + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + self.dataModel = dataModel + self.iconProvider = iconProvider + self.tapHandler = tapHandler + } + + public convenience init(dataModel: Model, + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == Model { + + self.init(dataModel: dataModel, + iconProvider: { iconFactory.markerIcon(for: $0) }, + tapHandler: tapHandler) + } + + // MARK: - PlacemarkManager + + open func configure(placemark: Placemark) { + // override in subclass + } +} diff --git a/TIMapUtils/Sources/Managers/PlacemarkManager.swift b/TIMapUtils/Sources/Managers/PlacemarkManager.swift new file mode 100644 index 00000000..7f256cf8 --- /dev/null +++ b/TIMapUtils/Sources/Managers/PlacemarkManager.swift @@ -0,0 +1,27 @@ +// +// 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 PlacemarkManager { + associatedtype Placemark + + func configure(placemark: Placemark) +} diff --git a/TIMapUtils/TIMapUtils.podspec b/TIMapUtils/TIMapUtils.podspec new file mode 100644 index 00000000..d4a63afe --- /dev/null +++ b/TIMapUtils/TIMapUtils.podspec @@ -0,0 +1,15 @@ +Pod::Spec.new do |s| + s.name = 'TIMapUtils' + s.version = '1.16.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' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '10.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + +end diff --git a/TIMoyaNetworking/TIMoyaNetworking.podspec b/TIMoyaNetworking/TIMoyaNetworking.podspec index 6d9e70d5..a25d2abc 100644 --- a/TIMoyaNetworking/TIMoyaNetworking.podspec +++ b/TIMoyaNetworking/TIMoyaNetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIMoyaNetworking' - s.version = '1.15.0' + s.version = '1.16.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 787a0874..198294bc 100644 --- a/TINetworking/TINetworking.podspec +++ b/TINetworking/TINetworking.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworking' - s.version = '1.15.0' + s.version = '1.16.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 ed2f2df0..6ec81bc0 100644 --- a/TINetworkingCache/TINetworkingCache.podspec +++ b/TINetworkingCache/TINetworkingCache.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TINetworkingCache' - s.version = '1.15.0' + s.version = '1.16.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 01d3ed4d..93ad71e9 100644 --- a/TIPagination/TIPagination.podspec +++ b/TIPagination/TIPagination.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIPagination' - s.version = '1.15.0' + s.version = '1.16.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 c45f7043..d9f3eb30 100644 --- a/TISwiftUtils/TISwiftUtils.podspec +++ b/TISwiftUtils/TISwiftUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TISwiftUtils' - s.version = '1.15.0' + s.version = '1.16.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 7c23989c..85973248 100644 --- a/TITableKitUtils/TITableKitUtils.podspec +++ b/TITableKitUtils/TITableKitUtils.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITableKitUtils' - s.version = '1.15.0' + s.version = '1.16.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 0616a5ef..3211d723 100644 --- a/TITransitions/TITransitions.podspec +++ b/TITransitions/TITransitions.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TITransitions' - s.version = '1.15.0' + s.version = '1.16.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 76f61d39..03d63acc 100644 --- a/TIUIElements/TIUIElements.podspec +++ b/TIUIElements/TIUIElements.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIElements' - s.version = '1.15.0' + s.version = '1.16.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 f52155a0..cd4dc918 100644 --- a/TIUIKitCore/TIUIKitCore.podspec +++ b/TIUIKitCore/TIUIKitCore.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = 'TIUIKitCore' - s.version = '1.15.0' + s.version = '1.16.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/YandexClusterPlacemarkManager.swift b/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift new file mode 100644 index 00000000..af3078f6 --- /dev/null +++ b/TIYandexMapUtils/Sources/YandexClusterPlacemarkManager.swift @@ -0,0 +1,109 @@ +// +// 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 TIMapUtils +import YandexMapsMobile +import UIKit + +open class YandexClusterPlacemarkManager: BasePlacemarkManager], YMKBoundingBox>, YMKClusterListener, YMKClusterTapListener { + public var placemarksMapping: Zip2Sequence<[YMKPlacemarkMapObject], [YandexPlacemarkManager]>? + + public init(placemarkManagers: [YandexPlacemarkManager], + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + super.init(dataModel: placemarkManagers, + iconProvider: iconProvider, + tapHandler: tapHandler) + } + + public convenience init(placemarkManagers: [YandexPlacemarkManager], + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == [Model] { + + self.init(placemarkManagers: placemarkManagers, + iconProvider: { iconFactory.markerIcon(for: $0.map { $0.dataModel }) }, + tapHandler: tapHandler) + } + + open func addMarkers(to map: YMKMap, clusterRadius: Double = 60, minZoom: UInt = 15) { + let clusterizedPlacemarkCollection = map.mapObjects.addClusterizedPlacemarkCollection(with: self) + + let emptyPlacemarks = clusterizedPlacemarkCollection.addEmptyPlacemarks(with: dataModel.map { $0.position }) + + self.placemarksMapping = zip(emptyPlacemarks, dataModel) + + placemarksMapping?.forEach { (placemark, manager) in + manager.configure(placemark: placemark) + } + + clusterizedPlacemarkCollection.clusterPlacemarks(withClusterRadius: clusterRadius, + minZoom: minZoom) + } + + // MARK: - YMKClusterListener + + open func onClusterAdded(with cluster: YMKCluster) { + configure(placemark: cluster) + } + + // MARK: - YMKClusterTapListener + + open func onClusterTap(with cluster: YMKCluster) -> Bool { + guard let tapHandler = tapHandler else { + return false + } + + return tapHandler(managers(in: cluster), .from(coordinates: managers(in: cluster).map { $0.position })) + } + + open func managers(in cluster: YMKCluster) -> [YandexPlacemarkManager] { + cluster.placemarks.compactMap { placemark in + placemarksMapping?.first { $0.0 == placemark }?.1 + } + } + + // MARK: - PlacemarkManager + + open override func configure(placemark: YMKCluster) { + placemark.addClusterTapListener(with: self) + 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))) + + } + } +} diff --git a/TIYandexMapUtils/Sources/YandexPlacemarkManager.swift b/TIYandexMapUtils/Sources/YandexPlacemarkManager.swift new file mode 100644 index 00000000..3681b81e --- /dev/null +++ b/TIYandexMapUtils/Sources/YandexPlacemarkManager.swift @@ -0,0 +1,64 @@ +// +// 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 TIMapUtils +import YandexMapsMobile + +open class YandexPlacemarkManager: BasePlacemarkManager, YMKMapObjectTapListener { + public let position: YMKPoint + + public init(dataModel: Model, + position: YMKPoint, + iconProvider: @escaping IconProviderClosure, + tapHandler: TapHandlerClosure?) { + + self.position = position + + super.init(dataModel: dataModel, + iconProvider: iconProvider, + tapHandler: tapHandler) + } + + public convenience init(dataModel: Model, + position: YMKPoint, + iconFactory: IF, + tapHandler: TapHandlerClosure?) where IF.Model == Model { + + self.init(dataModel: dataModel, + position: position, + iconProvider: { iconFactory.markerIcon(for: $0) }, + tapHandler: tapHandler) + } + + // MARK: - YMKMapObjectTapListener + + public func onMapObjectTap(with mapObject: YMKMapObject, point: YMKPoint) -> Bool { + tapHandler?(dataModel, point) ?? false + } + + // MARK: - PlacemarkManager + + override open func configure(placemark: YMKPlacemarkMapObject) { + placemark.addTapListener(with: self) + placemark.setIconWith(iconProvider(dataModel)) + } +} diff --git a/TIYandexMapUtils/TIYandexMapUtils.podspec b/TIYandexMapUtils/TIYandexMapUtils.podspec new file mode 100644 index 00000000..e1b64a7b --- /dev/null +++ b/TIYandexMapUtils/TIYandexMapUtils.podspec @@ -0,0 +1,21 @@ +Pod::Spec.new do |s| + s.name = 'TIYandexMapUtils' + s.version = '1.16.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' } + s.author = { 'petropavel13' => 'ivan.smolin@touchin.ru' } + s.source = { :git => 'https://github.com/TouchInstinct/LeadKit.git', :tag => s.version.to_s } + + s.ios.deployment_target = '10.0' + s.swift_versions = ['5.3'] + + s.source_files = s.name + '/Sources/**/*' + + s.static_framework = true + s.user_target_xcconfig = { 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.pod_target_xcconfig = { 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + + s.dependency 'TIMapUtils', s.version.to_s + s.dependency 'YandexMapsMobile', '4.0.0-lite' +end diff --git a/project-scripts/push_to_podspecs.sh b/project-scripts/push_to_podspecs.sh index 5a6130ce..2cf66e80 100755 --- a/project-scripts/push_to_podspecs.sh +++ b/project-scripts/push_to_podspecs.sh @@ -14,7 +14,11 @@ ORDERED_PODSPECS="../TISwiftUtils/TISwiftUtils.podspec ../TITableKitUtils/TITableKitUtils.podspec ../TINetworking/TINetworking.podspec ../TINetworkingCache/TINetworkingCache.podspec -../TIMoyaNetworking/TIMoyaNetworking.podspec" +../TIMoyaNetworking/TIMoyaNetworking.podspec +../TIMapUtils/TIMapUtils.podspec +../TIAppleMapUtils/TIAppleMapUtils.podspec +../TIGoogleMapUtils/TIGoogleMapUtils.podspec +../TIYandexMapUtils/TIYandexMapUtils.podspec" for podspec_path in ${ORDERED_PODSPECS}; do bundle exec pod repo push git@github.com:TouchInstinct/Podspecs ${podspec_path} --allow-warnings