maplibre-gl-js icon indicating copy to clipboard operation
maplibre-gl-js copied to clipboard

Strategy for tile-prefetching during camera-movement

Open JannikGM opened this issue 3 years ago • 37 comments

We often use flyTo, but maplibre doesn't seem to be smart enough to prefetch tiles along the animation path. The same is true for user mouse-movement (pan / zoom / rotate).

Ideally, the map would extrapolate where the map is going to render in the near future, so it can already load tiles. When combined with a small delay in the AnimationOptions this could be used to prefetch before the movement even starts.

@AbelVM had recently shown a "hack" on maplibre Slack, which prefetches neighbouring tiles in the browser-cache: https://github.com/AbelVM/mapworkbox Even this naive brute-force strategy (which doesn't respect camera movement direction) shows that better prefetching can have a performance impact.

JannikGM avatar Mar 23 '21 09:03 JannikGM

The target of the PoC was not to provide a final feature, but to test the feasibility and benchmark the WorkBox dynamic precaching vs vanilla one. That's why the tile logic is that simple, as it only takes into account pan & zoom prediction in a coarse way :shrug:

Regardless that whiny side note :rofl: , using dynamic precaching for flyTo, panTo, etc. makes total sense to me, as it lowers the source loading time a 35% (as per my benchmarks) it might lead to a way smoother animation and the logic to implement that is quite simple

AbelVM avatar Mar 23 '21 14:03 AbelVM

I have started working on an approach to add this feature to MapLibre. It is a bit of a mess because service workers and bundlers are still getting to know each other and I am a vanilla-JS guy. But that's just dev stuff, not the code itself. :nerd_face:

The starting idea is to attach a service worker that will remain asleep till the user uses any "moving" function where the destination is provided, so we can build all the logic (jumpTo, easeTo and flyTo). IMHO, it makes no sense to prefetch tiles for simple and short-ranged user interactions like pan, zoom or rotate as the common tiles fetching and caching might be enough, and the times from start to end of movement is not enough to get noticeable performance improvements, if any.

The very first version of the precaching is intended to hijack those moving functions and start precaching as soon as they are called. And which tiles are supposed to be precached (eventually)?

  • All the tiles that contain the center of the map during the animation
  • The 3 sibling tiles of those center tiles
  • All the tiles within the final viewport, once the camera has arrived to the destination

This way, the "focus area" of the map will be perfectly loaded and rendered during the animation, and the final scenario will be ready to welcome the user camera as it arrives.

Taking into account the pitch and bearing at every frame of the animation to estimate the tiles in the viewport would imply heavy calculations that might nullify the precaching advantage, so they are out of my scope in this first version

AbelVM avatar Aug 28 '21 21:08 AbelVM

I think this is a great idea. I know that our style is probably too complicated to be rendered fast, and it won't show up while animating the flyto, but it would be great to prioritize the final tiles at the destination. For us at least...

HarelM avatar Aug 29 '21 05:08 HarelM

First swerve: moving form precaching (service workers) to preloading (web workers).

We don't need to manage the cache life-cycle of the tiles to be used during and at the end of the camera movement, we just need to preload them fast enough for them to be locally available when requested by the camera logic.

