Building an OpenStreetMap app in Rust, Part VI
By: Tor Hovland
Tags: Openstreetmap, Technology, Rust, Webassembly, Geolocation

OSM chunks

So far, we have downloaded OSM data from https://openstreetmap.org/api. That API gives you all objects within a bounding box, and is really meant to be used by map editing apps. It’s also quite restrictive with regard to the size of the bounding box you can use, although that’s not really a concern for us. Still, a better option is to use the Overpass API. It lets you post very specific queries against OSM, and it comes with an interactive query editor.

In OSM the term “way” seems like an odd name for what is essentially polylines. A way isn’t just streets and stuff, but also coastlines, perimeters, borders, creeks, power lines, etc. What we’re interested in are ways tagged as “highways”, which is another odd term for any kind of road, track, or path. The query we need to run can be specified like this:

https://overpass-api.de/api/interpreter?data=[bbox];way[highway];(._;>;);out;&bbox=10.29,63.40,10.30,63.42

The funny bit within parens means that we don’t just need the actual way objects, but also the nodes that they refer to. That’s where the coordinates are.

Now, how big bounding box do we want? Obviously, we don’t want the user to download a lot of data that isn’t needed. But we also don’t want to hammer the API with lots of minimal queries. Out of curiosity, I built a tool that would follow a GPX track, fire an OSM query whenever it moved outside a triggering bounding box, and count the number of queries, the total downloaded data, and the total time waiting for HTTP requests to complete.

Of course, the actual results would depend on the GPX file, and there isn’t going to be one chunk size that is always optimal. For example, in urban areas a smaller box is needed than in remote areas. But it gives a rough idea.

I found that a box with “radius” above about a kilometer yields significantly more data to download, while a box below 500 meters only serves to increase the number of requests and total request time. Hence, I’ll stick with a kilometer for now.

Random walk

We’ll be adding geolocation to the app, but for testing purposes it makes sense to simulate movement. So, I’ve added an option to run a random walk within the app. You enable it with a random_walk query string parameter. In the init() function I can use it to enable a stream of RandomWalk messages:

if url.search().contains_key("random_walk") {

orders.stream(streams::interval(1000, || Msg::RandomWalk));

}

In the update() function I handle the messages like this:

Msg::RandomWalk => {

if let Some(pos) = &model.position {

let bbox = pos.bbox(model.osm_chunk_radius); let mut rng = thread_rng();

let bearing = rng.gen_range(0.0..360.0);

let distance = rng.gen_range(0.0..200.0);

model.position = Some(destination(pos, bearing, distance)); map::pan_to_position(&model);

map::render_position(&model); if model.is_outside_osm_trigger_box() {

info!("Outside OSM trigger box. Initiating download.");

model.osm_chunk_position = model.position; orders.perform_cmd(async move {

Msg::OsmFetched(send_osm_request(&bbox).await)

});

} // Make sure the map is centered on our position even if

// the size of the map has changed

orders.after_next_render(|_| Msg::InvalidateMapSize);

}

}

The result can be seen below. The outer square represents the download box, and you can see that we have indeed downloaded all the roads (shown in green) within that box. The road that happens to be nearest us is colored blue, and is the one we show tags and distances for at the bottom.

Notice that when we step outside the inner square, which is the download trigger box, a new set of roads is fetched, and the map gets updated.

Geolocation

The final piece necessary to make this app any useful at all is to add geolocation. Remember back in part I, when I said I expected a number of rough spots while developing this app on a rather bleeding edge platform? Well, I must say that things have gone surprisingly smoothly! But not entirely without issues, and I did run into some trouble with geolocation.

Now, wasm-bingen (or web-sys , really) does support the Geolocation API, but as it turns out, not in a way that works. I’m supposed to be able to do this:

fn geo_callback(position: JsValue) {

let s = js_sys::JSON::stringify(&position)

.expect("Unable to stringify JSON"); info!("Geo callback: {:?}", s);

} let geolocation = web_sys::window()

.expect("Unable to get browser window.")

.navigator()

.geolocation()

.expect("Unable to get geolocation."); let geo_callback_function =

Closure::wrap(

Box::new(|pos| geo_callback(pos)) as Box<dyn Fn(JsValue)>

); geolocation.get_current_position(

&geo_callback_function.as_ref().unchecked_ref()

).expect("Unable to get position"); geo_callback_function.forget();

Never mind the scary type gymnastics involved here. That’s kind of just how wasm-bindgen works. It’s supposed to be low-level, and to let toolkits like Gloo figure out how to build a more accessible abstraction on top.

Oh, and the forget at the bottom there tells Rust not to run the destructor on geo_callback_function when it goes out of scope. That would kill the closure, and any hope of getting a Geolocation callback from the browser.

Anyway, the point is that the only output our callback function logs when it gets a position is:

Geo callback: "{}"

We expected an object with a coords property and a timestamp. The problem is that the GeolocationPosition object doesn’t really offer these as properties, but rather as getter functions on the object’s prototype.

As a workaround I implemented the Geolocation in Javascript, and then used wasm-bindgen to pass the resulting coordinates from Javascript to Rust. But I got some help on the GitHub issue I logged as well as on the Rust Discord, and the better solution is to define the correct bindings yourself:

#[wasm_bindgen]

extern "C" {

type GeolocationCoordinates; #[wasm_bindgen(method, getter)]

fn latitude(this: &GeolocationCoordinates) -> f64; #[wasm_bindgen(method, getter)]

fn longitude(this: &GeolocationCoordinates) -> f64; type GeolocationPosition; #[wasm_bindgen(method, getter)]

fn coords(this: &GeolocationPosition) ->

GeolocationCoordinates;

}

With that in place, I can change my callback function to:

let (app, msg_mapper) = (orders.clone_app(), orders.msg_mapper()); let geo_callback = move |position: JsValue| {

let pos: GeolocationPosition = position.into();

let coords = pos.coords(); app.update(msg_mapper(Msg::Position(

coords.latitude(),

coords.longitude(),

)));

};

So rather than pulling data out from properties, we are now calling getter functions.

On the first line there, Seed is letting me make a cheap clone of the app, as well as a message mapper which can turn a message into whatever app.update() needs. This lets the callback own its own things. If we tried just letting it borrow them, we would run into lifetime issues. The browser callbacks have a static lifetime, which theoretically means they could live longer than the Seed app!

The move keyword means that we let the closure take ownership of the things it needs ( app and msg_mapper ).

The Msg::Position message is being handled pretty much the same way as the Msg::RandomWalk above, except that we don’t generate a random location, of course.

Two more things: First, we don’t want to just get location once, but rather let the browser watch for changes in location. Second, we need to enable high accuracy, otherwise we get very approximate locations that are not good enough to tell us which way we are on.

let mut options = PositionOptions::new();

options.enable_high_accuracy(true); geolocation

.watch_position_with_error_callback_and_options(

geo_callback_function.as_ref().unchecked_ref(),

None,

&options,

)

.expect("Unable to get position.");

That’s it! We now have a useful app! It’s really satisfying to see how it updates with new way information as soon as I make a turn on to a new road or path, and also how swiftly it updates its map data when I move outside the triggering box.

Try it yourself, and let me know what you think! Preferably on a mobile phone or other device with Geolocation, but if you would like to try it in random walk mode instead, just add the query string parameter.

So what’s the next valuable feature to work on? That would have to be the option to add a note to your current location. We’ll look into that next.