Reputation: 6998
I'm creating an MapboxGL map that needs to support lots of interactivity via buttons and sliders.
MapboxGL map instances are stateful and have temperamental lifecycles due to lots of async fetching of tiles and sources. This means most of the work is done inside event handlers. This makes me think that RxJS is a solid candidate for managing all of this interactivity.
For example, if you want to add a feature layer to a map, the mapbox example suggest doing everything in a map.on('load', handler)
. That's fine for static maps, but it's a hassle to coordinate imperative event handlers for a map that will have 20+ toggles/sliders/controls.
What I want to do is create sequences for each individual operation. The most basic of which is loading the map then adding the base feature layer after the map loads.
So here's what I've got, I've been using RxJS for about 5 hours total, so maybe there is something I just don't know that I don't know.
Snippet:
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/dark-v9',
center: [-122.78, 38.43],
zoom: 10
});
// Check every 100ms if the map is loaded, if so emit
var loaded$ = Rx.Observable
.interval(100)
.map(() => map.loaded())
.filter(x => x)
.first();
// We must wait until the map is loaded before attaching the feature layer.
var addLayer = loaded$.subscribe(
baseMapLoaded, errorHandler, addFeatureLayerAndSource
);
// Attaching the feature layer sets the map.loaded() to false
// while it fetches the tiles.
//
// When the feature layer and source are loaded,
// log 'all tiles loaded' then 'completed' to the console
//
// Why can I declare `addLayer` and `completed` like this?
// Shouldn't these both fire after the loaded$ emits the first time,
// causing an error?
var completed = loaded$.subscribe(tilesLoaded, errorHandler, completeHandler);
/* Misc Handlers */
function addFeatureLayerAndSource() {
console.log(2, 'adding feature layer and source');
map.addSource('parcel_data', {
'type': 'vector',
'tiles': ['http://localhost:5000/api/v1/tiles/{z}/{x}/{y}']
});
map.addLayer({
"id": "parcel_data",
"type": "fill",
"source": "parcel_data",
"source-layer": "parcel_data",
});
}
function baseMapLoaded() { console.log(1, 'base map loaded'); }
function tilesLoaded() { console.log(3, 'all tiles loaded'); }
function errorHandler(err) { console.log('error handling', err); }
function completeHandler() { console.log('completed'); }
EDIT: Updated the snippet to reflect @CalvinBeldin's help. Currently trying to figure out why this actually works. The order of my console.logs is correct:
1 "base map loaded"
2 "adding feature layer and source"
3 "all tiles loaded"
completed
Upvotes: 2
Views: 640
Reputation: 438
I might not completely understood what is the problem. The main points:
loadSource$ = Observable.fromEvent(map, 'load').first();
// You need load$ to be a BehaviorSubject or replay last value on
// In order to keep state of wheter laod event happend or not
// For observers which subscribed after load event already happened
load$ = loadSource.replay(1);
render$ = Observable.fromEvent(map, 'render');
renderAfterLoad$ = load$.flatMap(() => render$);
load$.subscribe(() => {
map.addSource('source_data', {
'type': 'vector',
'tiles': ['http://localhost:5000/api/v1/tiles/{z}/{x}/{y}']
});
map.addLayer({
"id": "layer_data",
"type": "fill",
"source": "source_data",
"source-layer": "source_layer_id",
});
});
render$.subscribe(() => {
console.log('all tiles loaded');
});
Upvotes: 0
Reputation: 3114
The Observable returned by renderObservable.last()
is never going to fire because renderObservable
's complete event never occurs (renderObservable
will just keep listening for render
events until it's disposed, so in this sense it never "completes").
You'll need to create a new Observable that emits when the last render occurs. Per limitations with the MapboxGL API, a polling solution might be the only way to go:
// Emits the value of map.loaded() every 100ms; you can update this
// time interval to better suit your needs, or get super fancy
// and implement a different polling strategy.
const loaded$ = Rx.Observable.interval(100).map(() => map.loaded());
// Will emit once map.loaded() is true.
const isLoaded$ = loaded$.filter(x => x).first();
Upvotes: 3