Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Annotate intersecting roads and maneuver points along the current route #2928

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add visible annotations to the map at intersecting roads along the ac…
…tive route as well as at maneuver points.
avi-c authored and 1ec5 committed May 17, 2022
commit ebe9bb5edbfc9ce3671b3f5d5b0f1a0e199db40e
12 changes: 12 additions & 0 deletions Example/NavigationMapView+RoadAnnotations.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import UIKit
import Turf
import MapboxDirections
import MapboxCoreNavigation
import MapboxNavigation
import MapboxCoreMaps
import MapboxMaps

// MARK: - Visible annotations on the map about the current drive

extension NavigationMapView {
}
2 changes: 2 additions & 0 deletions Example/ViewController.swift
Original file line number Diff line number Diff line change
@@ -573,6 +573,7 @@ class ViewController: UIViewController {
completion: CompletionHandler? = nil) {
navigationViewController.modalPresentationStyle = .fullScreen
activeNavigationViewController = navigationViewController
activeNavigationViewController?.showIntersectionAnnotations = true

present(navigationViewController, animated: true) {
completion?()
@@ -591,6 +592,7 @@ class ViewController: UIViewController {

func dismissActiveNavigationViewController() {
activeNavigationViewController?.dismiss(animated: true) {
self.activeNavigationViewController?.showIntersectionAnnotations = false
self.activeNavigationViewController = nil
}
}
6 changes: 6 additions & 0 deletions MapboxNavigation-SPM.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -35,6 +35,8 @@
DA303CA021B76B5C00F921DC /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA303C9D21B76B5C00F921DC /* LaunchScreen.storyboard */; };
DA303CA421B76CB000F921DC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DA303CA221B76CB000F921DC /* Localizable.stringsdict */; };
DA303CA521B76CB000F921DC /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = DA303CA221B76CB000F921DC /* Localizable.stringsdict */; };
DA493E282833FED5006D09AB /* NavigationMapView+RoadAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA493E272833FED4006D09AB /* NavigationMapView+RoadAnnotations.swift */; };
DA493E292833FED5006D09AB /* NavigationMapView+RoadAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA493E272833FED4006D09AB /* NavigationMapView+RoadAnnotations.swift */; };
DA8805002316EAED00B54D87 /* ViewController+InstructionsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = AED6285522CBE4CE00058A51 /* ViewController+InstructionsCard.swift */; };
DAF5BE4A26A1FD1200DD3F2B /* MapboxGeocoder in Frameworks */ = {isa = PBXBuildFile; productRef = DAF5BE4926A1FD1200DD3F2B /* MapboxGeocoder */; };
E27A2204265674E400AA935F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E27A2202265674E400AA935F /* Localizable.strings */; };
@@ -81,6 +83,7 @@
DA3327391F50C6DA00C5EE88 /* sl */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = sl; path = sl.lproj/Main.strings; sourceTree = "<group>"; };
DA33273D1F50C7CA00C5EE88 /* uk */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Main.strings; sourceTree = "<group>"; };
DA3525712011435E0048DDFC /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Main.strings; sourceTree = "<group>"; };
DA493E272833FED4006D09AB /* NavigationMapView+RoadAnnotations.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "NavigationMapView+RoadAnnotations.swift"; path = "Example/NavigationMapView+RoadAnnotations.swift"; sourceTree = "<group>"; };
DA545ABA1FA993DF0090908E /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Main.strings; sourceTree = "<group>"; };
DA545ABE1FA9A1370090908E /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Main.strings; sourceTree = "<group>"; };
DA5AD03C1FEBA03700FC7D7B /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Main.strings; sourceTree = "<group>"; };
@@ -152,6 +155,7 @@
C51FC31620F689F800400CE7 /* CustomStyles.swift */,
C5D9800C1EFA8BA9006DBF2E /* CustomViewController.swift */,
8A0D5DB525DF2A86006F0919 /* StyledFeature.swift */,
DA493E272833FED4006D09AB /* NavigationMapView+RoadAnnotations.swift */,
);
name = Example;
sourceTree = "<group>";
@@ -388,6 +392,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
DA493E282833FED5006D09AB /* NavigationMapView+RoadAnnotations.swift in Sources */,
358D14681E5E3B7700ADE590 /* ViewController.swift in Sources */,
C5D9800D1EFA8BA9006DBF2E /* CustomViewController.swift in Sources */,
AED6285622CBE4CE00058A51 /* ViewController+InstructionsCard.swift in Sources */,
@@ -405,6 +410,7 @@
files = (
C53F2EE420EBC95600D9798F /* ViewController.swift in Sources */,
C53F2EE520EBC95600D9798F /* CustomViewController.swift in Sources */,
DA493E292833FED5006D09AB /* NavigationMapView+RoadAnnotations.swift in Sources */,
C5DE4B6220F6B6B3007AFBE6 /* CustomStyles.swift in Sources */,
8A0D5DB725DF2A86006F0919 /* StyledFeature.swift in Sources */,
DA8805002316EAED00B54D87 /* ViewController+InstructionsCard.swift in Sources */,
11 changes: 11 additions & 0 deletions MapboxNavigation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
@@ -485,6 +485,10 @@
E2DAFABA27BCF3C200BA12BD /* RoutesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DAFAB927BCF3C200BA12BD /* RoutesCoordinator.swift */; };
E2F08C70269DB17C002EFDC5 /* AccessToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2F08C6F269DB17C002EFDC5 /* AccessToken.swift */; };
F46FF187260277F7007CC0E0 /* DateComponentsFormatter+NavigationAdditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = F46FF186260277F7007CC0E0 /* DateComponentsFormatter+NavigationAdditions.swift */; };
F43EE329261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */; };
F43EE32A261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */; };
F488A0BE26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift in Sources */ = {isa = PBXBuildFile; fileRef = F488A0BD26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift */; };
F488A0C826261D8100A4CC8C /* ElectronicHorizon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F488A0C726261D8100A4CC8C /* ElectronicHorizon.swift */; };
/* End PBXBuildFile section */

