Skip to content

Commit

Permalink
Centralize intersection description code
Browse files Browse the repository at this point in the history
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
davecraig committed Jan 10, 2025
1 parent efab8ae commit 57d5d42
Show file tree
Hide file tree
Showing 14 changed files with 521 additions and 743 deletions.
464 changes: 102 additions & 362 deletions app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt

Large diffs are not rendered by default.

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

0 comments on commit 57d5d42

Please sign in to comment.