feat: current location marker and other ui settings to supported maps

This commit is contained in:
Ivan Smolin 2023-05-31 17:13:54 +03:00
parent 5a74c342d9
commit a5bc2dc8f0
13 changed files with 489 additions and 23 deletions

View File

@ -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"),

View File

@ -24,9 +24,10 @@ import TIMapUtils
import MapKit
open class AppleMapManager<DataModel>: BaseMapManager<MKMapView,
ApplePlacemarkManager<DataModel>,
AppleClusterPlacemarkManager<DataModel>,
MKCameraUpdate> {
ApplePlacemarkManager<DataModel>,
AppleClusterPlacemarkManager<DataModel>,
MKCameraUpdate,
AppleMapUISettings> {
public typealias ClusteringIdentifier = String

View File

@ -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<MKMapView> {
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
}
}

View File

@ -24,9 +24,10 @@ import TIMapUtils
import GoogleMaps
open class GoogleMapManager<DataModel>: BaseMapManager<GMSMapView,
GooglePlacemarkManager<DataModel>,
GoogleClusterPlacemarkManager<DataModel>,
GMSCameraUpdate> {
GooglePlacemarkManager<DataModel>,
GoogleClusterPlacemarkManager<DataModel>,
GMSCameraUpdate,
GoogleMapUISettings> {
public init<IF: MarkerIconFactory, CIF: MarkerIconFactory>(map: GMSMapView,
positionGetter: @escaping PositionGetter,

View File

@ -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<GMSMapView> {
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
}
}

View File

@ -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

View File

@ -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)
}
}

View File

@ -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,

View File

@ -26,10 +26,12 @@ import UIKit.UIGeometry
open class BaseMapManager<Map: AnyObject,
PM: PlacemarkManager,
CPM: PlacemarkManager,
CUF: CameraUpdateFactory> 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<Map: AnyObject,
public var cameraUpdateOnMarkersAdded: CUF.Update?
public var settings: MS?
public var cameraUpdateOnMarkerTap: CameraUpdateOnPointTap? = {
CUF.update(for: .focus(target: $0, zoom: CommonZoomLevels.building.floatValue))
}
@ -109,4 +113,10 @@ open class BaseMapManager<Map: AnyObject,
open func remove(clusterPlacemarkManager: CPM) {
// override in subclass
}
open func apply(settings: MS) {
self.settings = settings
settings.apply(to: map)
}
}

View File

@ -0,0 +1,36 @@
//
// 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.
//
open class BaseMapUISettings<MapView>: 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
}
}

View File

@ -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)
}

View File

@ -24,9 +24,10 @@ import TIMapUtils
import YandexMapsMobile
open class YandexMapManager<DataModel>: BaseMapManager<YMKMapView,
YandexPlacemarkManager<DataModel>,
YandexClusterPlacemarkManager<DataModel>,
YMKCameraUpdate> {
YandexPlacemarkManager<DataModel>,
YandexClusterPlacemarkManager<DataModel>,
YMKCameraUpdate,
YandexMapUISettings> {
public init<IF: MarkerIconFactory, CIF: MarkerIconFactory>(map: YMKMapView,
positionGetter: @escaping PositionGetter,

View File

@ -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<YMKMapView> {
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)
}
}
}