Soundscape-Android

GeoEngine introduction

The GeoEngine module takes care of parsing map tile data and making that data available to the rest of the app for generating audio callouts and some UI. The classes at the bottom levl are based around GeoJSON objects as per https://en.wikipedia.org/wiki/GeoJSON. Originally, the app was parsing GeoJSON from the soundscape-backend server and this was the natural result. However, now that we’ve switched to parsing Mapbox Vector Tiles the use of GeoJSON classes is somewhat legacy, though it does ease debugging as it’s easy to output GeoJSON and render it on top of other maps e.g. using geojson.io . Here’s a simplified view of the basic classes involved:

classDiagram class Feature{ // A GeoJSON Feature geometry: Point/LineString/Polygon properties: HashMap foreign: HashMap } class FeatureCollection{ // A container for Features from GeoJSON } class Way { // Each way represents a road/path segment between two intersections val intersection: Intersection[2] // Start and End of this Way fun followWays(fromIntersection: Intersection?) fun getName(direction: Boolean): String fun doesIntersect(other: Way): Boolean fun direction(fromIntersection: Intersection, deviceHeading: Double) : Direction fun heading(fromIntersection: Intersection): Double } class Intersection { // An Intersection is the point at which multiple Ways connect val members: List of Way val location: LngLatAlt } Way <|-- Feature Intersection <|-- Feature FeatureCollection *-- Feature class FeatureTree{ // An r-tree for searching Features based on location fun getNearestFeature(location: LngLatAlt, distance: Double): Feature? fun getNearestCollection(location: LngLatAlt, distance: Double, maxCount: Int, initialCollection: FeatureCollection?): FeatureCollection fun getNearestCollectionWithinTriangle(triangle: Triangle, maxCount: Int): FeatureCollection fun getContainingPolygons(location: LngLatAlt) : FeatureCollection } FeatureTree *-- Feature

Features are points, lines or polygons along with some metadata describing what they are. We construct these based on the Mapbox Vector Tile contents and then divide them up into separate searchable FeatureTrees depending on the type of feature they are.

Parsing the MVT (MapBox Vector Tiles)

Each MVT tile is processed layer by layer creating Features for all of the points that we are interested in. The main layers of interest are the POI which contains points and polygons for all of the various points of interest, and the transportation layers which contains roads and paths. We don’t currently parse other layers (e.g. groundcover, water, housenumbers), though we may add some of those in future if we want to use that data e.g. rivers and streams. After the initial parsing, some post-processing is done on the roads with the aim of:

The TileGrid

At any time the app is working on a 2x2 grid of tiles. After parsing each tile independently, Ways which cross the tile boundaries need joining together, as do Polygons. The result is a set of FeatureCollections which cover the whole grid. The post processing on the grid is then:

Use the FeatureTrees

Searching the FeatureTrees is reasonably efficient, though multiple searches should be avoided due to CPU performance costs. They are very useful when generating the audio callouts. For nearby POI, getNearestCollection can be called to find POI within a certain distance from the user location. The alternative getNearestCollectionWithinTriangle can be used to search within a triangle rather than within a circle. If we do this with a Triangle created with one corner at the user location we can do a ‘field-of-view’ search. This also works for searching roads and paths.

Traverse the Ways and Intersections

Once code has an Intersection or a Way it can follow Ways across the map tiles. Each Way has a reference to Intersections at either end and each Intersection contains a list of all of the Ways which comprise it. This traversal is very efficient and makes these features straightforward:

Map Matching

When describing intersections, it’s very important to describe them from the correct road coming into the intersection. The MapMatchFilter is the class which aims to track that road. Map matching is the term used to describe matching raw GPS locations on to map data. This is relatively straightforward in car satnavs, but doing it for pedestrians is a bit harder. Pedestrians can walk on paths, in any direction, cross roads and indeed cross open space not even using a path. There are lots of algorithms based on hidden Markov models that are designed to do this, but many of them are aimed at matching already completed GPS tracks rather than predicting live the next location. In fact it’s possible that most applications don’t actually need pedestrian map matching. The paper on which our algorithm is based is

An Improved Map-Matching Technique Based on the Fréchet Distance Approach for Pedestrian Navigation Services” by Yoonsik Bang, Jiyoung Kim and Kiyun Yu.

However, this paper doesn’t describe how to create the paths on which to run the algorithm. I think our approach can be improved, but it does work well enough for a start.

APIs

Most of the GeoEngine APIs use two pieces of data: