diff --git a/.gitignore b/.gitignore index 28d4534fc..f5f8a3da9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,8 +12,8 @@ /captures .externalNativeBuild .cxx +app/gpxFiles/* app/*.geojson -app/callout*.txt # Gradle files .gradle/ 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 3eb4aaff2..58deb9fa6 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/GeoEngine.kt @@ -360,7 +360,8 @@ class GeoEngine { mapMatchFilter.filter( LngLatAlt(location.longitude, location.latitude), gridState, - FeatureCollection() + FeatureCollection(), + false ) } val matchedWay = mapMatchFilter.matchedWay 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 index 4e0cad850..b6967f886 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/callouts/IntersectionUtils.kt @@ -14,7 +14,7 @@ import org.scottishtecharmy.soundscape.geoengine.mvttranslation.Intersection import org.scottishtecharmy.soundscape.geoengine.mvttranslation.Way import org.scottishtecharmy.soundscape.geoengine.mvttranslation.WayEnd import org.scottishtecharmy.soundscape.geoengine.utils.Direction -import org.scottishtecharmy.soundscape.geoengine.utils.calculateHeadingOffset +import org.scottishtecharmy.soundscape.geoengine.utils.calculateSmallestAngleBetweenLines import org.scottishtecharmy.soundscape.geoengine.utils.checkWhetherIntersectionIsOfInterest import org.scottishtecharmy.soundscape.geoengine.utils.confectNamesForRoad import org.scottishtecharmy.soundscape.geoengine.utils.findShortestDistance @@ -100,7 +100,8 @@ fun getRoadsDescriptionFromFov(gridState: GridState, userGeometry.ruler.distanceToLineString(userGeometry.mapMatchedLocation.point, road.geometry as LineString) val snappedHeading = userGeometry.snappedHeading() if (snappedHeading != null) { - if (calculateHeadingOffset(roadDistance.heading, snappedHeading) > 45.0) { + var innerAngle = calculateSmallestAngleBetweenLines(roadDistance.heading, snappedHeading) + if (innerAngle > 45.0) { // This way is not at the angle of travel, so skip it continue } @@ -369,6 +370,7 @@ fun addIntersectionCalloutFromDescription( val incomingHeading = (heading.toDouble() + 180.0) % 360.0 val directions = getCombinedDirectionSegments(incomingHeading) + val intersectionResults = emptyList().toMutableList() for (way in description.intersection.members) { val wayHeading = way.heading(description.intersection) @@ -408,7 +410,7 @@ fun addIntersectionCalloutFromDescription( var destinationText = way.getName(way.intersections[WayEnd.START.id] == description.intersection, gridState, localizedContext) val intersectionCallout = localizedContext?.getString(roadDirectionId, destinationText) ?: "\t$destinationText $unlocalizedDirection" - results.add( + intersectionResults.add( PositionedString( text = intersectionCallout, type = AudioType.COMPASS, @@ -417,4 +419,9 @@ fun addIntersectionCalloutFromDescription( ) } } + // Order intersection callout by heading from left to right + intersectionResults.sortWith(Comparator { p1, p2 -> + p1.heading!!.compareTo(p2.heading!!) + }) + results.addAll(intersectionResults) } diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/filters/MapMatchFilter.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/filters/MapMatchFilter.kt index 493242d6d..fc781eb21 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/filters/MapMatchFilter.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/filters/MapMatchFilter.kt @@ -12,7 +12,7 @@ import org.scottishtecharmy.soundscape.geoengine.utils.rulers.CheapRuler import org.scottishtecharmy.soundscape.geoengine.utils.PointAndDistanceAndHeading import org.scottishtecharmy.soundscape.geoengine.utils.addSidewalk import org.scottishtecharmy.soundscape.geoengine.utils.bearingFromTwoPoints -import org.scottishtecharmy.soundscape.geoengine.utils.calculateHeadingOffset +import org.scottishtecharmy.soundscape.geoengine.utils.calculateSmallestAngleBetweenLines import org.scottishtecharmy.soundscape.geoengine.utils.circleToPolygon import org.scottishtecharmy.soundscape.geoengine.utils.clone import org.scottishtecharmy.soundscape.geoengine.utils.findShortestDistance @@ -26,7 +26,6 @@ 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.geojsonparser.moshi.GeoJsonObjectMoshiAdapter -import kotlin.math.abs import kotlin.math.asin import kotlin.math.cos import kotlin.math.min @@ -96,19 +95,29 @@ class IndexedLineString { } line = LineString() - var nextIntersection = route[0].doesIntersect(route[1]) - if(nextIntersection == null) { - // We can get here if the tile grid is recalculated, and tiles rejoined and the route is - // left with out of date and disconnected ways. - line = null - return - } - var firstIntersection = route[0].getOtherIntersection(nextIntersection) - var forwards = nextIntersection != route[0].intersections[WayEnd.START.id] - for ((index, way) in route.withIndex()) { - if (index != 0) { - forwards = firstIntersection != way.intersections[WayEnd.END.id] + + var forwards = true + if(index < route.size - 1) { + // Most of the Ways in the route + val (nextIntersection, ourIndex) = route[index].doesIntersect(route[index + 1]) + if(nextIntersection == null) { + // We can get here if the tile grid is recalculated, and tiles rejoined and the route is + // left with out of date and disconnected ways. + line = null + return + } + forwards = (ourIndex == WayEnd.END.id) + } else { + // The last Way in the route + val (firstIntersection, ourIndex) = route[index].doesIntersect(route[index - 1]) + if(firstIntersection == null) { + // We can get here if the tile grid is recalculated, and tiles rejoined and the route is + // left with out of date and disconnected ways. + line = null + return + } + forwards = (ourIndex == WayEnd.START.id) } // And add its coordinates to the LineString along with whether or not we reversed the @@ -116,33 +125,14 @@ class IndexedLineString { // each intersection, but is simple. if (forwards) { line!!.coordinates.addAll((way.geometry as LineString).coordinates) - direction?.set(index, true) } else { line!!.coordinates.addAll((way.geometry as LineString).coordinates.reversed()) - direction?.set(index, false) } + direction?.set(index, forwards) // Note the index at which this Way ends indices?.set(index, line!!.coordinates.size) - - if(firstIntersection != null) { - firstIntersection = way.getOtherIntersection(firstIntersection) - if(firstIntersection?.intersectionType == IntersectionType.TILE_EDGE) { - for(member in firstIntersection!!.members) { - if(member.wayType == WayType.JOINER) { - if((member.intersections[WayEnd.START.id] == null) && - (member.intersections[WayEnd.END.id] == null)) - { - assert(false) - } else { - firstIntersection = member.getOtherIntersection(firstIntersection!!) - break - } - } - } - } - } } hashCode = line?.coordinates.hashCode() } @@ -294,7 +284,7 @@ class RoadFollower(val parent: MapMatchFilter, var minIndex = Int.MAX_VALUE var maxIndex = -1 for((index, way) in route.withIndex()) { - if(way.doesIntersect(newWay) != null) { + if(way.doesIntersect(newWay).first != null) { if(index < minIndex) minIndex = index if(index > maxIndex) maxIndex = index } @@ -356,12 +346,12 @@ class RoadFollower(val parent: MapMatchFilter, } var newRoute = emptyList().toMutableList() - if(route.first().doesIntersect(newWay) != null) { + if(route.first().doesIntersect(newWay).first != null) { if(!route.first().intersections.contains(null)) { newRoute.add(newWay) newRoute.addAll(route) } - } else if(route.last().doesIntersect(newWay) != null) { + } else if(route.last().doesIntersect(newWay).first != null) { if(!route.last().intersections.contains(null)) { newRoute.addAll(route) newRoute.add(newWay) @@ -370,15 +360,17 @@ class RoadFollower(val parent: MapMatchFilter, assert(false) } if(newRoute.isNotEmpty()) { - if (newRoute[0].doesIntersect(newRoute[1]) == null) { + if (newRoute[0].doesIntersect(newRoute[1]).first == null) { assert(false) } route = newRoute validateRoute() ils.updateFromRoute(route) - nearestPoint?.let { point -> - nearestPoint = ruler.distanceToLineString(point.point, ils.line as LineString) + if(ils.line != null) { + nearestPoint?.let { point -> + nearestPoint = ruler.distanceToLineString(point.point, ils.line as LineString) + } } directionOnLine = 0.0 directionHysteresis = 5 @@ -455,7 +447,7 @@ class RoadFollower(val parent: MapMatchFilter, !nearestPoint.positionAlongLine.isNaN()) { val delta = nearestPoint.positionAlongLine - lastNearestPoint.positionAlongLine - if((directionOnLine != 0.0) && (sign(delta) != sign(directionOnLine))) { + if((directionOnLine != 0.0) && ((sign(delta) != sign(directionOnLine)) || (delta == 0.0))) { // Change of direction? --directionHysteresis if(directionHysteresis == 0) { @@ -554,9 +546,7 @@ class RoadFollower(val parent: MapMatchFilter, val roadHeading = nearestPoint?.heading if (!gpsHeading.isNaN() && roadHeading != null) { - var headingDifference = calculateHeadingOffset(gpsHeading, roadHeading) - if (headingDifference > 90.0) - headingDifference = 180.0 - headingDifference + var headingDifference = calculateSmallestAngleBetweenLines(gpsHeading, roadHeading) val cosHeadingDifference = cos(toRadians(headingDifference)) if (cosHeadingDifference < 0.703) { // Unreliable GPS heading - the GPS is moving in a very different direction @@ -679,7 +669,7 @@ class MapMatchFilter { while(iterator.hasNext()) { val follower = iterator.next() val lastWayInRoute = follower.route.last() - val intersection = lastWayInRoute.doesIntersect(way) + var (intersection, ourIndex) = lastWayInRoute.doesIntersect(way) if (intersection != null) { // This road intersects with the last way, so it can either replace it, or // be added on to it. @@ -687,7 +677,7 @@ class MapMatchFilter { added = true } else { val firstWayInRoute = follower.route.first() - val firstIntersection = firstWayInRoute.doesIntersect(way) + val (firstIntersection, ourIndex2) = firstWayInRoute.doesIntersect(way) if (firstIntersection != null) { // This road intersects with the first way, so it can either replace it, // or be added on to it. @@ -725,7 +715,7 @@ class MapMatchFilter { } var colorIndex = 0 - fun filter(location: LngLatAlt, gridState: GridState, collection: FeatureCollection): Triple { + fun filter(location: LngLatAlt, gridState: GridState, collection: FeatureCollection, dump: Boolean): Triple { extendFollowerList(location, gridState) @@ -792,7 +782,7 @@ class MapMatchFilter { } } if(useDijkstra) { - val testDistance = (follower.averagePointGap * 3) + 10.0 + val testDistance = (follower.averagePointGap * 8) + 15.0 val shortestDistance = findShortestDistance( matchedLocation!!.point, matched, diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/mvttranslation/WayGenerator.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/mvttranslation/WayGenerator.kt index 77d73918b..bbe6a9d44 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/mvttranslation/WayGenerator.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/mvttranslation/WayGenerator.kt @@ -170,27 +170,34 @@ class Way : Feature() { } } - fun doesIntersect(other: Way) : Intersection? { - for(ours in intersections) { + fun doesIntersect(other: Way) : Pair { + for((ourIndex ,ours) in intersections.withIndex()) { if(ours == null) continue for(theirs in other.intersections) { if(theirs == null) continue // Check for direct intersection first if(ours == theirs) - return ours + return Pair(ours, ourIndex) + } + } + + for((ourIndex ,ours) in intersections.withIndex()) { + if(ours == null) continue + for(theirs in other.intersections) { + if(theirs == null) continue // Check for tile-edge joiner if((ours.intersectionType == IntersectionType.TILE_EDGE) && - (theirs.intersectionType == IntersectionType.TILE_EDGE)) { + (theirs.intersectionType == IntersectionType.TILE_EDGE)) { for(member in ours.members) { if(theirs.members.contains(member)) { - return theirs + return Pair(theirs, ourIndex) } } } } } - return null + return Pair(null, 0) } fun isSidewalkOrCrossing() : Boolean { @@ -381,8 +388,8 @@ class Way : Feature() { newWay1.length = length1 val newWay2 = Way() - newWay2.intersections[0] = intersections[1] - newWay2.intersections[1] = newIntersection + newWay2.intersections[0] = newIntersection + newWay2.intersections[1] = intersections[1] newWay2.geometry = line2 newWay2.length = length2 diff --git a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/GeoUtils.kt b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/GeoUtils.kt index 07928812c..cc4af7d98 100644 --- a/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/GeoUtils.kt +++ b/app/src/main/java/org/scottishtecharmy/soundscape/geoengine/utils/GeoUtils.kt @@ -1209,11 +1209,30 @@ fun pointIsWithinBoundingBox(point: LngLatAlt?, box: BoundingBox) : Boolean { * heading of a road. * @param heading1 * @param heading2 - * @return The inner angle between the two headings in degrees. + * @return The inner angle between the two headings in degrees. This can be up to 180.0 degrees + * because that's the maximum difference between headings. */ fun calculateHeadingOffset(heading1: Double, heading2: Double): Double { - var diff = abs(heading1 - heading2) % 360.0 + val normalizedHeading1 = (heading1 % 360.0 + 360.0) % 360.0 + val normalizedHeading2 = (heading2 % 360.0 + 360.0) % 360.0 + var diff = abs(normalizedHeading1 - normalizedHeading2) if (diff > 180.0) diff = 360.0 - diff return diff } + +/** + * calculateSmallestAngleBetweenLines calculates the smallest angle between two lines. + * + * heading of a road. + * @param heading1 + * @param heading2 + * @return The inner angle between the two lines in degrees. This can be up to 90.0 degrees as it's + * the smallest angle between the two lines. + */ +fun calculateSmallestAngleBetweenLines(heading1: Double, heading2: Double): Double { + var innerAngle = calculateHeadingOffset(heading1, heading2) + if (innerAngle > 90.0) + innerAngle = 180.0 - innerAngle + return innerAngle +} 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 b6980d26b..ae7da480d 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 @@ -563,6 +563,10 @@ fun checkWhetherIntersectionIsOfInterest( if(testNearestRoad == null) return 0 + // We don't announce intersections with only 2 or fewer Ways + if(intersection.members.size <= 2) + return -1 + var needsFurtherChecking = 0 val setOfNames = emptySet().toMutableSet() for (way in intersection.members) { diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt b/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt index a070febbe..e3be85681 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/IntersectionsTestMvt.kt @@ -21,7 +21,8 @@ class IntersectionsTestMvt { mapMatchFilter.filter( location = currentLocation, gridState = gridState, - collection = FeatureCollection() + collection = FeatureCollection(), + dump = false ) val userGeometry = UserGeometry( location = currentLocation, diff --git a/app/src/test/java/org/scottishtecharmy/soundscape/MvtTileTest.kt b/app/src/test/java/org/scottishtecharmy/soundscape/MvtTileTest.kt index e12e890a1..1b81fb597 100644 --- a/app/src/test/java/org/scottishtecharmy/soundscape/MvtTileTest.kt +++ b/app/src/test/java/org/scottishtecharmy/soundscape/MvtTileTest.kt @@ -37,6 +37,10 @@ import org.scottishtecharmy.soundscape.geoengine.utils.createPolygonFromTriangle import org.scottishtecharmy.soundscape.geoengine.utils.getFovTriangle import org.scottishtecharmy.soundscape.geojsonparser.geojson.FeatureCollection import org.scottishtecharmy.soundscape.geojsonparser.geojson.LngLatAlt +import kotlin.io.path.Path +import kotlin.io.path.listDirectoryEntries +import kotlin.io.path.name +import kotlin.io.path.nameWithoutExtension /** * FileGridState overrides ProtomapsGridState updateTile to get tiles from test resources instead of @@ -99,11 +103,10 @@ private fun vectorTileToGeoJsonFromFile( } private fun parseGpxFromFile(filename: String): FeatureCollection { - val path = "src/test/res/org/scottishtecharmy/soundscape/" val fc = FeatureCollection() var currentFeature = Feature() - File(path + filename).useLines { lines -> + File(filename).useLines { lines -> lines.forEach { line -> // Get the location @@ -579,13 +582,11 @@ class MvtTileTest { ) // Update the nearest road filter with our new location - if(startIndex + index == 526) { - mapMatchFilter.dump() - } val mapMatchedResult = mapMatchFilter.filter( LngLatAlt(location.longitude, location.latitude), gridState, - collection + collection, + false ) if(mapMatchedResult.first != null) { @@ -643,13 +644,20 @@ class MvtTileTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun testCallouts() { - testMovingGrid("travel.gpx", "callout-travel.txt", "map-matching.geojson") - testMovingGrid("travel-2.gpx", "callout-travel-2.txt", "map-matching-2.geojson") - testMovingGrid("travel-3.gpx", "callout-travel-3.txt", "map-matching-3.geojson") -// testMovingGrid("travel-4.gpx", "callout-travel-4.txt", "map-matching-4.geojson") -// testMovingGrid("travel-5.gpx", "callout-travel-5.txt", "map-matching-5.geojson") -// testMovingGrid("travel-6.gpx", "callout-travel-6.txt", "map-matching-6.geojson") -// testMovingGrid("travel-7.gpx", "callout-travel-7.txt", "map-matching-7.geojson") + + val directoryPath = Path("src/test/res/org/scottishtecharmy/soundscape/gpxFiles/") + + + val resultsStoragePath = "gpxFiles/" + val resultsStorageDir = File(resultsStoragePath) + if (!resultsStorageDir.exists()) { + resultsStorageDir.mkdirs() + } + + val directoryEntries = directoryPath.listDirectoryEntries("*.gpx") + for(file in directoryEntries) { + testMovingGrid(file.toString(), "gpxFiles/${file.nameWithoutExtension}.txt", "gpxFiles/${file.nameWithoutExtension}.geojson") + } } @OptIn(ExperimentalCoroutinesApi::class) diff --git a/app/src/test/res/org/scottishtecharmy/soundscape/travel-2.gpx b/app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel-2.gpx similarity index 100% rename from app/src/test/res/org/scottishtecharmy/soundscape/travel-2.gpx rename to app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel-2.gpx diff --git a/app/src/test/res/org/scottishtecharmy/soundscape/travel-3.gpx b/app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel-3.gpx similarity index 100% rename from app/src/test/res/org/scottishtecharmy/soundscape/travel-3.gpx rename to app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel-3.gpx diff --git a/app/src/test/res/org/scottishtecharmy/soundscape/travel-small.gpx b/app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel-small.gpx similarity index 100% rename from app/src/test/res/org/scottishtecharmy/soundscape/travel-small.gpx rename to app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel-small.gpx diff --git a/app/src/test/res/org/scottishtecharmy/soundscape/travel.gpx b/app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel.gpx similarity index 100% rename from app/src/test/res/org/scottishtecharmy/soundscape/travel.gpx rename to app/src/test/res/org/scottishtecharmy/soundscape/gpxFiles/travel.gpx