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
Show current way name #155
Conversation
What do you think of the consideration in #142 (comment)? OSRM is suppressing name changes that are unworthy of a voice notification but worthy of an UI update. That tradeoff still makes some sense to me, but it does complicate this feature. |
// Somehow check the current style has mapbox streets | ||
// | ||
//let streets = MGLSource(identifier: "mapbox://mapbox-streets-v7") | ||
//guard mapView.style!.sources.contains(streets) else { |
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.
Here’s an example of checking whether a source is Mapbox Streets. You’d want to check whether any source in the style is Mapbox Streets.
if let userPuck = userPuck { | ||
let features = mapView.visibleFeatures(in: userPuck.frame) | ||
|
||
for layer in 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.
visibleFeatures(in:)
returns an array of features, not an array of layers. The iOS SDK currently lacks a way to tell what style layer each visible feature belongs to: mapbox/mapbox-gl-native#7162. As a workaround, you can call visibleFeatures(in:styleLayerIdentifiers:)
multiple times, once per relevant layer.
for layer in features { | ||
|
||
// TODO: check for acceptable classes | ||
// if let feature = layer as? MGLPolylineFeature, |
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.
MGLPolylineFeature is a kind of feature; a feature is part of a shape or vector source. But note for example that a single vector source might include features of many types at the same time.
You want to find out which style layers would display roads, which are linear. A style layer that renders linear shapes is an MGLLineStyleLayer. After you filter the style’s list of layers to include only MGLLineStyleLayers, you can further filter the list to layers whose source is Mapbox Streets and whose source layer identifier is road
.
You could also filter the layers by their predicates, but note that predicates can be arbitrarily complex, so this might be more trouble than it’s worth. For example, if you wanted to know whether a layer’s predicate includes only motorways, you’d have to account for predicates such as (class IN {'motorway_link', 'trunk_link'} AND class != 'motorway' AND oneway = FALSE) OR layer = -2
etc.
// if let feature = layer as? MGLPolylineFeature, | ||
|
||
// TODO: Localize | ||
if let name = layer.attribute(forKey: "name") as? String { |
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.
Should this match the map’s language or the UI language? Should we automatically localize the map to match the UI language (mapbox/ios-sdk-examples#36)?
Updated the logic a bit in 06fac74 but now I'm not getting any features |
@@ -268,6 +270,56 @@ extension RouteMapViewController: NavigationMapViewDelegate { | |||
return defaultReturn | |||
} | |||
|
|||
if let userLocation = mapView.userLocation, | |||
let style = mapView.style, | |||
routeController.showCurrentWayNameLabel == true { |
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.
If showCurrentWayNameLabel
is a Boolean, is it necessary to compare it to true
?
return false | ||
} | ||
} else { | ||
return false |
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.
This filter
call is more complex than it needs to be:
let streetsSourceIdentifiers = style.sources.flatMap {
$0 as? MGLVectorSource
}.filter {
$0.configurationURL?.absoluteString.range(of: "mapbox.mapbox-streets-v7") != nil
}.map {
$0.identifier
}
return false | ||
} | ||
} else { | ||
return false |
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 hope we never release a mapbox.mapbox-streets-v7.1
. 😉
Implement this extension:
extension MGLVectorSource {
var isMapboxStreets: Bool {
guard let configurationURL = configurationURL else {
return false
}
return configurationURL.scheme = "mapbox" && configurationURL.host.components(separatedBy: ",").contains("mapbox.mapbox-streets-v7")
}
}
then filter on it:
let streetsSourceIdentifiers = style.sources.flatMap {
$0 as? MGLVectorSource
}.filter {
$0.isMapboxStreets
}.map {
$0.identifier
}
$0 as? MGLLineStyleLayer | ||
} | ||
|
||
let sourceIdenitifers = validSources.map { |
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.
Typo: Idenitifers
if let identifier = $0.sourceIdentifier { | ||
return sourceIdenitifers.contains(identifier) && $0.sourceLayerIdentifier == "road" | ||
} else { | ||
return false |
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.
This filter
could be written more succinctly as:
lineLayers.filter { streetsSourceIdentifiers.contains($0.sourceIdentifier ?? "") && $0.sourceLayerIdentifier == "road" }
Playing with macosapp a bit, I’m discovering that if you perform a query at the location marked by a
you get a feature with only the attributes that mbgl considers for rendering line layers (
you get the same, plus a feature from the symbol layer that includes the This wouldn’t be a problem with source querying, but source querying doesn’t allow you to specify a bounding box or point at which to query – it returns all loaded features. Unless I’m missing something, unfortunately this could rule out feature querying as a source for this street name label. |
Ah, that’s the |
@1ec5 ready for review again. Now, we're adding the |
@@ -37,7 +37,7 @@ open class RouteController: NSObject { | |||
|
|||
Note: the feature queries the underlying tile data and makes the assumption the current style includes the source `mapbox://mapbox.streets-v7`. All default Mapbox styles include this source. If you are using a custom source, make sure it is included when using this feature. Otherwise, it will be ignored. | |||
*/ | |||
public var showCurrentWayNameLabel = false | |||
public var showCurrentWayNameLabel = true |
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.
Does this still need to be an option?
@@ -42,6 +42,7 @@ class RouteMapViewController: UIViewController, PulleyPrimaryContentControllerDe | |||
var arrowCurrentStep: RouteStep? | |||
|
|||
let streetsLanguages = ["zh", "ru", "fr", "es", "en"] |
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.
According to this documentation, the Mapbox Streets v7 source’s road_label
layer has a name_ru
property. There’s also an undocumented name_zh-Hans
property that should be documented.
@@ -57,6 +58,10 @@ class RouteMapViewController: UIViewController, PulleyPrimaryContentControllerDe | |||
mapView.tintColor = NavigationUI.shared.tintColor | |||
recenterButton.tintColor = NavigationUI.shared.tintColor | |||
recenterButton.applyDefaultCornerRadiusShadow(cornerRadius: 22) | |||
wayNameLabel.applyDefaultCornerRadiusShadow() | |||
wayNameLabel.layer.masksToBounds = true | |||
wayNameLabel.insets = UIEdgeInsetsMake(1, 4, 1, 4) |
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.
Use the Swift-style UIEdgeInsets(top:left:bottom:right:)
instead of the C-style UIEdgeInsetsMake(_:_:_:_:)
.
let streetsSourceIdentifiers = streetsSources.map { | ||
$0.identifier | ||
} | ||
assert(!streetsSourceIdentifiers.isEmpty, "The option `showCurrentWayNameLabel` must contain the source `mapbox.mapbox-streets-v7`") |
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.
An option can’t contain a source. A style contains a source.
let streetsSourceIdentifiers = streetsSources.map { | ||
$0.identifier | ||
} | ||
assert(!streetsSourceIdentifiers.isEmpty, "The option `showCurrentWayNameLabel` must contain the source `mapbox.mapbox-streets-v7`") |
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.
If the style doesn’t contain Mapbox Streets source, we could add it.
for feature in features { | ||
|
||
var key = "name" | ||
if let language = Locale.preferredLanguages.first!.components(separatedBy: "-").first, |
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.
It’d be great to adapt this UI to the user’s preferred language, but are we doing that in the turn banner and step table and on the map? I think it’d be more important to be consistent throughout the UI. We could use the runtime styling API to localize the map, with code very similar to this code. However, the Directions API doesn’t yet return localized way names, so the turn banner and step table are stuck in the local language.
@@ -40,6 +41,9 @@ class RouteMapViewController: UIViewController, PulleyPrimaryContentControllerDe | |||
var shieldImageDownloadToken: SDWebImageDownloadToken? | |||
var arrowCurrentStep: RouteStep? | |||
|
|||
let streetsLanguages = ["zh", "ru", "fr", "es", "en"] | |||
let RoadLabelLayerIdentifier = "roadLabelLayer" |
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.
This property is only used in one method, and there won’t ever be a need for another class to know about it. Make this a local variable instead.
let style = mapView.style, | ||
recenterButton.isHidden { | ||
|
||
let streetsLanguages = ["zh", "ru", "fr", "es", "en"] |
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.
Also de
.
let streetsSourceIdentifiers = streetsSources.map { | ||
$0.identifier | ||
} | ||
assert(!streetsSourceIdentifiers.isEmpty, "Style must contain the source `mapbox.mapbox-streets-v7`") |
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.
If there isn’t a Streets-based source in the style, let’s create one instead of asserting.
|
||
var key = "name" | ||
if let language = Locale.preferredLanguages.first!.components(separatedBy: "-").first, | ||
streetsLanguages.contains(language) || language == "zh-Hans" { |
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.
If the first item in Locale.preferredLanguages
is zh-Hans
, language
is zh
, not zh-Hans
, and this special case won’t work.
currentWayNameCounter += 1 | ||
} | ||
print(currentWayNameCounter) | ||
if currentWayNameCounter >= 10 { |
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.
It takes ten location updates (at least ten seconds) to update the label? What if the user has already turned onto another road? Wouldn’t they get confused when they glance at the label in the meantime and find that it’s still the old road name?
Instead of delaying the update, let’s check whether the feature matches the route line. If more than one consecutive coordinates in the feature’s coordinates
are also in the route step’s coordinates
, then use the feature for the label. Otherwise, ignore the feature.
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.
Added Levenshtein comparison in 04cba87
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.
Instead of comparing names, let’s try to compare geometries. Consider the case where the user is traveling on a highway overpass over a residential surface street and we query at exactly the point where the two roads cross. Both roads intersect the query point, but only one of them matches the route step geometry beyond that point.
Look at the two coordinates along the road on either side of the query point. If either coordinate is contained by the current route step’s coordinates
array, that road matches and we should update the label for that road.
If we really want to consider the name’s edit distance, I guess we could use that as a fallback for the couple dozen (?) places in the U.S. where one road runs parallel above another road.
MapboxNavigation/String.swift
Outdated
return numbers.reduce(numbers[0], {$0 < $1 ? $0 : $1}) | ||
} | ||
|
||
func distanceFrom(string: String) -> Int{ |
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.
Let’s rely on an existing package instead of trying to maintain this functionality ourselves. I’m partial to MDCDamerauLevenshtein, which has the benefit of being more efficiently written by virtue of being written mostly in C. (I’m a bit skeptical of relying so heavily on array subscripting in Swift.)
return name.distanceFrom(string: $0) <= 6 | ||
} | ||
|
||
if !filteredStreetNames.isEmpty { |
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.
If I’m reading this code correctly, we’re trying to never update the label unless the user travels along similarly named streets for their entire trip. If the user actually travels from Main Street to Broadway Avenue, wouldn’t the edit distance be high enough that the label will never update to Broadway Avenue?
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.
OK, I misread the code: a “filtered” street name must be w/i 6 of any of the streets along the route. Presumably there would be an edit distance of 0 between Broadway Avenue and the Broadway Avenue in the route data. However, there’s also the problem of false positives: if the user drives along State Route 185 where it passes over State Route 85, the label will happily switch to “State Route 85” because of an edit distance of one.
style.addLayer(streetLabelLayer) | ||
} | ||
|
||
if let userPuck = mapView.view(for: userLocation), |
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.
All we need for querying is the coordinate (snapped to the route line), not the view itself. Use MGLMapView.convert(_:toCoordinateFrom:)
to get the point corresponding to the snapped location.
// todo: account for -10 | ||
let tenMetersAheadofFeature = coordinate(at: 5, fromStartOf: featureSlice)! | ||
let tenMetersAheadOfUser = coordinate(at: 5, fromStartOf: slicedLine)! | ||
let distanceBetwenAheadOfUserAndFeature = tenMetersAheadofFeature - tenMetersAheadOfUser |
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.
@1ec5 I feel like in some cases we're looking at either end of the the query point:
This is 10m
<-----5m----*-----5m---->
Instead of
On the money
*-----5m---->
*-----5m---->
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.
Oddly enough, this is double the distance we're looking ahead. If I change this value, it is always the sum of tenMetersAheadOfUser and tenMetersAheadofFeature.
This is because of:
// todo: account for -10
On a two-way street, the route line may travel in the direction opposite the polyline feature.
let pointAheadFeature = coordinate(at: lookAheadDistance, fromStartOf: featureSlice)! | ||
let pointAheadUser = coordinate(at: lookAheadDistance, fromStartOf: slicedLine)! | ||
let pointBehindFeature = coordinate(at: -lookAheadDistance, fromStartOf: featureSlice)! | ||
let pointBehindUser = coordinate(at: -lookAheadDistance, fromStartOf: slicedLine)! |
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.
@1ec5 how can i clean this up? Even when i guard here, it crashes. I'm not sure if coordinate(at:) supports negative values.
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.
Correct, coordinate(at:fromStartOf:)
doesn’t support negative distances. We could either create a coordinate(at:fromEndOf:)
method or pass in the reversed()
coordinate array.
@frederoni when you get in, could you take a look at adding style support to this PR? Ideally, the label could be stylized as needed. |
Added |
@1ec5 @frederoni this is ready for review. |
@@ -0,0 +1,19 @@ | |||
// | |||
// MGLVectorSource.swift |
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.
Nit: Drop default header comments
<nil key="textColor"/> | ||
<nil key="highlightedColor"/> | ||
<userDefinedRuntimeAttributes> | ||
<userDefinedRuntimeAttribute type="number" keyPath="inspectableTextStyle"> |
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.
We don't have these inspectableTextStyle
anymore. Should be able to just drop the key.
<rect key="frame" x="8" y="2" width="69" height="21"/> | ||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> | ||
<fontDescription key="fontDescription" type="system" pointSize="12"/> | ||
<nil key="textColor"/> |
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.
Hm, is this just an empty key? Does it log any errors? Can we drop the key?
Going to merge this. Tests failures are out of our control right now. |
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.
Sorry for the late review; I was off the grid the other day.
@@ -145,7 +145,7 @@ class ViewController: UIViewController, MGLMapViewDelegate, NavigationViewContro | |||
|
|||
navigationViewController.simulatesLocationUpdates = simulatesLocationUpdates | |||
navigationViewController.routeController.snapsUserLocationAnnotationToRoute = true | |||
navigationViewController.voiceController?.volume = 0.5 | |||
navigationViewController.voiceController?.volume = 0 |
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.
Did you mean to check this in?
smallestLabelDistance = minDistanceBetweenPoints | ||
|
||
var key = "name" | ||
if let languages = Locale.preferredLanguages.first, |
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.
The text in the label should match the map, turn banner, or voice instructions, even if we happen to have a translated name in the vector tiles that is closer to the user’s preferred language. After all, it’s important to be able to match what’s on screen to real-world signage.
Since the Directions API always uses OpenStreetMap’s name
tag, we should always use the name
attribute for consistency with the turn banner and voice instructions.
Closes: #142
I'm still not convinced this any better than using the way name returned in the route name. Perhaps in cases where we cannot find a feature, we can fallback to the way name in the route?
/c @frederoni @1ec5