/* Begin PBXContainerItemProxy section */
@@ -1139,6 +1143,9 @@
E2DAFAB927BCF3C200BA12BD /* RoutesCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoutesCoordinator.swift; sourceTree = "<group>"; };
E2F08C6F269DB17C002EFDC5 /* AccessToken.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccessToken.swift; sourceTree = "<group>"; };
F46FF186260277F7007CC0E0 /* DateComponentsFormatter+NavigationAdditions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "DateComponentsFormatter+NavigationAdditions.swift"; sourceTree = "<group>"; };
F43EE328261F98DC0039D56F /* NavigationMapView+RoadAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationMapView+RoadAnnotations.swift"; sourceTree = "<group>"; };
F488A0BD26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationMapView+IntersectionAnnotations.swift"; sourceTree = "<group>"; };
F488A0C726261D8100A4CC8C /* ElectronicHorizon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElectronicHorizon.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
@@ -1868,6 +1875,8 @@
8A11FEEF27A3514C00285B6F /* CPRouteChoice.swift */,
8AD220AA27C091EE000734A5 /* Solar.swift */,
8AD220AE27C09544000734A5 /* Date.swift */,
F488A0BD26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift */,
F488A0C726261D8100A4CC8C /* ElectronicHorizon.swift */,
);
name = Extensions;
sourceTree = "<group>";
@@ -2712,6 +2721,7 @@
8AD2210F27C434CD000734A5 /* TitleLabel.swift in Sources */,
8A50A3CB26EC09FB00894A8E /* FeedbackSubtypeCollectionViewCell.swift in Sources */,
8A50A3D326EC0AE100894A8E /* IdleTimerManager.swift in Sources */,
F488A0C826261D8100A4CC8C /* ElectronicHorizon.swift in Sources */,
8D5DFFF1207C04840093765A /* NSAttributedString.swift in Sources */,
8AD2211F27C43D11000734A5 /* FloatingButton.swift in Sources */,
8A2081CB25E07CED00F9B8A6 /* NavigationMapViewIdentifiers.swift in Sources */,
@@ -2745,6 +2755,7 @@
2EBF20AE25D6F89000DB7BF2 /* Utils.swift in Sources */,
160D8279205996DA00D278D6 /* DataCache.swift in Sources */,
351BEBF21E5BCC63006FE110 /* Style.swift in Sources */,
F488A0BE26261C4600A4CC8C /* NavigationMapView+IntersectionAnnotations.swift in Sources */,
43FB386923A202420064481E /* Route.swift in Sources */,
3EA937B1F4DF73EB004BA6BE /* InstructionPresenter.swift in Sources */,
5A1C075824BDEB44000A6330 /* PassiveLocationProvider.swift in Sources */,
2 changes: 2 additions & 0 deletions Sources/MapboxCoreNavigation/CoreConstants.swift
Original file line number Diff line number Diff line change
@@ -483,6 +483,8 @@ extension RoadGraph {
/**
A key in the user info dictionary of a `Notification.Name.electronicHorizonDidEnterRoadObject` or `Notification.Name.electronicHorizonDidExitRoadObject` notification. The corresponding value is a `RoadObject.Identifier` identifying the road object that the user entered or exited. */
public static let roadObjectIdentifierKey: NotificationUserInfoKey = .init(rawValue: "roadObjectIdentifier")

public static let roadGraphIdentifierKey: NotificationUserInfoKey = .init(rawValue: "roadGraph")

/**
A key in the user info dictionary of a `Notification.Name.electronicHorizonDidEnterRoadObject` or `Notification.Name.electronicHorizonDidExitRoadObject` notification. The corresponding value is an `NSNumber` containing a Boolean value set to `true` if the user entered at the beginning or exited at the end of the road object, or `false` if they entered or exited somewhere along the road object. */
Original file line number Diff line number Diff line change
@@ -313,6 +313,7 @@ class NavigatorElectronicHorizonObserver: ElectronicHorizonObserver {
.treeKey: RoadGraph.Edge(position.tree().start),
.updatesMostProbablePathKey: position.type() == .update,
.distancesByRoadObjectKey: distances.map(DistancedRoadObject.init),
.roadGraphIdentifierKey: Navigator.shared.roadGraph,
]
NotificationCenter.default.post(name: .electronicHorizonDidUpdatePosition, object: nil, userInfo: userInfo)
}
38 changes: 38 additions & 0 deletions Sources/MapboxNavigation/ElectronicHorizon.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import MapboxCoreNavigation

