forked from Scottish-Tech-Army/Soundscape-Android
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Centralize intersection description code
The main change here is to de-duplicate the intersection description code from the unit tests and GeoEngine and have a single function in a new file IntersectionUtils. This should be the same as as that which was in the ComplexIntersections unit test. A new function in the same file can then take the IntersectionDescription and turn it into a callout. The other change in that area is the start of improving the callout code to use the results of the ActivityRecognition to switch between heading, facing and travelling in the callouts. The final change is that getNearestRoad now uses FeatureTree to perform it's searching. Not only does this improve its performance, but it allows us to search outside the FOV - the nearest road could be 0.5m behind the current FOV and searching only within the FOV would preclude that. The unit tests all still pass with this change.
- Loading branch information
Showing
14 changed files
with
521 additions
and
743 deletions.
There are no files selected for viewing
464 changes: 102 additions & 362 deletions
464
app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt
Large diffs are not rendered by default.
Oops, something went wrong.
211 changes: 211 additions & 0 deletions
211
app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
package org.scottishtecharmy.soundscape.geoengine.callouts | ||
|
||
import android.content.Context | ||
import android.util.Log | ||
import org.scottishtecharmy.soundscape.R | ||
import org.scottishtecharmy.soundscape.geoengine.PositionedString | ||
import org.scottishtecharmy.soundscape.geoengine.filters.CalloutHistory | ||
import org.scottishtecharmy.soundscape.geoengine.filters.TrackedCallout | ||
import org.scottishtecharmy.soundscape.geoengine.utils.FeatureTree | ||
import org.scottishtecharmy.soundscape.geoengine.utils.RelativeDirections | ||
import org.scottishtecharmy.soundscape.geoengine.utils.checkWhetherIntersectionIsOfInterest | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getFovTrianglePoints | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionRoadNames | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionRoadNamesRelativeDirections | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getNearestRoad | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getRelativeDirectionLabel | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getRelativeDirectionsPolygons | ||
import org.scottishtecharmy.soundscape.geoengine.utils.getRoadBearingToIntersection | ||
import org.scottishtecharmy.soundscape.geoengine.utils.removeDuplicates | ||
import org.scottishtecharmy.soundscape.geoengine.utils.sortedByDistanceTo | ||
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Feature | ||
import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection | ||
import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt | ||
import org.scottishtecharmy.soundscape.geojsonparser.geojson.Point | ||
|
||
enum class ComplexIntersectionApproach { | ||
INTERSECTION_WITH_MOST_OSM_IDS, | ||
NEAREST_NON_TRIVIAL_INTERSECTION | ||
} | ||
|
||
data class IntersectionDescription(val roads: FeatureCollection = FeatureCollection(), | ||
val location: LngLatAlt = LngLatAlt(), | ||
val name: String = "", | ||
val fovBaseLocation: LngLatAlt = LngLatAlt()) | ||
|
||
/** | ||
* getIntersectionDescriptionFromFov returns a description of the 'best' intersection within the | ||
* field of view. The description includes the roads that join the intersection, the location of the | ||
* intersection and the name of the intersection. | ||
* | ||
* @param roadTree A FeatureTree of roads to use | ||
* @param intersectionTree A FeatureTree of intersections to use | ||
* @param currentLocation The location at the base of the FOV as a LngLatAlt | ||
* @param deviceHeading The direction for the FOV triangle as a Double | ||
* @param fovDistance The distance that the FOV triangle covers in metres as a Double | ||
* @param approach The algorithm used to pick the best intersection. | ||
* | ||
* @return An IntersectionDescription containing all the data required for callouts to describe the | ||
* intersection. | ||
*/ | ||
fun getIntersectionDescriptionFromFov(roadTree: FeatureTree, | ||
intersectionTree: FeatureTree, | ||
currentLocation: LngLatAlt, | ||
deviceHeading: Double, | ||
fovDistance: Double, | ||
approach: ComplexIntersectionApproach | ||
) : IntersectionDescription { | ||
|
||
// Create FOV triangle | ||
val points = getFovTrianglePoints(currentLocation, deviceHeading, fovDistance) | ||
|
||
// Find roads within FOV | ||
val fovRoads = roadTree.generateFeatureCollectionWithinTriangle( | ||
currentLocation, points.left, points.right) | ||
if(fovRoads.features.isEmpty()) return IntersectionDescription() | ||
|
||
// Find intersections within FOV | ||
val fovIntersections = intersectionTree.generateFeatureCollectionWithinTriangle( | ||
currentLocation, points.left, points.right) | ||
if(fovIntersections.features.isEmpty()) return IntersectionDescription() | ||
|
||
// Sort the FOV intersections by distance | ||
val sortedFovIntersections = sortedByDistanceTo(currentLocation, fovIntersections) | ||
|
||
// Which road are we nearest to? In order to allow any road (including those outwith the FOV | ||
// triangle as it could be 1m behind us) we pass in the complete road FeatureTree. | ||
val nearestRoad = getNearestRoad(currentLocation, roadTree) | ||
|
||
// Inspect each intersection so as to skip trivial ones | ||
val nonTrivialIntersections = FeatureCollection() | ||
for (i in 0 until sortedFovIntersections.features.size) { | ||
// Get the roads for the intersection | ||
val intersectionRoads = getIntersectionRoadNames(sortedFovIntersections.features[i], fovRoads) | ||
// Skip 'simple' intersections e.g. ones where the only roads involved have the same name | ||
if(checkWhetherIntersectionIsOfInterest(intersectionRoads, nearestRoad)) { | ||
nonTrivialIntersections.addFeature(sortedFovIntersections.features[i]) | ||
} | ||
} | ||
if(nonTrivialIntersections.features.isEmpty()) { | ||
return IntersectionDescription() | ||
} | ||
|
||
// We have two different approaches to picking the intersection we're interested in | ||
val intersection: Feature? = when(approach) { | ||
ComplexIntersectionApproach.INTERSECTION_WITH_MOST_OSM_IDS -> { | ||
// Pick the intersection feature with the most osm_ids and describe that. | ||
nonTrivialIntersections.features.maxByOrNull { feature -> | ||
(feature.foreign?.get("osm_ids") as? List<*>)?.size ?: 0 | ||
} | ||
} | ||
|
||
ComplexIntersectionApproach.NEAREST_NON_TRIVIAL_INTERSECTION -> { | ||
// Use the nearest "checked" intersection to the device location? | ||
nonTrivialIntersections.features[0] | ||
} | ||
} | ||
|
||
// Use the nearest intersection, but remove duplicated OSM ids from it (those which loop back) | ||
val nearestIntersection = removeDuplicates(sortedFovIntersections.features[0]) | ||
|
||
// Find the bearing that we're coming in at - measured to the nearest intersection | ||
val nearestRoadBearing = getRoadBearingToIntersection(nearestIntersection, nearestRoad, deviceHeading) | ||
|
||
// Create a set of relative direction polygons | ||
val intersectionLocation = intersection!!.geometry as Point | ||
val relativeDirections = getRelativeDirectionsPolygons( | ||
intersectionLocation.coordinates, | ||
nearestRoadBearing, | ||
5.0, | ||
RelativeDirections.COMBINED | ||
) | ||
|
||
val intersectionNameProperty = intersection.properties?.get("name") | ||
val intersectionName = if(intersectionNameProperty == null) | ||
"" | ||
else | ||
intersectionNameProperty as String | ||
|
||
// And use the polygons to describe the roads at the intersection | ||
val intersectionRoadNames = getIntersectionRoadNames(intersection, fovRoads) | ||
return IntersectionDescription( | ||
getIntersectionRoadNamesRelativeDirections( | ||
intersectionRoadNames, | ||
intersection, | ||
relativeDirections | ||
), | ||
intersectionLocation.coordinates, | ||
intersectionName, | ||
currentLocation | ||
) | ||
} | ||
|
||
/** | ||
* addIntersectionCalloutFromDescription adds a callout to the results list for the intersection | ||
* described in the parameters. This will become more configurable e.g. whether to include the | ||
* distance or not. | ||
* | ||
* @param description The description of the intersection to callout | ||
* @param localizedContext A context for obtaining localized strings | ||
* @param results The list of callouts that is appended to | ||
* @param calloutHistory An optional CalloutHistory to use so as to filter out recently played out | ||
* callouts | ||
*/ | ||
fun addIntersectionCalloutFromDescription( | ||
description: IntersectionDescription, | ||
localizedContext: Context, | ||
results: MutableList<PositionedString>, | ||
calloutHistory: CalloutHistory? = null | ||
) { | ||
if(description.roads.features.isEmpty()) return | ||
|
||
if(calloutHistory != null) { | ||
val callout = | ||
TrackedCallout( | ||
description.name, | ||
description.location, | ||
isPoint = true, | ||
isGeneric = false, | ||
) | ||
if (calloutHistory.find(callout)) { | ||
Log.d("Intersections", "Discard ${callout.callout} as in history") | ||
return | ||
} else { | ||
calloutHistory.add(callout) | ||
} | ||
} | ||
|
||
// Report distance | ||
results.add( | ||
PositionedString( | ||
"${localizedContext.getString(R.string.intersection_approaching_intersection)} ${ | ||
localizedContext.getString( | ||
R.string.distance_format_meters, | ||
description.fovBaseLocation.distance(description.location).toInt().toString(), | ||
) | ||
}", | ||
), | ||
) | ||
|
||
// Report roads | ||
for (feature in description.roads.features) { | ||
val direction = feature.properties?.get("Direction").toString().toIntOrNull() | ||
|
||
// Don't call out the road we are on (0) as part of the intersection | ||
if (direction != null && direction != 0) { | ||
val relativeDirectionString = getRelativeDirectionLabel( | ||
localizedContext, | ||
direction | ||
) | ||
if (feature.properties?.get("name") != null) { | ||
val intersectionCallout = | ||
localizedContext.getString( | ||
R.string.directions_intersection_with_name_direction, | ||
feature.properties?.get("name"), | ||
relativeDirectionString, | ||
) | ||
results.add(PositionedString(intersectionCallout)) | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.