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:
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.
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:
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:
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.
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:
"Roselea Drive"
rather than just "path"
) we add a tag to each un-named way that joins that intersection. The tag will either be "destination:forward"
or "destination:backward"
depending on which way the Way runs (in to or out of the intersection). These can be used when describing paths later when approaching intersections e.g. "Path to Roselea Drive"
when travelling one way, or "Path to Mosswell Road"
when travelling in the opposite direction. We also add tags indicating dead-ends which is a useful way of filtering callouts e.g. don’t call out un-named dead-ends - there are a lot of dead-end service roads in my local area, and calling them adds little context.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
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.
Most of the GeoEngine APIs use two pieces of data:
GridState
which contains all of the FeatureTrees
for the current TileGrid
UserGeometry
which contains all of the information about the current user location, headings and map matched location. The heading calculations aim to match iOS which uses the phone heading, GPS travel heading as well as head tracking heading (not yet implemented on Android). It’s possible to have no heading at all if the user isn’t moving and the phone is locked and the phone is not held flat.