extension RoadGraph.Edge {
var mpp: [RoadGraph.Edge]? {

guard level == 0 else { return nil }

var mostProbablePath = [self]

for child in outletEdges {
if let childMPP = child.mpp {
mostProbablePath.append(contentsOf: childMPP)
}
}

return mostProbablePath
}

func edgeNames(roadGraph: RoadGraph) -> [String] {
guard let metadata = roadGraph.edgeMetadata(edgeIdentifier: identifier) else {
return []
}
let names = metadata.names.map { name -> String in
switch name {
case .name(let name):
return name
case .code(let code):
return "(\(code))"
}
}

// If the road is unnamed, fall back to the road class.
if names.isEmpty {
return ["\(metadata.mapboxStreetsRoadClass.rawValue)"]
}
return names
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import CoreLocation
import UIKit
import MapboxDirections
import MapboxCoreNavigation
import Turf
import MapboxMaps

extension NavigationMapView {

struct EdgeIntersection {
var root: RoadGraph.Edge
var branch: RoadGraph.Edge
var rootMetadata: RoadGraph.Edge.Metadata
var rootShape: LineString
var branchMetadata: RoadGraph.Edge.Metadata
var branchShape: LineString

var coordinate: CLLocationCoordinate2D? {
rootShape.coordinates.first
}

var annotationPoint: CLLocationCoordinate2D? {
guard let length = branchShape.distance() else { return nil }
let targetDistance = min(length / 2, Double.random(in: 15...30))
guard let annotationPoint = branchShape.coordinateFromStart(distance: targetDistance) else { return nil }
return annotationPoint
}

var wayName: String? {
guard let roadName = rootMetadata.names.first else { return nil }

switch roadName {
case .name(let name):
return name
case .code(let code):
return "(\(code))"
}
}
var intersectingWayName: String? {
guard let roadName = branchMetadata.names.first else { return nil }

switch roadName {
case .name(let name):
return name
case .code(let code):
return "(\(code))"
}
}

var incidentAngle: CLLocationDegrees {
return (branchMetadata.heading - rootMetadata.heading).wrap(min: 0, max: 360)
}

var description: String {
return "EdgeIntersection: root: \(wayName ?? "") intersection: \(intersectingWayName ?? "") coordinate: \(String(describing: coordinate))"
}
}

enum AnnotationTailPosition: Int {
case left
case right
case center
}

class AnnotationCacheEntry: Equatable, Hashable {
var wayname: String
var coordinate: CLLocationCoordinate2D
var intersection: EdgeIntersection?
var feature: Feature
var lastAccessTime: Date

init(coordinate: CLLocationCoordinate2D, wayname: String, intersection: EdgeIntersection? = nil, feature: Feature) {
self.wayname = wayname
self.coordinate = coordinate
self.intersection = intersection
self.feature = feature
self.lastAccessTime = Date()
}

static func == (lhs: AnnotationCacheEntry, rhs: AnnotationCacheEntry) -> Bool {
return lhs.wayname == rhs.wayname
}

func hash(into hasher: inout Hasher) {
hasher.combine(wayname.hashValue)
}
}

class AnnotationCache {
private let maxEntryAge = TimeInterval(30)
var entries = Set<AnnotationCacheEntry>()
var cachePruningTimer: Timer?

init() {
// periodically prune the cache to remove entries that have been passed already
cachePruningTimer = Timer.scheduledTimer(withTimeInterval: 15, repeats: true, block: { [weak self] _ in
self?.prune()
})
}

deinit {
cachePruningTimer?.invalidate()
cachePruningTimer = nil
}

func setValue(feature: Feature, coordinate: CLLocationCoordinate2D, intersection: EdgeIntersection?, for wayname: String) {
entries.insert(AnnotationCacheEntry(coordinate: coordinate, wayname: wayname, intersection: intersection, feature: feature))
}

func value(for wayname: String) -> AnnotationCacheEntry? {
let matchingEntry = entries.first { entry -> Bool in
entry.wayname == wayname
}

if let matchingEntry = matchingEntry {
// update the timestamp used for pruning the cache
matchingEntry.lastAccessTime = Date()
}

return matchingEntry
}

private func prune() {
let now = Date()

entries.filter { now.timeIntervalSince($0.lastAccessTime) > maxEntryAge }.forEach { remove($0) }
}

public func remove(_ entry: AnnotationCacheEntry) {
entries.remove(entry)
}
}
}
423 changes: 423 additions & 0 deletions Sources/MapboxNavigation/NavigationMapView.swift

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Sources/MapboxNavigation/NavigationMapViewIdentifiers.swift
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@ extension NavigationMapView {
static let waypointSymbolLayer = "\(identifier)_waypointSymbolLayer"
static let buildingExtrusionLayer = "\(identifier)_buildingExtrusionLayer"
static let routeDurationAnnotationsLayer: String = "\(identifier)_routeDurationAnnotationsLayer"
static let intersectionAnnotationsLayer = "\(identifier)_intersectionAnnotationsLayer"
static let puck2DLayer: String = "puck"
static let puck3DLayer: String = "puck-model-layer"
}
@@ -26,6 +27,7 @@ extension NavigationMapView {
static let voiceInstructionSource = "\(identifier)_instructionSource"
static let waypointSource = "\(identifier)_waypointSource"
static let routeDurationAnnotationsSource: String = "\(identifier)_routeDurationAnnotationsSource"
static let intersectionAnnotationsSource = "\(identifier)_intersectionAnnotationsSource"
static let puck3DSource: String = "puck-model-source"
}

19 changes: 19 additions & 0 deletions Sources/MapboxNavigation/NavigationViewController.swift
Original file line number Diff line number Diff line change
@@ -81,6 +81,22 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter
}
}

