-
Notifications
You must be signed in to change notification settings - Fork 325
Add initial implementation of delay information views. #2634
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| import UIKit | ||
|
|
||
| class DelayInformationView: UIView { | ||
|
|
||
| override func draw(_ rect: CGRect) { | ||
| let lineWidth: CGFloat = 2 | ||
| let cornerRadius: CGFloat = 3 | ||
| let tipLength: CGFloat = 6 | ||
| let fillColor: UIColor = .red | ||
| let strokeColor: UIColor = .black | ||
|
|
||
| let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2) | ||
| let path = UIBezierPath() | ||
| path.move(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - tipLength)) | ||
| path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - tipLength - cornerRadius), controlPoint: CGPoint(x: rect.minX, y: rect.maxY - tipLength)) | ||
| path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius)) | ||
| path.addQuadCurve(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY), controlPoint: CGPoint(x: rect.minX, y: rect.minY)) | ||
| path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY)) | ||
| path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + cornerRadius), controlPoint: CGPoint(x: rect.maxX, y: rect.minY)) | ||
| path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - tipLength - cornerRadius)) | ||
| path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - tipLength), controlPoint: CGPoint(x: rect.maxX, y: rect.maxY - tipLength)) | ||
| path.addLine(to: CGPoint(x: rect.midX + tipLength, y: rect.maxY - tipLength)) | ||
| path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) | ||
| path.addLine(to: CGPoint(x: rect.midX - tipLength, y: rect.maxY - tipLength)) | ||
| path.close() | ||
|
|
||
| fillColor.setFill() | ||
| path.fill() | ||
|
|
||
| strokeColor.setStroke() | ||
| path.lineWidth = lineWidth | ||
| path.stroke() | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,6 +60,29 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
| */ | ||
| public weak var courseTrackingDelegate: NavigationMapViewCourseTrackingDelegate? | ||
|
|
||
| /** | ||
| Controls whether delays are shown along the route line. | ||
| */ | ||
| public var showsDelay: Bool = false { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now boolean flag is used to show/hide delay information views. Since delay information is provided for |
||
| didSet { | ||
| guard let style = style else { return } | ||
|
|
||
| if showsDelay { | ||
| guard let routes = routes else { return } | ||
| let delayInformationSource = addDelayInformationSource(style, routes: routes) | ||
| addDelayLayer(style, source: delayInformationSource) | ||
| } else { | ||
| if let delaySource = style.source(withIdentifier: SourceIdentifier.delay) { | ||
| style.removeSource(delaySource) | ||
| } | ||
|
|
||
| if let delayLayer = style.layer(withIdentifier: StyleLayerIdentifier.delay) { | ||
| style.removeLayer(delayLayer) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| let sourceOptions: [MGLShapeSourceOption: Any] = [.maximumZoomLevel: 16] | ||
|
|
||
| struct SourceIdentifier { | ||
|
|
@@ -78,6 +101,8 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
| static let mainRouteCasing = "\(identifierNamespace).mainRouteCasing" | ||
|
|
||
| static let buildingExtrusion = "\(identifierNamespace).buildingExtrusion" | ||
|
|
||
| static let delay = "\(identifierNamespace).delay" | ||
| } | ||
|
|
||
| struct StyleLayerIdentifier { | ||
|
|
@@ -103,6 +128,8 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
| static let instructionCircle = "\(identifierNamespace).instructionCircle" | ||
|
|
||
| static let buildingExtrusion = "\(identifierNamespace).buildingExtrusion" | ||
|
|
||
| static let delay = "\(identifierNamespace).delay" | ||
| } | ||
|
|
||
| // MARK: - Instance Properties | ||
|
|
@@ -480,6 +507,11 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
| let mainRouteCasingLayer = addMainRouteCasingLayer(style, source: mainRouteCasingSource, lineGradient: routeCasingGradient(0.0), below: mainRouteLayer) | ||
| let alternativeRoutesLayer = addAlternativeRoutesLayer(style, source: allRoutesSource, below: mainRouteCasingLayer) | ||
| addAlternativeRoutesCasingLayer(style, source: allRoutesSource, below: alternativeRoutesLayer) | ||
|
|
||
| if showsDelay { | ||
| let delayInformationSource = addDelayInformationSource(style, routes: routes) | ||
| addDelayLayer(style, source: delayInformationSource) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Route line insertion methods | ||
|
|
@@ -509,6 +541,24 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
|
|
||
| return mainRouteCasingSource | ||
| } | ||
|
|
||
| func addDelayInformationSource(_ style: MGLStyle, routes: [Route]) -> MGLSource { | ||
| if let delayInformationSource = style.source(withIdentifier: SourceIdentifier.delay) as? MGLShapeSource { | ||
| return delayInformationSource | ||
| } | ||
|
|
||
| let delayInformationSource = MGLShapeSource(identifier: SourceIdentifier.delay, features: delayFeatures(routes), options: nil) | ||
| style.addSource(delayInformationSource) | ||
|
|
||
| // TODO: Improve delay view size calculation for various delay values. | ||
| let delayInformationView = DelayInformationView(frame: CGRect(x: 0, y: 0, width: 80, height: 40)) | ||
| delayInformationView.backgroundColor = .clear | ||
| if let imageRepresentation = delayInformationView.imageRepresentation { | ||
| style.setImage(imageRepresentation, forName: "delay_information") | ||
| } | ||
|
|
||
| return delayInformationSource | ||
| } | ||
|
|
||
| @discardableResult func addMainRouteLayer(_ style: MGLStyle, source: MGLSource, lineGradient: NSExpression?) -> MGLStyleLayer { | ||
| let customMainRouteLayer = navigationMapViewDelegate?.navigationMapView(self, | ||
|
|
@@ -655,6 +705,36 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
|
|
||
| return alternativeRoutesCasingLayer | ||
| } | ||
|
|
||
| @discardableResult func addDelayLayer(_ style: MGLStyle, source: MGLSource) -> MGLStyleLayer { | ||
| if let delayLayer = style.layer(withIdentifier: StyleLayerIdentifier.delay) { | ||
| return delayLayer | ||
| } | ||
|
|
||
| let opacityStops = [ | ||
| 11: 0.0, | ||
| 13: 0.7, | ||
| 15: 1 | ||
| ] | ||
|
|
||
| let delayLayer = MGLSymbolStyleLayer(identifier: StyleLayerIdentifier.delay, source: source) | ||
| delayLayer.iconImageName = NSExpression(forConstantValue: "delay_information") | ||
| delayLayer.iconScale = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", [11: 0.05, 13: 0.5, 15: 1]) | ||
| delayLayer.iconOpacity = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", opacityStops) | ||
| delayLayer.iconAnchor = NSExpression(forConstantValue: NSValue(mglIconAnchor: .bottom)) | ||
| delayLayer.iconHaloColor = NSExpression(forConstantValue: UIColor.black) | ||
| delayLayer.text = NSExpression(forKeyPath: "delay") | ||
| delayLayer.textColor = NSExpression(forConstantValue: UIColor.white) | ||
| delayLayer.textFontSize = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", [11: 0.05, 13: 7, 15: 15]) | ||
| delayLayer.textOpacity = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", opacityStops) | ||
| delayLayer.maximumTextWidth = NSExpression(forConstantValue: 80.0) | ||
| delayLayer.textAnchor = NSExpression(forConstantValue: NSValue(mglTextAnchor: .bottom)) | ||
| delayLayer.textOffset = NSExpression(forConstantValue: CGVector(dx: 0, dy: -1)) | ||
|
|
||
| style.addLayer(delayLayer) | ||
|
|
||
| return delayLayer | ||
| } | ||
|
|
||
| // MARK: - Vanishing route line methods | ||
|
|
||
|
|
@@ -823,11 +903,13 @@ open class NavigationMapView: MGLMapView, UIGestureRecognizerDelegate { | |
| StyleLayerIdentifier.mainRoute, | ||
| StyleLayerIdentifier.mainRouteCasing, | ||
| StyleLayerIdentifier.alternativeRoutes, | ||
| StyleLayerIdentifier.alternativeRoutesCasing | ||
| StyleLayerIdentifier.alternativeRoutesCasing, | ||
| StyleLayerIdentifier.delay | ||
| ].compactMap { style.layer(withIdentifier: $0) }) | ||
| style.remove(Set([ | ||
| SourceIdentifier.allRoutes, | ||
| SourceIdentifier.mainRouteCasing | ||
| SourceIdentifier.mainRouteCasing, | ||
| SourceIdentifier.delay | ||
| ].compactMap { style.source(withIdentifier: $0) })) | ||
|
|
||
| routes = nil | ||
|
|
@@ -1525,3 +1607,51 @@ extension NavigationMapView { | |
| highlightedBuildingsLayer.fillExtrusionOpacity = NSExpression(format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)", opacityStops) | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Delay Information | ||
|
|
||
| extension NavigationMapView { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since |
||
|
|
||
| func delay(_ expectedTravelTime: TimeInterval, typicalTravelTime: TimeInterval?) -> String { | ||
| var delay = "" | ||
| // TODO: Handle case when typicalTravelTime is larger than expectedTravelTime. | ||
| // TODO: Improve size calculation for various delays. | ||
| if let typicalTravelTime = typicalTravelTime, expectedTravelTime >= typicalTravelTime { | ||
| let timeDifference = Int(expectedTravelTime - typicalTravelTime) | ||
| let (hours, minutes) = (timeDifference / 3600, (timeDifference % 3600) / 60) | ||
|
|
||
| if hours > 0 { | ||
| delay += "+\(hours) hr " | ||
| } | ||
|
|
||
| if minutes > 0 { | ||
| if delay.isEmpty { delay += "+" } | ||
| delay += "\(minutes) min" | ||
| } | ||
| } | ||
|
|
||
| return delay | ||
| } | ||
|
|
||
| func delayFeatures(_ routes: [Route]) -> [MGLPointFeature] { | ||
| var features = [MGLPointFeature]() | ||
|
|
||
| routes.forEach { | ||
| $0.legs.forEach { | ||
| $0.steps.forEach { | ||
| let totalDelay = delay($0.expectedTravelTime, typicalTravelTime: $0.typicalTravelTime) | ||
| if !totalDelay.isEmpty { | ||
| let pointFeature = MGLPointFeature() | ||
| pointFeature.coordinate = $0.maneuverLocation | ||
| pointFeature.attributes = [ | ||
| "delay": totalDelay | ||
| ] | ||
| features.append(pointFeature) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return features | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really like creating
DelayInformationViewwhich is then later converted toUIImageso that it's possible to use it inMGLSymbolStyleLayer(there are some additional issues like text overlapping etc).On the other hand seems that
MGLPointAnnotationwill not be performant enough when using large amount of delay annotations. It'll also require implementation ofMGMapViewDelegatemethods insideNavigationMapView.@1ec5 if you have any better ideas please let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@MaximAlien have you seen https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#layout-symbol-icon-text-fit? Might be useful here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/cc @langsmith ^^^ in case we've not considered this for the Android impl.