From a5bc2dc8f0f9c35f2749fbfa4bf9231b8178036c Mon Sep 17 00:00:00 2001 From: Ivan Smolin Date: Wed, 31 May 2023 17:13:54 +0300 Subject: [PATCH] feat: current location marker and other ui settings to supported maps --- Package.swift | 12 +- TIAppleMapUtils/Sources/AppleMapManager.swift | 7 +- .../Sources/AppleMapUISettings.swift | 38 ++++ .../Sources/GoogleMapManager.swift | 7 +- .../Sources/GoogleMapUISettings.swift | 39 +++++ .../Operations/BorderDrawingOperation.swift | 15 +- .../CurrentLocationDrawingOperation.swift | 163 ++++++++++++++++++ .../DefaultClusterIconRenderer.swift | 2 +- .../Sources/Managers/BaseMapManager.swift | 18 +- .../Sources/Managers/BaseMapUISettings.swift | 36 ++++ .../Sources/Managers/MapUISettings.swift | 33 ++++ .../Sources/YandexMapManager.swift | 7 +- .../Sources/YandexMapUISettings.swift | 135 +++++++++++++++ 13 files changed, 489 insertions(+), 23 deletions(-) create mode 100644 TIAppleMapUtils/Sources/AppleMapUISettings.swift create mode 100644 TIGoogleMapUtils/Sources/GoogleMapUISettings.swift create mode 100644 TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift create mode 100644 TIMapUtils/Sources/Managers/BaseMapUISettings.swift create mode 100644 TIMapUtils/Sources/Managers/MapUISettings.swift create mode 100644 TIYandexMapUtils/Sources/YandexMapUISettings.swift diff --git a/Package.swift b/Package.swift index 18d25962..65b0de13 100644 --- a/Package.swift +++ b/Package.swift @@ -103,8 +103,16 @@ let package = Package( .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"), + + .target(name: "TIMapUtils", + dependencies: [], + path: "TIMapUtils/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), + + .target(name: "TIAppleMapUtils", + dependencies: ["TIMapUtils"], + path: "TIAppleMapUtils/Sources", + plugins: [.plugin(name: "TISwiftLintPlugin")]), // MARK: - Elements .target(name: "OTPSwiftView", dependencies: ["TIUIElements"], path: "OTPSwiftView/Sources"), diff --git a/TIAppleMapUtils/Sources/AppleMapManager.swift b/TIAppleMapUtils/Sources/AppleMapManager.swift index e4994236..2afd1c8b 100644 --- a/TIAppleMapUtils/Sources/AppleMapManager.swift +++ b/TIAppleMapUtils/Sources/AppleMapManager.swift @@ -24,9 +24,10 @@ import TIMapUtils import MapKit open class AppleMapManager: BaseMapManager, - AppleClusterPlacemarkManager, - MKCameraUpdate> { + ApplePlacemarkManager, + AppleClusterPlacemarkManager, + MKCameraUpdate, + AppleMapUISettings> { public typealias ClusteringIdentifier = String diff --git a/TIAppleMapUtils/Sources/AppleMapUISettings.swift b/TIAppleMapUtils/Sources/AppleMapUISettings.swift new file mode 100644 index 00000000..36692165 --- /dev/null +++ b/TIAppleMapUtils/Sources/AppleMapUISettings.swift @@ -0,0 +1,38 @@ +// +// Copyright (c) 2023 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 AppleMapUISettings: BaseMapUISettings { + public var showCompassButton = false + + override open func apply(to mapView: MKMapView) { + super.apply(to: mapView) + + mapView.showsUserLocation = showUserLocation + mapView.isZoomEnabled = isZoomEnabled + mapView.isPitchEnabled = isTiltEnabled + mapView.isRotateEnabled = isRotationEnabled + mapView.showsCompass = showCompassButton + } +} diff --git a/TIGoogleMapUtils/Sources/GoogleMapManager.swift b/TIGoogleMapUtils/Sources/GoogleMapManager.swift index 47ed240a..f3427fed 100644 --- a/TIGoogleMapUtils/Sources/GoogleMapManager.swift +++ b/TIGoogleMapUtils/Sources/GoogleMapManager.swift @@ -24,9 +24,10 @@ import TIMapUtils import GoogleMaps open class GoogleMapManager: BaseMapManager, - GoogleClusterPlacemarkManager, - GMSCameraUpdate> { + GooglePlacemarkManager, + GoogleClusterPlacemarkManager, + GMSCameraUpdate, + GoogleMapUISettings> { public init(map: GMSMapView, positionGetter: @escaping PositionGetter, diff --git a/TIGoogleMapUtils/Sources/GoogleMapUISettings.swift b/TIGoogleMapUtils/Sources/GoogleMapUISettings.swift new file mode 100644 index 00000000..7ad90770 --- /dev/null +++ b/TIGoogleMapUtils/Sources/GoogleMapUISettings.swift @@ -0,0 +1,39 @@ +// +// Copyright (c) 2023 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 + +open class GoogleMapUISettings: BaseMapUISettings { + public var showMyLocationButton = true + + open override func apply(to mapView: GMSMapView) { + super.apply(to: mapView) + + mapView.isMyLocationEnabled = showUserLocation + + mapView.settings.zoomGestures = isZoomEnabled + mapView.settings.tiltGestures = isTiltEnabled + mapView.settings.rotateGestures = isRotationEnabled + mapView.settings.myLocationButton = showMyLocationButton + } +} diff --git a/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift index 797593fd..1cb62540 100644 --- a/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift +++ b/TIMapUtils/Sources/Drawing/Operations/BorderDrawingOperation.swift @@ -23,19 +23,19 @@ import UIKit public struct BorderDrawingOperation: DrawingOperation { - public var frameableContentSize: CGSize + public var frameableContentRect: CGRect public var border: CGFloat public var color: CGColor public var radius: CGFloat public var exteriorBorder: Bool - public init(frameableContentSize: CGSize, + public init(frameableContentRect: CGRect, border: CGFloat, color: CGColor, radius: CGFloat, exteriorBorder: Bool) { - self.frameableContentSize = frameableContentSize + self.frameableContentRect = frameableContentRect self.border = border self.color = color self.radius = radius @@ -47,10 +47,10 @@ public struct BorderDrawingOperation: 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 + let width = frameableContentRect.width + margin * 2 + let height = frameableContentRect.height + margin * 2 - return CGRect(origin: .zero, + return CGRect(origin: frameableContentRect.origin, size: CGSize(width: width, height: height)) } @@ -59,7 +59,8 @@ public struct BorderDrawingOperation: DrawingOperation { let ctxSize = drawArea.size.ceiledContextSize - let ctxRect = CGRect(origin: .zero, size: CGSize(width: ctxSize.width, height: ctxSize.height)) + let ctxRect = CGRect(origin: frameableContentRect.origin, + 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 diff --git a/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift b/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift new file mode 100644 index 00000000..334bee2b --- /dev/null +++ b/TIMapUtils/Sources/Drawing/Operations/CurrentLocationDrawingOperation.swift @@ -0,0 +1,163 @@ +// +// Copyright (c) 2023 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 CurrentLocationDrawingOperation: DrawingOperation { + public enum Defaults { + public static var iconSize: CGSize { + CGSize(width: 27, height: 27) + } + + public static var borderWidth: CGFloat { + 2 + } + } + + public var iconSize: CGSize + public var borderWidth: CGFloat + public var mainColor: CGColor + public var borderColor: CGColor + public var backgroundColor: CGColor + public var showHeadingArrow: Bool + + private var innerCircleSize: CGSize { + CGSize(width: innerBorderSize.width - borderWidth, + height: innerBorderSize.height - borderWidth) + } + + private var innerBorderSize: CGSize { + CGSize(width: outerBorderSize.width - borderWidth, + height: outerBorderSize.height - borderWidth) + } + + private var outerBorderSize: CGSize { + CGSize(width: iconSize.width - 8, + height: iconSize.height - 8) + } + + private var innerCircleOrigin: CGPoint { + CGPoint(x: (iconSize.width - innerCircleSize.width) / 2, + y: (iconSize.height - innerCircleSize.height) / 2) + } + + private var innerBorderOrigin: CGPoint { + CGPoint(x: (iconSize.width - innerBorderSize.width) / 2, + y: (iconSize.height - innerBorderSize.height) / 2) + } + + private var outerBorderOrigin: CGPoint { + CGPoint(x: (iconSize.width - outerBorderSize.width) / 2, + y: (iconSize.height - outerBorderSize.height) / 2) + } + + private var iconRect: CGRect { + CGRect(origin: .zero, + size: iconSize) + } + + private var innerCircleRect: CGRect { + CGRect(origin: innerCircleOrigin, + size: innerCircleSize) + } + + private var innerBorderRect: CGRect { + CGRect(origin: innerBorderOrigin, + size: innerBorderSize) + } + + private var outerBorderRect: CGRect { + CGRect(origin: outerBorderOrigin, + size: outerBorderSize) + } + + public init(iconSize: CGSize = Defaults.iconSize, + borderWidth: CGFloat = Defaults.borderWidth, + mainColor: CGColor, + borderColor: CGColor, + showHeadingArrow: Bool = true) { + + self.iconSize = iconSize + self.borderWidth = borderWidth + self.mainColor = mainColor + self.borderColor = borderColor + self.backgroundColor = mainColor.copy(alpha: 0.3) ?? mainColor + self.showHeadingArrow = showHeadingArrow + } + + public func affectedArea(in context: CGContext? = nil) -> CGRect { + iconRect + } + + public func apply(in context: CGContext) { + let backgroundDrawing = SolidFillDrawingOperation(color: backgroundColor, + ellipseRect: iconRect) + + let innerCircleDrawing = SolidFillDrawingOperation(color: mainColor, + ellipseRect: innerCircleRect) + + let innerBorderDrawing = BorderDrawingOperation(frameableContentRect: innerBorderRect, + border: borderWidth, + color: borderColor, + radius: min(innerBorderSize.width, innerBorderSize.height) / 2, + exteriorBorder: false) + + let outerBorderDrawing = BorderDrawingOperation(frameableContentRect: outerBorderRect, + border: borderWidth, + color: backgroundColor, + radius: min(outerBorderSize.width, outerBorderSize.height) / 2, + exteriorBorder: false) + + // flip horizontally + + context.translateBy(x: 0, y: iconSize.height) + context.scaleBy(x: 1, y: -1) + + backgroundDrawing.apply(in: context) + + context.setFillColor(mainColor) + context.setLineWidth(.zero) + + guard showHeadingArrow else { + innerCircleDrawing.apply(in: context) + + innerBorderDrawing.apply(in: context) + + return + } + + context.addLines(between: [ + CGPoint(x: innerBorderRect.minX, y: innerBorderRect.midY), + CGPoint(x: iconRect.midX, y: iconRect.maxY), + CGPoint(x: innerBorderRect.maxX, y: innerBorderRect.midY) + ]) + + context.drawPath(using: .fillStroke) + + innerCircleDrawing.apply(in: context) + + innerBorderDrawing.apply(in: context) + + context.setBlendMode(.destinationAtop) + outerBorderDrawing.apply(in: context) + } +} diff --git a/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift index 08f50e73..65044aaf 100644 --- a/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift +++ b/TIMapUtils/Sources/IconProviders/DefaultClusterIconRenderer.swift @@ -113,7 +113,7 @@ open class DefaultClusterIconRenderer { open func borderDrawingOperation(iconSize: CGSize, cornerRadius: CGFloat) -> DrawingOperation { - BorderDrawingOperation(frameableContentSize: iconSize, + BorderDrawingOperation(frameableContentRect: CGRect(origin: .zero, size: iconSize), border: borderWidth, color: border.color.cgColor, radius: cornerRadius, diff --git a/TIMapUtils/Sources/Managers/BaseMapManager.swift b/TIMapUtils/Sources/Managers/BaseMapManager.swift index 43345bf0..bfadcb0e 100644 --- a/TIMapUtils/Sources/Managers/BaseMapManager.swift +++ b/TIMapUtils/Sources/Managers/BaseMapManager.swift @@ -26,10 +26,12 @@ import UIKit.UIGeometry open class BaseMapManager where PM.Position: LocationCoordinate, - PM.Position == CUF.Position, - CUF.Update.Map == Map, - CUF.BoundingBox == CPM.Position { + CUF: CameraUpdateFactory, + MS: MapUISettings> where PM.Position: LocationCoordinate, + PM.Position == CUF.Position, + CUF.Update.Map == Map, + CUF.BoundingBox == CPM.Position, + MS.MapView == Map { public typealias PositionGetter = (PM.DataModel) -> PM.Position? @@ -55,6 +57,8 @@ open class BaseMapManager: MapUISettings { + public var showUserLocation = true + + public var isZoomEnabled = true + public var isTiltEnabled = false + public var isRotationEnabled = false + + public init() { + } + + open func apply(to mapView: MapView) { + // override in subclass + } +} diff --git a/TIMapUtils/Sources/Managers/MapUISettings.swift b/TIMapUtils/Sources/Managers/MapUISettings.swift new file mode 100644 index 00000000..1a1c99a2 --- /dev/null +++ b/TIMapUtils/Sources/Managers/MapUISettings.swift @@ -0,0 +1,33 @@ +// +// Copyright (c) 2023 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 MapUISettings: AnyObject { + associatedtype MapView + + var isZoomEnabled: Bool { get set } + var isTiltEnabled: Bool { get set } + var isRotationEnabled: Bool { get set } + + var showUserLocation: Bool { get set } + + func apply(to mapView: MapView) +} diff --git a/TIYandexMapUtils/Sources/YandexMapManager.swift b/TIYandexMapUtils/Sources/YandexMapManager.swift index bddbf6fc..f9ae44e0 100644 --- a/TIYandexMapUtils/Sources/YandexMapManager.swift +++ b/TIYandexMapUtils/Sources/YandexMapManager.swift @@ -24,9 +24,10 @@ import TIMapUtils import YandexMapsMobile open class YandexMapManager: BaseMapManager, - YandexClusterPlacemarkManager, - YMKCameraUpdate> { + YandexPlacemarkManager, + YandexClusterPlacemarkManager, + YMKCameraUpdate, + YandexMapUISettings> { public init(map: YMKMapView, positionGetter: @escaping PositionGetter, diff --git a/TIYandexMapUtils/Sources/YandexMapUISettings.swift b/TIYandexMapUtils/Sources/YandexMapUISettings.swift new file mode 100644 index 00000000..17fc6d8b --- /dev/null +++ b/TIYandexMapUtils/Sources/YandexMapUISettings.swift @@ -0,0 +1,135 @@ +// +// Copyright (c) 2023 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 YandexMapUISettings: BaseMapUISettings { + open class UserLocationObjectListener: NSObject, YMKUserLocationObjectListener { + public var tintColor: UIColor { + didSet { + userLocationView?.accuracyCircle.fillColor = tintColor.withAlphaComponent(0.2) + } + } + + public var currentLocationIcon: UIImage { + didSet { + updateCurrentLocationIcon() + } + } + + private weak var userLocationView: YMKUserLocationView? + + public init(tintColor: UIColor, currentLocationIcon: UIImage) { + self.tintColor = tintColor + self.currentLocationIcon = currentLocationIcon + } + + // MARK: - YMKUserLocationObjectListener + + open func onObjectAdded(with view: YMKUserLocationView) { + self.userLocationView = view + + updateCurrentLocationIcon() + + view.accuracyCircle.fillColor = tintColor.withAlphaComponent(0.2) + } + + open func onObjectRemoved(with view: YMKUserLocationView) {} + + open func onObjectUpdated(with view: YMKUserLocationView, event: YMKObjectEvent) {} + + private func updateCurrentLocationIcon() { + userLocationView?.arrow.setIconWith(currentLocationIcon) + + let style = YMKIconStyle(anchor: CGPoint(x: 0.5, y: 0.5) as NSValue, + rotationType:YMKRotationType.rotate.rawValue as NSNumber, + zIndex: 0, + flat: true, + visible: true, + scale: 1, + tappableArea: nil) + + userLocationView?.pin.setIconWith(currentLocationIcon, style: style) + } + } + + public weak var userLocationLayer: YMKUserLocationLayer? + public weak var mapKit: YMKMapKit? + public var userLocationObjectListener: UserLocationObjectListener? + + public var tintColor: UIColor = .red { + didSet { + userLocationObjectListener?.tintColor = tintColor + } + } + + public var showHeadingArrow: Bool = true { + didSet { + userLocationObjectListener?.currentLocationIcon = currentLocationIcon(showHeadingArrow: showHeadingArrow) + } + } + + public init(mapKit: YMKMapKit = .sharedInstance()) { + self.mapKit = mapKit + } + + open override func apply(to mapView: YMKMapView) { + super.apply(to: mapView) + + if showUserLocation { + if userLocationLayer == nil { + userLocationLayer = mapKit?.createUserLocationLayer(with: mapView.mapWindow) + } + + let currentLocationIcon = currentLocationIcon(showHeadingArrow: showHeadingArrow) + + userLocationObjectListener = UserLocationObjectListener(tintColor: tintColor, + currentLocationIcon: currentLocationIcon) + } else { + userLocationObjectListener = nil + } + + userLocationLayer?.setVisibleWithOn(showUserLocation) + userLocationLayer?.setObjectListenerWith(userLocationObjectListener) + + let map = mapView.mapWindow.map + + map.isZoomGesturesEnabled = isZoomEnabled + map.isTiltGesturesEnabled = isTiltEnabled + map.isRotateGesturesEnabled = isRotationEnabled + } + + // MARK: - Subclass override + + open func currentLocationIcon(showHeadingArrow: Bool = true) -> UIImage { + let locationDrawingOperation = CurrentLocationDrawingOperation(mainColor: tintColor.cgColor, + borderColor: UIColor.white.cgColor, + showHeadingArrow: showHeadingArrow) + + let renderer = UIGraphicsImageRenderer(bounds: locationDrawingOperation.affectedArea()) + + return renderer.image { + locationDrawingOperation.apply(in: $0.cgContext) + } + } +}