From 57d5d423697a0ac346140288bf2f467712d26258 Mon Sep 17 00:00:00 2001 From: Dave Craig Date: Fri, 10 Jan 2025 10:22:27 +0000 Subject: [PATCH] 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. --- .../soundscape/geoengine/GeoEngine.kt | 464 ++++-------------- .../geoengine/callouts/IntersectionUtils.kt | 211 ++++++++ .../geoengine/utils/CompassDirections.kt | 180 ++++++- .../soundscape/geoengine/utils/FeatureTree.kt | 1 + .../geoengine/utils/IntersectionUtils.kt | 2 - .../soundscape/geoengine/utils/TileUtils.kt | 48 +- .../soundscape/ComplexIntersections.kt | 193 +------- .../soundscape/CrossingTest.kt | 9 +- .../soundscape/IntersectionsTest.kt | 65 +-- .../soundscape/IntersectionsTestMvt.kt | 66 +-- .../soundscape/RoundaboutsTest.kt | 6 +- .../soundscape/StreetPreviewTest.kt | 12 +- .../soundscape/TileUtilsTest.kt | 2 +- .../VisuallyCheckIntersectionLayers.kt | 5 +- 14 files changed, 521 insertions(+), 743 deletions(-) create mode 100644 app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt delete mode 100644 app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/IntersectionUtils.kt diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt index 3bad06b4..0ac8ac92 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt @@ -36,12 +36,12 @@ import org.scottishtecharmy.soundscape.geoengine.filters.LocationUpdateFilter import org.scottishtecharmy.soundscape.geoengine.filters.TrackedCallout import org.scottishtecharmy.soundscape.geoengine.mvttranslation.InterpolatedPointsJoiner import org.scottishtecharmy.soundscape.geoengine.mvttranslation.vectorTileToGeoJson +import org.scottishtecharmy.soundscape.geoengine.callouts.ComplexIntersectionApproach +import org.scottishtecharmy.soundscape.geoengine.callouts.addIntersectionCalloutFromDescription import org.scottishtecharmy.soundscape.geoengine.utils.FeatureTree -import org.scottishtecharmy.soundscape.geoengine.utils.RelativeDirections import org.scottishtecharmy.soundscape.geoengine.utils.ResourceMapper import org.scottishtecharmy.soundscape.geoengine.utils.TileGrid import org.scottishtecharmy.soundscape.geoengine.utils.TileGrid.Companion.getTileGrid -import org.scottishtecharmy.soundscape.geoengine.utils.checkWhetherIntersectionIsOfInterest import org.scottishtecharmy.soundscape.geoengine.utils.cleanTileGeoJSON import org.scottishtecharmy.soundscape.geoengine.utils.deduplicateFeatureCollection import org.scottishtecharmy.soundscape.geoengine.utils.distanceToPolygon @@ -49,13 +49,9 @@ import org.scottishtecharmy.soundscape.geoengine.utils.getCompassLabelFacingDire import org.scottishtecharmy.soundscape.geoengine.utils.getCompassLabelFacingDirectionAlong import org.scottishtecharmy.soundscape.geoengine.utils.getFeatureNearestPoint 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.getPoiFeatureCollectionBySuperCategory -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.callouts.getIntersectionDescriptionFromFov import org.scottishtecharmy.soundscape.geoengine.utils.getSuperCategoryElements import org.scottishtecharmy.soundscape.geoengine.utils.mergeAllPolygonsInFeatureCollection import org.scottishtecharmy.soundscape.geoengine.utils.pointIsWithinBoundingBox @@ -113,6 +109,7 @@ class GeoEngine { private var centralBoundingBox = BoundingBox() private var inVehicle = false + private var inMotion = false private var featureTrees = Array(Fc.MAX_COLLECTION_ID.id) { FeatureTree(null) } @OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class) private val treeContext = newSingleThreadContext("TreeContext") @@ -130,6 +127,7 @@ class GeoEngine { else -> false } + inMotion = (event.activityType != DetectedActivity.STILL) } } @@ -433,7 +431,8 @@ class GeoEngine { localizedContext, orientation.toInt(), roadName.toString(), - configLocale + inMotion, + inVehicle ) results.add(PositionedString(facingDirectionAlongRoad)) return results @@ -486,151 +485,22 @@ class GeoEngine { intersectionFilter.update(location) intersectionCalloutHistory.trim(location) - // The intersection callout logic is: - // - // Find all roads within 60m - // Find all intersections within 60m - // - // Call getFovRoadsFeatureCollection with both FeatureCollections to get collections of roads - // and intersections in the forwards pointing field of view. Prior to rtrees this involved a - // straight search. Now we use the rtree code to get the Features within the bounding box of - // the FOV triangle before performing the more expensive weeding out those outwith the actual - // triangle. This is all done inside generateFeatureCollectionWithinFov. - val heading = directionProvider.getCurrentDirection().toDouble() val fovDistance = 50.0 - val points = getFovTrianglePoints(location, heading, fovDistance) - val fovIntersectionsFeatureCollection = - featureTrees[Fc.INTERSECTIONS.id].generateFeatureCollectionWithinTriangle(location, - points.left, - points.right) - - val fovRoadsFeatureCollection = - featureTrees[Fc.ROADS.id].generateFeatureCollectionWithinTriangle(location, - points.left, - points.right) - - if (fovIntersectionsFeatureCollection.features.isNotEmpty() && - fovRoadsFeatureCollection.features.isNotEmpty() - ) { - val intersectionsSortedByDistance = - sortedByDistanceTo( - location, - fovIntersectionsFeatureCollection, - ) - - val testNearestRoad = - getNearestRoad( - location, - fovRoadsFeatureCollection, - ) - val intersectionsNeedsFurtherCheckingFC = FeatureCollection() - - for (i in 0 until intersectionsSortedByDistance.features.size) { - val intersectionRoadNames = - getIntersectionRoadNames(intersectionsSortedByDistance.features[i], fovRoadsFeatureCollection) - if (checkWhetherIntersectionIsOfInterest(intersectionRoadNames, testNearestRoad)) { - intersectionsNeedsFurtherCheckingFC.addFeature(intersectionsSortedByDistance.features[i]) - } - } - if (intersectionsNeedsFurtherCheckingFC.features.isNotEmpty()) { - // Approach 1: find the intersection feature with the most osm_ids and use that? - val featureWithMostOsmIds: Feature? = - intersectionsNeedsFurtherCheckingFC.features.maxByOrNull { feature -> - (feature.foreign?.get("osm_ids") as? List<*>)?.size ?: 0 - } - val nearestIntersection = FeatureTree(fovIntersectionsFeatureCollection).getNearestFeature( - location - ) - val nearestRoadBearing = - getRoadBearingToIntersection( - nearestIntersection, - testNearestRoad, - heading, - ) - if (featureWithMostOsmIds != null) { - val intersectionLocation = - featureWithMostOsmIds.geometry as Point - val intersectionRelativeDirections = - getRelativeDirectionsPolygons( - intersectionLocation.coordinates, - nearestRoadBearing, - // fovDistance, - 5.0, - RelativeDirections.COMBINED, - ) - val distanceToNearestIntersection = location.distance(intersectionLocation.coordinates) - val intersectionRoadNames = - getIntersectionRoadNames( - featureWithMostOsmIds, - fovRoadsFeatureCollection, - ) + val intersectionDescription = getIntersectionDescriptionFromFov( + featureTrees[Fc.ROADS_AND_PATHS.id], + featureTrees[Fc.INTERSECTIONS.id], + location, + heading, + fovDistance, + ComplexIntersectionApproach.NEAREST_NON_TRIVIAL_INTERSECTION) - val intersectionName = - featureWithMostOsmIds.properties?.get("name") as String - val callout = - TrackedCallout( - intersectionName, - intersectionLocation.coordinates, - isPoint = true, - isGeneric = false, - ) - if (intersectionCalloutHistory.find(callout)) { - Log.d(TAG, "Discard ${callout.callout}") - return emptyList() - } else { - results.add( - PositionedString( - "${localizedContext.getString(R.string.intersection_approaching_intersection)} ${ - localizedContext.getString( - R.string.distance_format_meters, - distanceToNearestIntersection.toInt().toString(), - ) - }", - ), - ) - intersectionCalloutHistory.add(callout) - } + addIntersectionCalloutFromDescription(intersectionDescription, + localizedContext, + results, + intersectionCalloutHistory) - val roadRelativeDirections = - getIntersectionRoadNamesRelativeDirections( - intersectionRoadNames, - featureWithMostOsmIds, - intersectionRelativeDirections, - ) - for (feature in roadRelativeDirections.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, - configLocale, - ) - 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, - ), - ) - } - } - } - } - } - } return results } @@ -862,7 +732,7 @@ class GeoEngine { if (roadGridFeatureCollection.features.isNotEmpty()) { //Log.d(TAG, "Found roads in tile") - val nearestRoad = getNearestRoad(location,roadGridFeatureCollection) + val nearestRoad = getNearestRoad(location, featureTrees[Fc.ROADS_AND_PATHS.id]) if (nearestRoad != null) { val properties = nearestRoad.properties @@ -877,7 +747,8 @@ class GeoEngine { localizedContext, orientation.toInt(), roadName.toString(), - configLocale + inMotion, + inVehicle ) list.add(PositionedString(facingDirectionAlongRoad)) } else { @@ -891,7 +762,8 @@ class GeoEngine { getCompassLabelFacingDirection( localizedContext, orientation.toInt(), - configLocale + inMotion, + inVehicle ) results.add(PositionedString(facingDirection)) } @@ -1127,225 +999,40 @@ class GeoEngine { val orientation = directionProvider.getCurrentDirection().toDouble() val fovDistance = 50.0 - val points = getFovTrianglePoints(location, orientation, fovDistance) - val fovRoadsFeatureCollection = featureTrees[Fc.ROADS.id]. - generateFeatureCollectionWithinTriangle(location, points.left, points.right) - - val fovIntersectionsFeatureCollection = featureTrees[Fc.INTERSECTIONS.id]. - generateFeatureCollectionWithinTriangle(location, points.left, points.right) - - val fovCrossingsFeatureCollection = featureTrees[Fc.CROSSINGS.id]. - generateFeatureCollectionWithinTriangle(location, points.left, points.right) - - val fovBusStopsFeatureCollection = featureTrees[Fc.BUS_STOPS.id]. - generateFeatureCollectionWithinTriangle(location, points.left, points.right) - - if (fovRoadsFeatureCollection.features.isNotEmpty()) { - val nearestRoad = getNearestRoad( - location, - fovRoadsFeatureCollection - ) - // TODO check for Settings, Unnamed roads on/off here - if (nearestRoad != null) { - if (nearestRoad.properties?.get("name") != null) { - list.add( - PositionedString( - "${localizedContext.getString(R.string.directions_direction_ahead)} ${nearestRoad.properties!!["name"]}" - ) - ) - } else { - // we are detecting an unnamed road here but pretending there is nothing here - list.add( - PositionedString( - localizedContext.getString(R.string.callouts_nothing_to_call_out_now) - ) - ) - } - } - - if (fovIntersectionsFeatureCollection.features.isNotEmpty() && - fovRoadsFeatureCollection.features.isNotEmpty() - ) { - val intersectionsSortedByDistance = sortedByDistanceTo( - location, - fovIntersectionsFeatureCollection - ) - - val testNearestRoad = getNearestRoad( - location, - fovRoadsFeatureCollection - ) - val intersectionsNeedsFurtherCheckingFC = FeatureCollection() - - for (i in 0 until intersectionsSortedByDistance.features.size) { - val intersectionRoadNames = getIntersectionRoadNames( - intersectionsSortedByDistance.features[i], - fovRoadsFeatureCollection - ) - val intersectionsNeedsFurtherChecking = - checkWhetherIntersectionIsOfInterest(intersectionRoadNames, testNearestRoad) - if (intersectionsNeedsFurtherChecking) { - intersectionsNeedsFurtherCheckingFC.addFeature( - intersectionsSortedByDistance.features[i] - ) - } - } - if (intersectionsNeedsFurtherCheckingFC.features.isNotEmpty()) { - // Approach 1: find the intersection feature with the most osm_ids and use that? - val featureWithMostOsmIds: Feature? = - intersectionsNeedsFurtherCheckingFC.features.maxByOrNull { feature -> - (feature.foreign?.get("osm_ids") as? List<*>)?.size ?: 0 - } - - val nearestIntersection = FeatureTree(intersectionsNeedsFurtherCheckingFC).getNearestFeature(location) - -// val nearestIntersection = intersectionsNeedsFurtherCheckingFC.features[0] - + // Detect if there is an intersection in the FOV + val intersectionDescription = getIntersectionDescriptionFromFov( + featureTrees[Fc.ROADS_AND_PATHS.id], + featureTrees[Fc.INTERSECTIONS.id], + location, + orientation, + fovDistance, + ComplexIntersectionApproach.NEAREST_NON_TRIVIAL_INTERSECTION + ) + addIntersectionCalloutFromDescription(intersectionDescription, + localizedContext, + list) - val nearestRoadBearing = getRoadBearingToIntersection( - nearestIntersection, - testNearestRoad, - orientation - ) - if (featureWithMostOsmIds != null) { - val intersectionLocation = - featureWithMostOsmIds.geometry as Point - val intersectionRelativeDirections = - getRelativeDirectionsPolygons( - intersectionLocation.coordinates, - nearestRoadBearing, - //fovDistance, - 5.0, - RelativeDirections.COMBINED - ) - val distanceToNearestIntersection = location.distance( - intersectionLocation.coordinates - ) - val intersectionRoadNames = getIntersectionRoadNames( - featureWithMostOsmIds, - fovRoadsFeatureCollection - ) - list.add( - PositionedString( - "${localizedContext.getString(R.string.intersection_approaching_intersection)} ${ - localizedContext.getString( - R.string.distance_format_meters, - distanceToNearestIntersection.toInt() - .toString() - ) - }" - ) - ) + // Detect if there is a crossing in the FOV + val points = getFovTrianglePoints(location, orientation, fovDistance) + val nearestCrossing = getNearestFeatureOnRoadInFov(Fc.CROSSINGS.id, + location, + points.left, + points.right) + appendNearestFeatureCallout(nearestCrossing, R.string.osm_tag_crossing, list) - val roadRelativeDirections = - getIntersectionRoadNamesRelativeDirections( - intersectionRoadNames, - featureWithMostOsmIds, - intersectionRelativeDirections - ) - for (feature in roadRelativeDirections.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, - configLocale - ) - if (feature.properties?.get("name") != null) { - val intersectionCallout = - localizedContext.getString( - R.string.directions_intersection_with_name_direction, - feature.properties?.get("name"), - relativeDirectionString - ) - list.add( - PositionedString( - intersectionCallout - ) - ) - } - } - } - } - } - } - // detect if there is a crossing in the FOV - val nearestCrossing = FeatureTree(fovCrossingsFeatureCollection).getNearestFeature( - location - ) - if (nearestCrossing != null) { - val crossingLocation = nearestCrossing.geometry as Point - val distanceToCrossing = location.distance(crossingLocation.coordinates) - // Confirm which road the crossing is on - val nearestRoadToCrossing = getNearestRoad( - crossingLocation.coordinates, - fovRoadsFeatureCollection - ) - if (nearestRoadToCrossing != null) { - val crossingText = buildString { - append(localizedContext.getString(R.string.osm_tag_crossing)) - append(". ") - append( - localizedContext.getString( - R.string.distance_format_meters, - distanceToCrossing.toInt().toString() - ) - ) - append(". ") - if (nearestRoadToCrossing.properties?.get("name") != null) { - append( - nearestRoadToCrossing.properties?.get( - "name" - ) - ) - } - } - list.add(PositionedString(crossingText)) - } - } + // Detect if there is a bus_stop in the FOV + val nearestBusStop = getNearestFeatureOnRoadInFov(Fc.BUS_STOPS.id, + location, + points.left, + points.right) + appendNearestFeatureCallout(nearestBusStop, R.string.osm_tag_bus_stop, list) - // detect if there is a bus_stop in the FOV - val nearestBusStop = FeatureTree(fovBusStopsFeatureCollection).getNearestFeature( - location - ) - if (nearestBusStop != null) { - val busStopLocation = nearestBusStop.geometry as Point - val distanceToBusStop = location.distance(busStopLocation.coordinates) - - // Confirm which road the crossing is on - val nearestRoadToBus = getNearestRoad( - busStopLocation.coordinates, - fovRoadsFeatureCollection - ) - if (nearestRoadToBus != null) { - val busText = buildString { - append(localizedContext.getString(R.string.osm_tag_bus_stop)) - append(". ") - append( - localizedContext.getString( - R.string.distance_format_meters, - distanceToBusStop.toInt().toString() - ) - ) - append(". ") - if (nearestRoadToBus.properties?.get("name") != null) { - append(nearestRoadToBus.properties?.get("name")) - } - } - list.add(PositionedString(busText)) - } - } - } else { + if(list.isEmpty()) { list.add( PositionedString( localizedContext.getString(R.string.callouts_nothing_to_call_out_now) ) ) - } list } @@ -1395,6 +1082,59 @@ class GeoEngine { return featureTrees[id].getNearestFeature(location, distance) } + data class FeatureByRoad(val feature: Feature, + val road: Feature, + val distance: Double = Double.POSITIVE_INFINITY) + private fun getNearestFeatureOnRoadInFov(id: Int, + location: LngLatAlt, + left: LngLatAlt, + right: LngLatAlt + ): FeatureByRoad? { + + val nearestFeature = featureTrees[id].getNearestFeatureWithinTriangle( + location, + left, + right) + if (nearestFeature != null) { + val featureLocation = nearestFeature.geometry as Point + + // Confirm which road the feature is on + val nearestRoad = getNearestRoad( + featureLocation.coordinates, + featureTrees[Fc.ROADS_AND_PATHS.id] + ) + if(nearestRoad != null) { + // We found a feature and the road that it is on + val distance = location.distance(featureLocation.coordinates) + return FeatureByRoad(nearestFeature, nearestRoad, distance) + } + } + + return null + } + + private fun appendNearestFeatureCallout(nearestFeature: FeatureByRoad?, + osmTagType: Int, + list: MutableList) { + if(nearestFeature != null) { + val text = buildString { + append(localizedContext.getString(osmTagType)) + append(". ") + append( + localizedContext.getString( + R.string.distance_format_meters, + nearestFeature.distance.toInt().toString() + ) + ) + append(". ") + if (nearestFeature.road.properties?.get("name") != null) { + append(nearestFeature.road.properties?.get("name")) + } + } + list.add(PositionedString(text)) + } + } + fun streetPreviewGo() { if(true) { streetPreviewGoInternal() diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt new file mode 100644 index 00000000..0ae82b9b --- /dev/null +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt @@ -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, + 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)) + } + } + } +} diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/CompassDirections.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/CompassDirections.kt index 2df6c082..eb726520 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/CompassDirections.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/CompassDirections.kt @@ -4,35 +4,169 @@ package org.scottishtecharmy.soundscape.geoengine.utils import android.content.Context import org.scottishtecharmy.soundscape.R -fun getCompassLabelFacingDirection(localizedContext: Context, degrees: Int, locale: java.util.Locale): String{ - return when (degrees) { - in 338..360, in 0..22 -> localizedContext.getString(R.string.directions_facing_n) - in 23..67 -> localizedContext.getString(R.string.directions_facing_ne) - in 68..112 -> localizedContext.getString(R.string.directions_facing_e) - in 113..157 -> localizedContext.getString(R.string.directions_facing_se) - in 158..202 -> localizedContext.getString(R.string.directions_facing_s) - in 203..247 -> localizedContext.getString(R.string.directions_facing_sw) - in 248..292 -> localizedContext.getString(R.string.directions_facing_w) - in 293..337 -> localizedContext.getString(R.string.directions_facing_nw) - else -> "" +fun getCompassLabelFacingDirection(localizedContext: Context, + degrees: Int, + inMotion: Boolean, + inVehicle: Boolean +): String{ + if(!inMotion) { + return when (degrees) { + in 338..360, in 0..22 -> localizedContext.getString(R.string.directions_facing_n) + in 23..67 -> localizedContext.getString(R.string.directions_facing_ne) + in 68..112 -> localizedContext.getString(R.string.directions_facing_e) + in 113..157 -> localizedContext.getString(R.string.directions_facing_se) + in 158..202 -> localizedContext.getString(R.string.directions_facing_s) + in 203..247 -> localizedContext.getString(R.string.directions_facing_sw) + in 248..292 -> localizedContext.getString(R.string.directions_facing_w) + in 293..337 -> localizedContext.getString(R.string.directions_facing_nw) + else -> "" + } + } else if(inVehicle) { + return when (degrees) { + in 338..360, in 0..22 -> localizedContext.getString(R.string.directions_traveling_n) + in 23..67 -> localizedContext.getString(R.string.directions_traveling_ne) + in 68..112 -> localizedContext.getString(R.string.directions_traveling_e) + in 113..157 -> localizedContext.getString(R.string.directions_traveling_se) + in 158..202 -> localizedContext.getString(R.string.directions_traveling_s) + in 203..247 -> localizedContext.getString(R.string.directions_traveling_sw) + in 248..292 -> localizedContext.getString(R.string.directions_traveling_w) + in 293..337 -> localizedContext.getString(R.string.directions_traveling_nw) + else -> "" + } + } else { + return when (degrees) { + in 338..360, in 0..22 -> localizedContext.getString(R.string.directions_heading_n) + in 23..67 -> localizedContext.getString(R.string.directions_heading_ne) + in 68..112 -> localizedContext.getString(R.string.directions_heading_e) + in 113..157 -> localizedContext.getString(R.string.directions_heading_se) + in 158..202 -> localizedContext.getString(R.string.directions_heading_s) + in 203..247 -> localizedContext.getString(R.string.directions_heading_sw) + in 248..292 -> localizedContext.getString(R.string.directions_heading_w) + in 293..337 -> localizedContext.getString(R.string.directions_heading_nw) + else -> "" + } } } -fun getCompassLabelFacingDirectionAlong(localizedContext: Context, degrees: Int, placeholder: String, locale: java.util.Locale):String{ - return when (degrees) { - in 338 .. 360, in 0 .. 22 -> localizedContext.getString(R.string.directions_along_facing_n, placeholder) - in 23 .. 67 -> localizedContext.getString(R.string.directions_along_facing_ne, placeholder) - in 68 .. 112 -> localizedContext.getString(R.string.directions_along_facing_e, placeholder) - in 113 .. 157 -> localizedContext.getString(R.string.directions_along_facing_se, placeholder) - in 158 .. 202 -> localizedContext.getString(R.string.directions_along_facing_s, placeholder) - in 203 .. 247 -> localizedContext.getString(R.string.directions_along_facing_sw, placeholder) - in 248 .. 292 -> localizedContext.getString(R.string.directions_along_facing_w, placeholder) - in 293 .. 337 -> localizedContext.getString(R.string.directions_along_facing_nw, placeholder) - else -> "" +fun getCompassLabelFacingDirectionAlong(localizedContext: Context, + degrees: Int, + placeholder: String, + inMotion: Boolean, + inVehicle: Boolean +):String{ + if(!inMotion) { + return when (degrees) { + in 338..360, in 0..22 -> localizedContext.getString( + R.string.directions_along_facing_n, + placeholder + ) + in 23..67 -> localizedContext.getString( + R.string.directions_along_facing_ne, + placeholder + ) + in 68..112 -> localizedContext.getString( + R.string.directions_along_facing_e, + placeholder + ) + in 113..157 -> localizedContext.getString( + R.string.directions_along_facing_se, + placeholder + ) + in 158..202 -> localizedContext.getString( + R.string.directions_along_facing_s, + placeholder + ) + in 203..247 -> localizedContext.getString( + R.string.directions_along_facing_sw, + placeholder + ) + in 248..292 -> localizedContext.getString( + R.string.directions_along_facing_w, + placeholder + ) + in 293..337 -> localizedContext.getString( + R.string.directions_along_facing_nw, + placeholder + ) + else -> "" + } + } else if(inVehicle) { + return when (degrees) { + in 338..360, in 0..22 -> localizedContext.getString( + R.string.directions_along_traveling_n, + placeholder + ) + in 23..67 -> localizedContext.getString( + R.string.directions_along_traveling_ne, + placeholder + ) + in 68..112 -> localizedContext.getString( + R.string.directions_along_traveling_e, + placeholder + ) + in 113..157 -> localizedContext.getString( + R.string.directions_along_traveling_se, + placeholder + ) + in 158..202 -> localizedContext.getString( + R.string.directions_along_traveling_s, + placeholder + ) + in 203..247 -> localizedContext.getString( + R.string.directions_along_traveling_sw, + placeholder + ) + in 248..292 -> localizedContext.getString( + R.string.directions_along_traveling_w, + placeholder + ) + in 293..337 -> localizedContext.getString( + R.string.directions_along_traveling_nw, + placeholder + ) + else -> "" + } + } else { + return when (degrees) { + in 338..360, in 0..22 -> localizedContext.getString( + R.string.directions_along_heading_n, + placeholder + ) + in 23..67 -> localizedContext.getString( + R.string.directions_along_heading_ne, + placeholder + ) + in 68..112 -> localizedContext.getString( + R.string.directions_along_heading_e, + placeholder + ) + in 113..157 -> localizedContext.getString( + R.string.directions_along_heading_se, + placeholder + ) + in 158..202 -> localizedContext.getString( + R.string.directions_along_heading_s, + placeholder + ) + in 203..247 -> localizedContext.getString( + R.string.directions_along_heading_sw, + placeholder + ) + in 248..292 -> localizedContext.getString( + R.string.directions_along_heading_w, + placeholder + ) + in 293..337 -> localizedContext.getString( + R.string.directions_along_heading_nw, + placeholder + ) + else -> "" + } } } -fun getRelativeDirectionLabel(localizedContext: Context, relativeDirection: Int, locale: java.util.Locale): String{ +fun getRelativeDirectionLabel(localizedContext: Context, + relativeDirection: Int): String{ return when (relativeDirection) { 0 -> localizedContext.getString(R.string.directions_direction_behind) 1 -> localizedContext.getString(R.string.directions_direction_behind_to_the_left) diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/FeatureTree.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/FeatureTree.kt index f98735cb..151056d2 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/FeatureTree.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/FeatureTree.kt @@ -426,6 +426,7 @@ class FeatureTree(featureCollection: FeatureCollection?) { val results = nearestWithinTriangle(arrayListOf(location, left, right), 1) ?: return null + if(results.count() == 0) return null return results.first().value() } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/IntersectionUtils.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/IntersectionUtils.kt deleted file mode 100644 index 5a21e930..00000000 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/IntersectionUtils.kt +++ /dev/null @@ -1,2 +0,0 @@ -package org.scottishtecharmy.soundscape.geoengine.utils - diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/TileUtils.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/TileUtils.kt index 6174f7df..315590ac 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/TileUtils.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/TileUtils.kt @@ -567,54 +567,18 @@ fun getIntersectionRoadNames( * Roads can contain crossings which are Points not LineStrings. * @param currentLocation * Location of device. - * @param roadFeatureCollection - * The intersection feature collection that contains the intersections we want to test. + * @param searchTree + * The FeatureTree to search for the nearest road. * @return A Feature that is the nearest road. */ fun getNearestRoad( currentLocation: LngLatAlt, - roadFeatureCollection: FeatureCollection + searchTree: FeatureTree ): Feature? { - //TODO I have no idea if roads can also be represented with MultiLineStrings. - // In which case this will fail. Need to have a look at some tiles with motorways/dual carriageways - - var maxDistanceToRoad = Int.MAX_VALUE.toDouble() - var nearestRoad : Feature? = null - - for (feature in roadFeatureCollection) { - if (feature.geometry.type == "LineString") { - val distanceToRoad = distanceToLineString( - currentLocation, - (feature.geometry as LineString) - ) - if (distanceToRoad < maxDistanceToRoad) { - nearestRoad = feature - maxDistanceToRoad = distanceToRoad - } - } else if (feature.geometry.type == "Polygon") { - val distanceToRoad = distanceToPolygon( - currentLocation, - (feature.geometry as Polygon) - ) - if (distanceToRoad < maxDistanceToRoad) { - nearestRoad = feature - maxDistanceToRoad = distanceToRoad - } - } else { - val distanceToRoad = currentLocation.distance( - LngLatAlt((feature.geometry as Point).coordinates.latitude, - (feature.geometry as Point).coordinates.longitude) - ) - if (distanceToRoad < maxDistanceToRoad) { - nearestRoad = feature - maxDistanceToRoad = distanceToRoad - } - } - } - - // TODO As the distance to the road has already been calculated - // perhaps we could insert the distance to the road as a property/foreign member of the Feature? + // This is just the nearest road. In the future, the algorithm will get almost certainly become + // more elaborate. + val nearestRoad = searchTree.getNearestFeature(currentLocation) return nearestRoad } diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/ComplexIntersections.kt b/app/src/test/java/org/scottishtecharmy/soundscape/ComplexIntersections.kt index 816337e6..07e848d2 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/ComplexIntersections.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/ComplexIntersections.kt @@ -3,24 +3,14 @@ package org.scottishtecharmy.soundscape import com.squareup.moshi.Moshi import org.junit.Assert import org.junit.Test +import org.scottishtecharmy.soundscape.geoengine.callouts.ComplexIntersectionApproach import org.scottishtecharmy.soundscape.geoengine.utils.FeatureTree -import org.scottishtecharmy.soundscape.geojsonparser.geojson.Feature import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection import org.scottishtecharmy.soundscape.geojsonparser.geojson.GeoMoshi import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import org.scottishtecharmy.soundscape.geojsonparser.geojson.Point -import org.scottishtecharmy.soundscape.geoengine.utils.RelativeDirections -import org.scottishtecharmy.soundscape.geoengine.utils.checkWhetherIntersectionIsOfInterest -import org.scottishtecharmy.soundscape.geoengine.utils.generateDebugFovGeoJson -import org.scottishtecharmy.soundscape.geoengine.utils.getFovFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionRoadNames -import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionRoadNamesRelativeDirections import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionsFeatureCollectionFromTileFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.getNearestRoad -import org.scottishtecharmy.soundscape.geoengine.utils.getRelativeDirectionsPolygons -import org.scottishtecharmy.soundscape.geoengine.utils.getRoadBearingToIntersection +import org.scottishtecharmy.soundscape.geoengine.callouts.getIntersectionDescriptionFromFov import org.scottishtecharmy.soundscape.geoengine.utils.getRoadsFeatureCollectionFromTileFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.sortedByDistanceTo class ComplexIntersections { @@ -45,101 +35,21 @@ class ComplexIntersections { getRoadsFeatureCollectionFromTileFeatureCollection( featureCollectionTest!! ) - // create FOV to pickup the roads - val fovRoadsFeatureCollection = getFovFeatureCollection( - currentLocation, - deviceHeading, - fovDistance, - FeatureTree(testRoadsCollectionFromTileFeatureCollection) - ) -// generateDebugFovGeoJson(currentLocation, deviceHeading, fovDistance, testRoadsCollectionFromTileFeatureCollection) // Get the intersections from the tile val testIntersectionsCollectionFromTileFeatureCollection = getIntersectionsFeatureCollectionFromTileFeatureCollection( featureCollectionTest ) - // Create a FOV triangle to pick up the intersections - val fovIntersectionsFeatureCollection = getFovFeatureCollection( + + val roadRelativeDirections = getIntersectionDescriptionFromFov( + FeatureTree(testRoadsCollectionFromTileFeatureCollection), + FeatureTree(testIntersectionsCollectionFromTileFeatureCollection), currentLocation, deviceHeading, fovDistance, - FeatureTree(testIntersectionsCollectionFromTileFeatureCollection) - ) - generateDebugFovGeoJson(currentLocation, deviceHeading, fovDistance, testIntersectionsCollectionFromTileFeatureCollection) - - val roadsFOVString = - moshi.adapter(FeatureCollection::class.java).toJson(fovRoadsFeatureCollection) - println("Roads in FOV: $roadsFOVString") - val intersectionsFOVString = - moshi.adapter(FeatureCollection::class.java).toJson(fovIntersectionsFeatureCollection) - println("Intersections in FOV: $intersectionsFOVString") - - // I will need a feature collection of all the intersections in the FOV sorted by distance to the current location - val intersectionsSortedByDistance = sortedByDistanceTo( - currentLocation, - fovIntersectionsFeatureCollection - ) - val intersectionsSortedByDistanceString = - moshi.adapter(FeatureCollection::class.java).toJson(intersectionsSortedByDistance) - println("Intersections in FOV sorted by distance: $intersectionsSortedByDistanceString") - println("Number of intersections in FOV: ${intersectionsSortedByDistance.features.size}") - - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) - val intersectionsNeedsFurtherCheckingFC = FeatureCollection() + ComplexIntersectionApproach.NEAREST_NON_TRIVIAL_INTERSECTION).roads - for (i in 0 until intersectionsSortedByDistance.features.size) { - val intersectionRoadNames = getIntersectionRoadNames(intersectionsSortedByDistance.features[i], fovRoadsFeatureCollection) - val intersectionsNeedsFurtherChecking = checkWhetherIntersectionIsOfInterest(intersectionRoadNames, testNearestRoad) - if(intersectionsNeedsFurtherChecking) { - intersectionsNeedsFurtherCheckingFC.addFeature(intersectionsSortedByDistance.features[i]) - } - } - println("Intersections that need further checking: ${intersectionsNeedsFurtherCheckingFC.features.size}") - - // Approach 1: find the intersection feature with the most osm_ids and use that? - // The code doesn't give us the correct result as it jumps to the wrong/next intersection - /*val featureWithMostOsmIds: Feature? = intersectionsNeedsFurtherCheckingFC.features.maxByOrNull { - feature -> - (feature.foreign?.get("osm_ids") as? List<*>)?.size ?: 0 - } - val newIntersectionFeatureCollection = FeatureCollection() - if (featureWithMostOsmIds != null) { - newIntersectionFeatureCollection.addFeature(featureWithMostOsmIds) - }*/ - - // Approach 2: Use the nearest "checked" intersection to the device location? - val intersectionsToCheckSortedByDistance = sortedByDistanceTo( - currentLocation, - intersectionsNeedsFurtherCheckingFC - ) - val nearestCheckedIntersection = intersectionsToCheckSortedByDistance.features[0] - - - val nearestIntersection = FeatureTree(fovIntersectionsFeatureCollection).getNearestFeature(currentLocation) - val nearestRoadBearing = getRoadBearingToIntersection(nearestIntersection, testNearestRoad, deviceHeading) - val intersectionLocation = nearestCheckedIntersection.geometry as Point - val intersectionRelativeDirections = getRelativeDirectionsPolygons( - LngLatAlt(intersectionLocation.coordinates.longitude, - intersectionLocation.coordinates.latitude), - nearestRoadBearing, - //fovDistance, - 5.0, - RelativeDirections.COMBINED - ) - val relativeDirectionsString = - moshi.adapter(FeatureCollection::class.java).toJson(intersectionRelativeDirections) - println("relative directions polygons: $relativeDirectionsString") - - val intersectionRoadNames = getIntersectionRoadNames(nearestCheckedIntersection, fovRoadsFeatureCollection) - val intersectionRoadNamesString = - moshi.adapter(FeatureCollection::class.java).toJson(intersectionRoadNames) - println("Intersection roads: $intersectionRoadNamesString") - val roadRelativeDirections = getIntersectionRoadNamesRelativeDirections( - intersectionRoadNames, - nearestCheckedIntersection, - intersectionRelativeDirections - ) Assert.assertEquals(3, roadRelativeDirections.features.size ) @@ -172,96 +82,19 @@ class ComplexIntersections { getRoadsFeatureCollectionFromTileFeatureCollection( featureCollectionTest!! ) - // create FOV to pickup the roads - val fovRoadsFeatureCollection = getFovFeatureCollection( - currentLocation, - deviceHeading, - fovDistance, - FeatureTree(testRoadsCollectionFromTileFeatureCollection) - ) + // Get the intersections from the tile val testIntersectionsCollectionFromTileFeatureCollection = getIntersectionsFeatureCollectionFromTileFeatureCollection( featureCollectionTest ) - // Create a FOV triangle to pick up the intersections - val fovIntersectionsFeatureCollection = getFovFeatureCollection( + val roadRelativeDirections = getIntersectionDescriptionFromFov( + FeatureTree(testRoadsCollectionFromTileFeatureCollection), + FeatureTree(testIntersectionsCollectionFromTileFeatureCollection), currentLocation, deviceHeading, fovDistance, - FeatureTree(testIntersectionsCollectionFromTileFeatureCollection) - ) - - val roadsFOVString = - moshi.adapter(FeatureCollection::class.java).toJson(fovRoadsFeatureCollection) - println("Roads in FOV: $roadsFOVString") - val intersectionsFOVString = - moshi.adapter(FeatureCollection::class.java).toJson(fovIntersectionsFeatureCollection) - println("Intersections in FOV: $intersectionsFOVString") - // TODO A lot more processing to make sense of the data above to enable a callout to the user... - - // I will need a feature collection of all the intersections in the FOV sorted by distance to the current location - val intersectionsSortedByDistance = sortedByDistanceTo( - currentLocation, - fovIntersectionsFeatureCollection - ) - val intersectionsSortedByDistanceString = - moshi.adapter(FeatureCollection::class.java).toJson(intersectionsSortedByDistance) - println("Intersections in FOV sorted by distance: $intersectionsSortedByDistanceString") - println("Number of intersections in FOV: ${intersectionsSortedByDistance.features.size}") - - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) - val intersectionsNeedsFurtherCheckingFC = FeatureCollection() - - for (i in 0 until intersectionsSortedByDistance.features.size) { - val intersectionRoadNames = getIntersectionRoadNames(intersectionsSortedByDistance.features[i], fovRoadsFeatureCollection) - val intersectionsNeedsFurtherChecking = checkWhetherIntersectionIsOfInterest(intersectionRoadNames, testNearestRoad) - if(intersectionsNeedsFurtherChecking) { - intersectionsNeedsFurtherCheckingFC.addFeature(intersectionsSortedByDistance.features[i]) - } - } - println("Intersections that need further checking: ${intersectionsNeedsFurtherCheckingFC.features.size}") - - // Approach 1: find the intersection feature with the most osm_ids and use that? - // The code does give us the correct result as it jumps to the intersection with the most osm_ids - val featureWithMostOsmIds: Feature? = intersectionsNeedsFurtherCheckingFC.features.maxByOrNull { - feature -> - (feature.foreign?.get("osm_ids") as? List<*>)?.size ?: 0 - } - - // Approach 2: Use the nearest "checked" intersection to the device location? - /*val intersectionsToCheckSortedByDistance = sortedByDistanceTo( - currentLocation.latitude, - currentLocation.longitude, - intersectionsNeedsFurtherCheckingFC - )*/ - val nearestIntersection = FeatureTree(fovIntersectionsFeatureCollection).getNearestFeature(currentLocation) - val nearestRoadBearing = getRoadBearingToIntersection(nearestIntersection, testNearestRoad, deviceHeading) - val intersectionLocation = featureWithMostOsmIds!!.geometry as Point - val intersectionRelativeDirections = getRelativeDirectionsPolygons( - LngLatAlt(intersectionLocation.coordinates.longitude, - intersectionLocation.coordinates.latitude), - nearestRoadBearing, - //fovDistance, - 5.0, - RelativeDirections.COMBINED - ) - //val newIntersectionFeatureCollection = FeatureCollection() - //newIntersectionFeatureCollection.addFeature(intersectionsToCheckSortedByDistance.features[0]) - - val intersectionRoadNames = getIntersectionRoadNames(featureWithMostOsmIds, fovRoadsFeatureCollection) - val intersectionRoadNamesString = - moshi.adapter(FeatureCollection::class.java).toJson(intersectionRoadNames) - println("Intersection roads: $intersectionRoadNamesString") - val roadRelativeDirections = getIntersectionRoadNamesRelativeDirections( - intersectionRoadNames, - featureWithMostOsmIds, - intersectionRelativeDirections - ) - - val intersectionRelativeDirectionsString = - moshi.adapter(FeatureCollection::class.java).toJson(intersectionRelativeDirections) - println("Intersection relative directions polygons: $intersectionRelativeDirectionsString") + ComplexIntersectionApproach.INTERSECTION_WITH_MOST_OSM_IDS).roads Assert.assertEquals(4, roadRelativeDirections.features.size ) // @@ -275,6 +108,4 @@ class ComplexIntersections { Assert.assertEquals("Weston Road", roadRelativeDirections.features[3].properties!!["name"]) } - - } \ No newline at end of file diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/CrossingTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/CrossingTest.kt index bf4cbc81..5c8593ec 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/CrossingTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/CrossingTest.kt @@ -45,13 +45,6 @@ class CrossingTest { fovDistance, FeatureTree(crossingsFeatureCollection) ) - // Create a FOV triangle to pick up the roads - val fovRoadsFeatureCollection = getFovFeatureCollection( - currentLocation, - deviceHeading, - fovDistance, - FeatureTree(testRoadsCollectionFromTileFeatureCollection) - ) Assert.assertEquals(1, fovCrossingFeatureCollection.features.size) val nearestCrossing = FeatureTree(fovCrossingFeatureCollection).getNearestFeature(currentLocation) @@ -66,7 +59,7 @@ class CrossingTest { // Confirm which road the crossing is on val nearestRoadToCrossing = getNearestRoad( LngLatAlt(crossingLocation.coordinates.longitude,crossingLocation.coordinates.latitude), - fovRoadsFeatureCollection + FeatureTree(testRoadsCollectionFromTileFeatureCollection) ) Assert.assertEquals(24.58, distanceToCrossing, 0.1) diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTest.kt index 8fb0f300..2870ea0c 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTest.kt @@ -2,25 +2,15 @@ package org.scottishtecharmy.soundscape import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection import org.scottishtecharmy.soundscape.geojsonparser.geojson.GeoMoshi -import org.scottishtecharmy.soundscape.geojsonparser.geojson.LineString import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import org.scottishtecharmy.soundscape.geojsonparser.geojson.Point -import org.scottishtecharmy.soundscape.geoengine.utils.RelativeDirections -import org.scottishtecharmy.soundscape.geoengine.utils.getFovFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionRoadNamesRelativeDirections import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionsFeatureCollectionFromTileFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.getNearestRoad -import org.scottishtecharmy.soundscape.geoengine.utils.getRelativeDirectionsPolygons -import org.scottishtecharmy.soundscape.geoengine.utils.getRoadBearingToIntersection import org.scottishtecharmy.soundscape.geoengine.utils.getRoadsFeatureCollectionFromTileFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.lineStringIsCircular -import org.scottishtecharmy.soundscape.geoengine.utils.removeDuplicates import com.squareup.moshi.Moshi import org.junit.Assert import org.junit.Test +import org.scottishtecharmy.soundscape.geoengine.callouts.ComplexIntersectionApproach import org.scottishtecharmy.soundscape.geoengine.utils.FeatureTree -import org.scottishtecharmy.soundscape.geoengine.utils.getFovTrianglePoints -import org.scottishtecharmy.soundscape.geoengine.utils.getIntersectionRoadNames +import org.scottishtecharmy.soundscape.geoengine.callouts.getIntersectionDescriptionFromFov class IntersectionsTest { @@ -40,58 +30,21 @@ class IntersectionsTest { featureCollectionTest!! ) ) - // create FOV to pickup the roads - val fovRoadsFeatureCollection = getFovFeatureCollection( - currentLocation, - deviceHeading, - fovDistance, - testRoadsTree - ) + // Get the intersections from the tile val testIntersectionsTree = FeatureTree( getIntersectionsFeatureCollectionFromTileFeatureCollection( featureCollectionTest ) ) - // Create a FOV triangle to pick up the intersection - val points = getFovTrianglePoints(currentLocation, deviceHeading, fovDistance) - val testNearestIntersection = testIntersectionsTree.getNearestFeatureWithinTriangle( - currentLocation, - points.left, - points.right - ) - - // This will remove the duplicate "osm_ids" from the intersection - val cleanNearestIntersection = removeDuplicates(testNearestIntersection) - - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) - val testNearestRoadBearing = getRoadBearingToIntersection(cleanNearestIntersection, testNearestRoad, deviceHeading) - - val testIntersectionRoadNames = getIntersectionRoadNames( - cleanNearestIntersection, fovRoadsFeatureCollection) - - // are any of the roads that make up the intersection circular? - for(road in testIntersectionRoadNames){ - if (lineStringIsCircular(road.geometry as LineString)){ - println("Circular path") - } - - } - - val intersectionLocation = cleanNearestIntersection!!.geometry as Point - - val intersectionRelativeDirections = getRelativeDirectionsPolygons( - intersectionLocation.coordinates, - testNearestRoadBearing, + return getIntersectionDescriptionFromFov(testRoadsTree, + testIntersectionsTree, + currentLocation, + deviceHeading, fovDistance, - RelativeDirections.COMBINED - ) - - return getIntersectionRoadNamesRelativeDirections( - testIntersectionRoadNames, - cleanNearestIntersection, - intersectionRelativeDirections) + ComplexIntersectionApproach.NEAREST_NON_TRIVIAL_INTERSECTION + ).roads } @Test diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt b/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt index 793b9947..5ec1d9c9 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt @@ -2,23 +2,13 @@ package org.scottishtecharmy.soundscape import org.junit.Assert import org.junit.Test +import org.scottishtecharmy.soundscape.geoengine.callouts.ComplexIntersectionApproach import org.scottishtecharmy.soundscape.geoengine.utils.FeatureTree import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt -import org.scottishtecharmy.soundscape.geojsonparser.geojson.Point -import org.scottishtecharmy.soundscape.geoengine.utils.RelativeDirections -import org.scottishtecharmy.soundscape.geoengine.utils.getFovFeatureCollection -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.getIntersectionsFeatureCollectionFromTileFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.getNearestRoad -import org.scottishtecharmy.soundscape.geoengine.utils.getRelativeDirectionsPolygons -import org.scottishtecharmy.soundscape.geoengine.utils.getRoadBearingToIntersection +import org.scottishtecharmy.soundscape.geoengine.callouts.getIntersectionDescriptionFromFov import org.scottishtecharmy.soundscape.geoengine.utils.getRoadsFeatureCollectionFromTileFeatureCollection -import org.scottishtecharmy.soundscape.geoengine.utils.lineStringIsCircular -import org.scottishtecharmy.soundscape.geoengine.utils.removeDuplicates import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection -import org.scottishtecharmy.soundscape.geojsonparser.geojson.LineString class IntersectionsTestMvt { private fun setupTest(currentLocation: LngLatAlt, @@ -28,60 +18,24 @@ class IntersectionsTestMvt { val featureCollectionTest = getGeoJsonForLocation(currentLocation) // Get the roads from the tile - val testRoadsCollectionFromTileFeatureCollection = + val testRoadsTree = FeatureTree( getRoadsFeatureCollectionFromTileFeatureCollection( featureCollectionTest ) - // create FOV to pickup the roads - val fovRoadsFeatureCollection = getFovFeatureCollection( - currentLocation, - deviceHeading, - fovDistance, - FeatureTree(testRoadsCollectionFromTileFeatureCollection) ) // Get the intersections from the tile - val testIntersectionsCollectionFromTileFeatureCollection = + val testIntersectionsTree = FeatureTree( getIntersectionsFeatureCollectionFromTileFeatureCollection( featureCollectionTest ) - // Create a FOV triangle to pick up the intersection (this intersection is a transition from - // Weston Road to Long Ashton Road) - val points = getFovTrianglePoints(currentLocation, deviceHeading, fovDistance) - val testNearestIntersection = FeatureTree(testIntersectionsCollectionFromTileFeatureCollection).getNearestFeatureWithinTriangle( + ) + return getIntersectionDescriptionFromFov(testRoadsTree, + testIntersectionsTree, currentLocation, - points.left, - points.right) - // This will remove the duplicate "osm_ids" from the intersection - val cleanNearestIntersection = removeDuplicates(testNearestIntersection) - - // what relative direction(s) are the road(s) that make up the nearest intersection? - // what road are we nearest to? - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) - - val testNearestRoadBearing = getRoadBearingToIntersection(cleanNearestIntersection, testNearestRoad, deviceHeading) - // what is the road direction type in relation to the nearest intersection and nearest road - - val testIntersectionRoadNames = getIntersectionRoadNames(cleanNearestIntersection, fovRoadsFeatureCollection) - - // are any of the roads that make up the intersection circular? - for(road in testIntersectionRoadNames){ - if (lineStringIsCircular(road.geometry as LineString)){ - println("Circular path") - } - } - - val intersectionLocation = cleanNearestIntersection!!.geometry as Point - val intersectionRelativeDirections = getRelativeDirectionsPolygons( - intersectionLocation.coordinates, - testNearestRoadBearing, + deviceHeading, fovDistance, - RelativeDirections.COMBINED - ) - - return getIntersectionRoadNamesRelativeDirections( - testIntersectionRoadNames, - cleanNearestIntersection, - intersectionRelativeDirections) + ComplexIntersectionApproach.NEAREST_NON_TRIVIAL_INTERSECTION + ).roads } @Test fun intersectionsStraightAheadType(){ diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/RoundaboutsTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/RoundaboutsTest.kt index 660d588d..55d4b335 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/RoundaboutsTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/RoundaboutsTest.kt @@ -114,7 +114,7 @@ class RoundaboutsTest { val boundingBoxOfCircle = getBoundingBoxOfLineString(roundaboutCircleRoad.features[0].geometry as LineString) val boundingBoxOfCircleCorners = getBoundingBoxCorners(boundingBoxOfCircle) val centerOfBoundingBox = getCenterOfBoundingBox(boundingBoxOfCircleCorners) - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) + val testNearestRoad = getNearestRoad(currentLocation, FeatureTree(fovRoadsFeatureCollection)) val testNearestRoadBearing = getRoadBearingToIntersection(nearestIntersection, testNearestRoad, deviceHeading) val roundaboutRoadsRelativeDirections = getRelativeDirectionsPolygons( @@ -220,7 +220,7 @@ class RoundaboutsTest { // This will remove the duplicate "osm_ids" from the intersection val cleanNearestIntersection = removeDuplicates(nearestIntersection) - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) + val testNearestRoad = getNearestRoad(currentLocation, FeatureTree(fovRoadsFeatureCollection)) val testNearestRoadBearing = getRoadBearingToIntersection(cleanNearestIntersection, testNearestRoad, deviceHeading) @@ -298,7 +298,7 @@ class RoundaboutsTest { println("Number of roads that make up the nearest intersection ${intersectionRoadNames.features.size}") // I need to test that the intersection roads have // "oneway" and "yes" tags and that the road names are all the same - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) + val testNearestRoad = getNearestRoad(currentLocation, FeatureTree(fovRoadsFeatureCollection)) for (road in intersectionRoadNames) { if(testNearestRoad!!.properties?.get("name") == road.properties?.get("name") && road.properties?.get("oneway") == "yes"){ diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/StreetPreviewTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/StreetPreviewTest.kt index ebd6efbc..60adacb9 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/StreetPreviewTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/StreetPreviewTest.kt @@ -39,7 +39,7 @@ class StreetPreviewTest { featureCollectionTest!!) val nearestRoad = getNearestRoad( LngLatAlt(-2.693002695425122,51.43938442591545), - roadFeatureCollectionTest + FeatureTree(roadFeatureCollectionTest) ) val nearestRoadTest = FeatureCollection() nearestRoadTest.addFeature(nearestRoad!!) @@ -103,7 +103,7 @@ class StreetPreviewTest { val nearestRoadTest = roadFeatureCollectionTest?.let { getNearestRoad( LngLatAlt(-2.693002695425122,51.43938442591545), - it + FeatureTree(it) ) } // trace along the road with equidistant points 30m apart. @@ -164,7 +164,7 @@ class StreetPreviewTest { if (fovRoadsFeatureCollection.features.size > 0) { val nearestRoad = getNearestRoad( currentLocation, - fovRoadsFeatureCollection + FeatureTree(roadFeatureCollectionTest) ) if (nearestRoad!!.properties?.get("name") != null) { @@ -188,7 +188,7 @@ class StreetPreviewTest { val testNearestRoad = getNearestRoad( currentLocation, - fovRoadsFeatureCollection + FeatureTree(roadFeatureCollectionTest) ) val intersectionsNeedsFurtherCheckingFC = FeatureCollection() @@ -261,7 +261,7 @@ class StreetPreviewTest { // Confirm which road the crossing is on val nearestRoadToCrossing = getNearestRoad( crossingLocation.coordinates, - fovRoadsFeatureCollection + FeatureTree(roadFeatureCollectionTest) ) val crossingCallout = buildString { @@ -290,7 +290,7 @@ class StreetPreviewTest { // Confirm which road the crossing is on val nearestRoadToBus = getNearestRoad( busStopLocation.coordinates, - fovRoadsFeatureCollection + FeatureTree(roadFeatureCollectionTest) ) val busStopCallout = buildString { diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/TileUtilsTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/TileUtilsTest.kt index c447de92..491fc253 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/TileUtilsTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/TileUtilsTest.kt @@ -527,7 +527,7 @@ class TileUtilsTest { ) // This should pick up three roads in the FoV Assert.assertEquals(3, fovRoadsFeatureCollection.features.size) - val nearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) + val nearestRoad = getNearestRoad(currentLocation, FeatureTree(testRoadsCollectionFromTileFeatureCollection)) // Should only be the nearest road in this Feature Collection assert(nearestRoad != null) // The nearest road to the current location should be Weston Road diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/VisuallyCheckIntersectionLayers.kt b/app/src/test/java/org/scottishtecharmy/soundscape/VisuallyCheckIntersectionLayers.kt index ff3ad212..89568349 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/VisuallyCheckIntersectionLayers.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/VisuallyCheckIntersectionLayers.kt @@ -85,7 +85,7 @@ class VisuallyCheckIntersectionLayers { fovIntersectionsFeatureCollection ) // Get the nearest Road in the FoV - val testNearestRoad = getNearestRoad(currentLocation, fovRoadsFeatureCollection) + val testNearestRoad = getNearestRoad(currentLocation, FeatureTree(testRoadsCollectionFromTileFeatureCollection)) val intersectionsNeedsFurtherCheckingFC = FeatureCollection() for (i in 0 until intersectionsSortedByDistance.features.size) { val intersectionRoadNames = getIntersectionRoadNames(intersectionsSortedByDistance.features[i], fovRoadsFeatureCollection) @@ -137,7 +137,7 @@ class VisuallyCheckIntersectionLayers { val crossingLocation = nearestCrossing!!.geometry as Point val nearestRoadToCrossing = getNearestRoad( LngLatAlt(crossingLocation.coordinates.longitude,crossingLocation.coordinates.latitude), - fovRoadsFeatureCollection + FeatureTree(testRoadsCollectionFromTileFeatureCollection) ) // *** End of Crossing @@ -153,7 +153,6 @@ class VisuallyCheckIntersectionLayers { Assert.assertEquals(7, roadRelativeDirections.features[2].properties!!["Direction"]) Assert.assertEquals("Clevedon Road", roadRelativeDirections.features[2].properties!!["name"]) - // ************************************************************* // *** Display Field of View triangle ***