public var showIntersectionAnnotations: Bool {
get {
navigationMapView?.showIntersectionAnnotations ?? false
}
set {
navigationMapView?.showIntersectionAnnotations = newValue
if let routeController = router as? RouteController {
if newValue {
routeController.startUpdatingElectronicHorizon(with: nil)
} else {
routeController.stopUpdatingElectronicHorizon()
}
}
}
}

// MARK: Configuring Spoken Instructions

/**
@@ -328,6 +344,7 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter
}

deinit {
showIntersectionAnnotations = false
navigationService?.stop()
}

@@ -417,6 +434,8 @@ open class NavigationViewController: UIViewController, NavigationStatusPresenter
viewObservers.forEach {
$0?.navigationViewDidDisappear(animated)
}

showIntersectionAnnotations = false
}

open override func viewDidLayoutSubviews() {
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "AnnotationCentered.png",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These assets should be stored as scalable PDFs. #2734/#2873 introduced more stylish RouteInfoAnnotationLeftHanded and RouteInfoAnnotationRightHanded assets, but there’s no corresponding RouteInfoAnnotationCentered asset.

"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "RouteInfoAnnotationLeftHanded.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "RouteInfoAnnotationRightHanded.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions Sources/MapboxNavigation/RouteLineController.swift
Original file line number Diff line number Diff line change
@@ -83,6 +83,11 @@ extension NavigationMapView {
let stepIndex = progress.currentLegProgress.stepIndex

navigationMapView.updatePreferredFrameRate(for: progress)
do {
try navigationMapView.updateAnnotations(for: progress)
} catch {
print(error)
}
if currentLegIndexMapped != legIndex {
navigationMapView.showWaypoints(on: route, legIndex: legIndex)
navigationMapView.show([route], legIndex: legIndex)
4 changes: 4 additions & 0 deletions Sources/MapboxNavigation/UIColor.swift
Original file line number Diff line number Diff line change
@@ -41,6 +41,10 @@ extension UIColor {
class var alternativeTrafficSevere: UIColor { #colorLiteral(red: 0.71, green: 0.51, blue: 0.51, alpha: 1.0) }
class var defaultBuildingColor: UIColor { #colorLiteral(red: 0.9833194452, green: 0.9843137255, blue: 0.9331936657, alpha: 0.8019049658) }
class var defaultBuildingHighlightColor: UIColor { #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 0.949406036) }
class var intersectionAnnotationDefaultBackgroundColor: UIColor { get { return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } }
class var intersectionAnnotationSelectedBackgroundColor: UIColor { get { return #colorLiteral(red: 0.337254902, green: 0.6588235294, blue: 0.9843137255, alpha: 1) } }
class var intersectionAnnotationDefaultLabelColor: UIColor { get { return #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) } }
class var intersectionAnnotationSelectedLabelColor: UIColor { get { return #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1) } }

class var defaultRouteRestrictedAreaColor: UIColor { #colorLiteral(red: 0, green: 0, blue: 0, alpha: 1) }

2 changes: 2 additions & 0 deletions Sources/MapboxNavigation/UIImage.swift
Original file line number Diff line number Diff line change
@@ -74,6 +74,8 @@ extension UIImage {
return scaledImage
}

// Produce a copy of the image with tint color applied.
// Useful for deployment to iOS versions prior to 13 where tinting support was added to UIImage natively.
func tint(_ tintColor: UIColor) -> UIImage {
let imageSize = size
let imageScale = scale