This change:

  • Avoids the need for an external, physical, file for the service worker code (https://github.com/w3c/ServiceWorker/issues/578), keeping MapLibreGL JS library as a single file (plus CSS, you know)
  • Allows us to drop the workbox dependency
  • Simplifies the logic and the code

Regarding the note on prioritizing the tiles of the final scenario vs. the fly-by ones, it's an easy change. But, maybe, I'd just split the tiles list and send two different minions to preload them without interfering

AbelVM avatar Aug 29 '21 09:08 AbelVM

News!

  • Instead of messing with the library code, I've built a tiny plugin for this functionality. It adds cached versions of panTo, zoomTo, jumpTo, easeTo and flyTo. Just 7.7KB once bundled :nerd_face:
  • The logic might be implemented within MapLibre, but it might be somehow... dirty, IMHO.
  • Web workers fulfill our needs, no need to drive us crazy with service workers and the implicit complexity
  • This first version will only preload the final scenario, not the tiles for the animation. I've made some tests and trying to precache the animation might be a futile task as the animation itself might be faster than the preloading (of a potentially huge number of tiles) :unamused:
  • Performance:
    • The tiles are preloaded way before the movement has ended

gnome-shell-screenshot-YWZG80

  • All the final scenario tiles that fall within the viewport are hitting cache! MapLibre still requests some extra tiles out of the viewport to take into account minor pan movements by the user without new requests & repaint, so I might need to add some buffer to the final viewport logic.

gnome-shell-screenshot-OMQN80

  • There is still a minor bug with paths in Firefox, but it's just a matter of time I fix it.

AbelVM avatar Aug 29 '21 18:08 AbelVM

Nice work! I would be interested to know more about this. If there was a flag in maplibre to enable this it might be useful to others, in an opt-in mode...? I don't know. In any case, if you decide to send a PR please do it over the typescript branch in order to avoid conflicts...

HarelM avatar Aug 29 '21 18:08 HarelM

So, that's it :nerd_face: I have built a tiny experimental plugin for tiles preloading at

https://github.com/AbelVM/maplibre-preload

Please read the caveats and final thoughts as this is not a golden hammer (definitely not to be included in the main lib, IMHO)

AbelVM avatar Aug 30 '21 21:08 AbelVM

This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.

github-actions[bot] avatar Oct 30 '21 01:10 github-actions[bot]

Should this plugin be added to https://github.com/maplibre/maplibre-gl-js-docs/blob/main/docs/data/plugins.json and/or https://github.com/maplibre/awesome-maplibre before closing this issue?

xabbu42 avatar Oct 30 '21 10:10 xabbu42

@AbelVM please decide if and how you would like to publish the tiny plugin you wrote...

HarelM avatar Oct 30 '21 20:10 HarelM

There is a pending PR in the Awesome List, actually: https://github.com/maplibre/awesome-maplibre/pull/4

But adding it to the plugins list might give it more visibility and maybe * someone * helps turning it prod-quality and adding it to main branch :slightly_smiling_face:

AbelVM avatar Oct 31 '21 08:10 AbelVM

Upstream mapbox is getting this feature right now: https://github.com/mapbox/mapbox-gl-js/pull/11328

Some thoughts on their proposed implementation:

  • I don't like how they emulate 60FPS to find all touched tiles.
  • I do like that they have some preload function to preload specific tiles.
  • I strongly agree with mourners first comment, that prefetching and actually moving should be separated.

JannikGM avatar Dec 07 '21 16:12 JannikGM

I think the most valuable UX related to this feature is that the map is "there" when you arrive to the destination. It's nice that the while the flight is animating the map is also presented, but I see it as a lesser improvement to UX, at least from my point of view. I'm not sure how preloading and moving will work in terms of splitting them, feels odd to me - i.e. write something like: map.floytopreload(...) and then map.flyto(...) doesn't feel like a good API but I might be missing a use case - my use case is that the user select a search results and I'm using flyTo to move to that position so I don't want the user to wait before the movement starts. My 2 cents: if we can use a flag in the flyToOptions to signal the map to try and get the destination tiles first and only after that which ever tiles are missing it would be a great benefit.

HarelM avatar Dec 08 '21 09:12 HarelM

My tiny plugin already has that kind of pattern, you can call map.cachedFlyTo(...) to just preload the tiles or map.cachedFlyTo(..., {run:true}) to preload and trigger the flyTo method.

It would be easy to overload the original methods with (an improved version of) my code, I just tried to be the least invasive as possible in the plugin

AbelVM avatar Dec 10 '21 11:12 AbelVM

It's nice that the while the flight is animating the map is also presented, but I see it as a lesser improvement to UX, at least from my point of view. I'm not sure how preloading and moving will work in terms of splitting them, feels odd to me - i.e. write something like: map.floytopreload(...) and then map.flyto(...) doesn't feel like a good API but I might be missing a use case - my use case is that the user select a search results and I'm using flyTo to move to that position so I don't want the user to wait before the movement starts.

I think it will be very useful UX to precache the flight path, and not just the destination. If we were to animate from London to New York, it will be quite disorienting if we're unable to see much of the zoom-out, travel and zoom-in between these two destinations.

For reference, OpenLayers has a very neat implementation: https://openlayers.org/en/latest/examples/preload.html

ganesh-rao avatar Mar 30 '22 21:03 ganesh-rao

Hmmm... It looks like they preload lower (z-n) resolution tiles if the z tiles are not cached:

https://github.com/openlayers/openlayers/blob/10fb55b9e620946551195d2cf52f9d320f701c30/src/ol/renderer/webgl/TileLayer.js#L313

Worths a look :thinking:

AbelVM avatar Mar 31 '22 12:03 AbelVM

Hi, this plugin sounds like something I need. My app has few flyto points on the map, but when working on 4K display it is very laggy. However I can't make your tiny plugin to work with my setup. I set up my map with reactmap-gl and maplibre. How should I install maplibre-preload to work with reactmap-gl? I import the built package to index.js or App.js but I don't see cachedFlyTo neither under map nor map.getMap(). Can you help?

kajo-ops avatar Apr 27 '22 14:04 kajo-ops

Can you share a stackblitz or jsbin?

wipfli avatar Apr 27 '22 19:04 wipfli

Here is a stackblitz: https://stackblitz.com/edit/github-kut1lo?file=src/index.js

I simplified it for testing purpose. I tried importing bundled module and installing plugin as node_module with: npm i file:../maplibre-preload and then importing it to App.js

Neither works. How should I do it?

kajo-ops avatar Apr 27 '22 22:04 kajo-ops

Can we close this issue? I think the plugin is a good enough solution at this point...

HarelM avatar Aug 17 '22 12:08 HarelM

I think we should still have this feature natively in maplibre.

The plugin only primes the browser cache, however, this will not work if the cache is disabled or the server disabled caching (such as people who generate tiles dynamically). For these cases, it will double the server workload and the bandwidth to download tiles.

A native implementation would only have to fetch tiles once, and it could also start preprocessing tiles right away (creating necessary GL buffers etc.). This would remove a lot of the micro-stutter people observe when moving the camera.

The plugin is a nice hack for some use-cases, but prefetching of tiles should be a core feature of maplibre. I don't think maplibre necessarily has to handle finding which tiles will be required, but there should be functions to prefetch and evict tiles based on an area or tile-id, so the existing plugin could be improved.

JannikGM avatar Aug 31 '22 13:08 JannikGM

I totally agree with @JannikGM , we need to smooth the user experience of flyTo (mainly) actions

As previously mentioned, the strategy used by OpenLayers worths a look

  • https://openlayers.org/en/latest/examples/preload.html
  • https://github.com/openlayers/openlayers/blob/10fb55b9e620946551195d2cf52f9d320f701c30/src/ol/renderer/webgl/TileLayer.js#L313

In order to save bandwidth and load times, looks like they preload tiles at lower zoom levels for the target and crossing area and over-zoom them to the current map zoom, and once arrived, gracefully swapping them when final tiles of the target area are loaded.

AbelVM avatar Sep 01 '22 06:09 AbelVM

Fair enough. I would recommend pitching a design for this to facilitate for the "main" use cases as the default behavior, and allow for more configuration for edge and uncommon use cases. For example I would consider only pre-fetching the target tiles as default and allow the developer a way to configure the per-fetch in a different way... I've seen that pre-fetching becomes more important when we are talking about the terrain due to camera movement. Cc @prozessor13

HarelM avatar Sep 01 '22 09:09 HarelM

Hey folks I wanted to share what the experience looks like right now on a good internet connection for the example where we fly to specific locations based on the scroll position here

https://maplibre.org/maplibre-gl-js/docs/examples/scroll-fly-to/

Screenshare - 2023-10-05 12 17 15 PM.webm

For these map story-telling use cases (even without terrain or pitched camera angles) maplibre-gl-js struggles to deliver a good user experience out of the box.

daniel-j-h avatar Oct 05 '23 12:10 daniel-j-h

Regardless of your bandwidth, concurrent connections to the same server are limited to 6 in your browser, so, if you pan/zoom too fast, you're just sending and canceling lots of requests on the fly, as the tiles are still downloading when they are tagged as not needed (out of the viewport) and their requests canceled.

So, yep, this is a big issue that we should look into imho.

AbelVM avatar Oct 05 '23 15:10 AbelVM

How do I set this pluginup in my typscript react app where I have my map initalized in another class?

aliidurraniii avatar Nov 03 '23 20:11 aliidurraniii

're just sending and canceling lots of requests on the fly, as the ti

Only on http 1.X I guess....

hheexx avatar Nov 05 '23 12:11 hheexx

Has there been any progress on this issue? I also don't get the plugin to work properly in my environment - only "Movement has finished before preloading" gets triggered.

rsalzer avatar Mar 27 '24 16:03 rsalzer

This project is just a PoC, quite naive. If there's a real interest on this feature, we should get serious, study the OpenLayers strategy (as it looks promising), and push this feature to MapLibre itself.

Any opinion @HarelM ?

AbelVM avatar Apr 02 '24 08:04 AbelVM

There are strong forces to keep the bundle size small and so if this is possible using a plugin without a lot of "hacking" I think it can be a good solution. @AbelVM approach is naive, but anyone with the interest of improving this can add a PR to the specified repo or create a different plugin. Given the above options and the community's engagement, I reluctant to say that there's a real interest here. Having said that, if the code changes are small and the value is high (without tons of configurations) I think we can entertain the idea of adding this to this library. There are my 2 cents at least.

HarelM avatar Apr 02 '24 08:04 HarelM