Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature request] Add handler for source and layer loaded events #18

Open
markusand opened this issue Jan 28, 2021 · 9 comments
Open

[Feature request] Add handler for source and layer loaded events #18

markusand opened this issue Jan 28, 2021 · 9 comments

Comments

@markusand
Copy link

This utility library is really handy. One valuable utility that would solve many people's headaches would be to have a reliable way to detect when a source or layer has been loaded properly.

map.U.onSourceLoad('sourcename', features => {
    // Do whatever with features
});

map.U.onLayerLoad('layername', () => {

});
@stevage
Copy link
Owner

stevage commented Feb 4, 2021

Ah, interesting suggestions. What does "load" mean for you in each of these contexts, and what are your use cases exactly?

@markusand
Copy link
Author

markusand commented Feb 4, 2021

I guess a loaded source means when all features have been set as data and are available, and a loaded layer would mean when has already been drawn on the map.

There are many events in mapbox such as sourcedata or style.load that may be used together to verify the resource status, but all implementations I've been using end up being a little bit buggy.

This is the most recent I'm using to detect source changes, still not giving 100% good results

const sourceId = 'source_id';
const sourceLoaded = source => {
	console.log('Source loaded', source);
};

const onLoadStart = startEvent => {
	if (startEvent.sourceId === sourceId) {
		map.off('dataloading', onLoadStart);
		const onLoadEnd = () => {
			if (endEvent.sourceId === sourceId && endEvent.isSourceLoaded && map.isSourceLoaded(sourceId)) {
				map.on('dataloading', onLoadStart);
				map.off('sourcedata', onLoadEnd);
				sourceLoaded(map.getSource(sourceId));
			}
		};
		map.on('sourcedata', onLoadEnd);
	}
};

map.on('dataloading', onLoadStart);

Use cases are multiple, in my case I'm using to detect when a vector tile source has finished to (re)load after a pan or zoom change.

I'm also using styledata and style layers checks to trigger an event when some layers change in a legend plugin I'm working on, again with some headaches :P

@stevage
Copy link
Owner

stevage commented Feb 4, 2021

Yeah, the inability to consistently detect these events is such a huge pain point :/

If you can come up with an implementation you're satisfied with, I'd be happy to include it.

@markusand
Copy link
Author

I'm making some VERY OPINIONATED assumptions and I haven't tested widely so I'm not sure about possible side effects, but seems to work pretty well on my use case and it's clearly a lot more readable that my previous suggestion.

const onSourceLoad = (name, { onLoadStart, onLoadEnd }) => {
    let isSourceLoading = false;
    map.on('sourcedata', event => {
        if (event.sourceId !== name) return;

        // Source starts loading
	// Consider an "initial" data event when sourceDataType is undefined or `content`
	// Discard `visibility` as it triggers very irregularly
	const { sourceDataType = 'content' } = event;
	if (!isSourceLoading && sourceDataType === 'content') {
	    if (onStartLoading) onStartLoading(map.getSource(name));
	    isSourceLoading = true;
	}

        // Source finishes loading
	if (isSourceLoading && map.isSourceLoaded(name)) {
	    if (onEndLoading) onEndLoading(map.getSource(name));
	    isSourceLoading = false;
	}
    });
};

I've changed a little bit the approach to trigger different actions when layer starts and finishes loading, so it could be used as:

map.U.onSourceLoad('sourcename', {
    onLoadStart: source => { /* Do whatever */ },
    onLoadEnd: source => { /* Do whatever */ },
});

I'm currently using this method to show and hide a loading spinner whenever a vt source is updating.

@stevage
Copy link
Owner

stevage commented Apr 8, 2021

Thanks for this - looks useful. I will have to look more closely. I'm a little bit iffy about the function signature (having to spell out onLoadStart and onLoadEnd ...). Maybe it would be cleaner/simpler to just have two separate functions: map.U.onSourceLoadStart(name, cb) and map.U.onSourceLoadEnd(name, cb).

Does this work on both vector tiles and GeoJSON? Does it trigger per tile, or after all the tiles for the current view have loaded?

@markusand
Copy link
Author

Object signature is convenient in my particular usecase, but certainly adds some bloating.

Works both on vt and geojson. Some considerations:

  • Triggers on geojson when zooming and panning, like if load was batched internally. I may have misunderstood how geojson works, but I thought it was loaded at once.
  • Triggers on vector tiles once all tiles have finished loading. Sometimes keeps waiting if a tile is partially offscreen and not fully rendered.

I wish Mapbox will release some day a good source and layers events API, because all this starts to feel very hacky to me 😓

@stevage
Copy link
Owner

stevage commented Apr 11, 2021

Triggers on geojson when zooming and panning, like if load was batched internally. I may have misunderstood how geojson works, but I thought it was loaded at once.

GeoJSON is loaded in bulk but then converted into vector tiles by mapbox-gl-js.

I have now tested this and it does seem useful.

@markusand
Copy link
Author

I found an edge case. Functions where triggered by sourcedata on any source/layer change, including setting a simple filter in a layer. I solved it by splitting into two functions and binding the onStartLoading part to the sourcedataloading event. This seems to work smoother only when actual source data is being loaded.

let isLoading = false;
const bindLoadStart = event => {
    const { sourceDataType = 'content' } = event; // Little hack here
    if (event.sourceId === sourceId && !isLoading && sourceDataType === 'content') {
	if (onStartLoading) onStartLoading(event);
	isLoading = true;
    }
};

const bindLoadEnd = event => {
    if (event.sourceId === sourceId && isLoading && map.isSourceLoaded(sourceId)) {
	if (onEndLoading) onEndLoading(event);
	isLoading = false;
    }
};

const bindLoading = () => {
    map.on('sourcedataloading', bindLoadStart);
    map.on('sourcedata', bindLoadEnd);
};

const unbindLoading = () => {
    map.off('sourcedataloading', bindLoadStart);
    map.off('sourcedata', bindLoadEnd);
};

Sorry if I'm spamming too much on this topic. Just wanting to let you know.

@stevage
Copy link
Owner

stevage commented Apr 14, 2021

Thanks, this is great.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants