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

Strategy for tile-prefetching during camera-movement #116

Open
JannikGM opened this issue Mar 23, 2021 · 34 comments
Open

Strategy for tile-prefetching during camera-movement #116

JannikGM opened this issue Mar 23, 2021 · 34 comments
Labels
enhancement New feature or request PR is more than welcomed Extra attention is needed

Comments

@JannikGM
Copy link
Contributor

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.

@AbelVM
Copy link

AbelVM commented Mar 23, 2021

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 🤷

Regardless that whiny side note 🤣 , 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
Copy link

AbelVM commented Aug 28, 2021

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. 🤓

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

@HarelM
Copy link
Member

HarelM commented Aug 29, 2021

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...

@AbelVM
Copy link

AbelVM commented Aug 29, 2021

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:

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
Copy link

AbelVM commented Aug 29, 2021

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 🤓
  • 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) 😒
  • 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.

@HarelM
Copy link
Member

HarelM commented Aug 29, 2021

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...

@AbelVM
Copy link

AbelVM commented Aug 30, 2021

So, that's it 🤓 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)

@github-actions
Copy link
Contributor

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 github-actions bot added the Stale label Oct 30, 2021
@xabbu42
Copy link
Contributor

xabbu42 commented Oct 30, 2021

@HarelM
Copy link
Member

HarelM commented Oct 30, 2021

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

@github-actions github-actions bot removed the Stale label Oct 31, 2021
@AbelVM
Copy link

AbelVM commented Oct 31, 2021

There is a pending PR in the Awesome List, actually: maplibre/awesome-maplibre#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 🙂

@JannikGM
Copy link
Contributor Author

JannikGM commented Dec 7, 2021

Upstream mapbox is getting this feature right now: mapbox/mapbox-gl-js#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.

@HarelM
Copy link
Member

HarelM commented Dec 8, 2021

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.

@AbelVM
Copy link

AbelVM commented Dec 10, 2021

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

@ganesh-rao
Copy link

ganesh-rao commented Mar 30, 2022

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

@AbelVM
Copy link

AbelVM commented Mar 31, 2022

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 🤔

@kajo-ops
Copy link

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?

@wipfli
Copy link
Member

wipfli commented Apr 27, 2022

Can you share a stackblitz or jsbin?

@kajo-ops
Copy link

kajo-ops commented Apr 27, 2022

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?

@HarelM
Copy link
Member

HarelM commented Aug 17, 2022

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

@HarelM HarelM added the need more info Further information is requested label Aug 17, 2022
@JannikGM
Copy link
Contributor Author

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.

@AbelVM
Copy link

AbelVM commented Sep 1, 2022

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

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.

@HarelM
Copy link
Member

HarelM commented Sep 1, 2022

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 HarelM added enhancement New feature or request PR is more than welcomed Extra attention is needed and removed need more info Further information is requested labels Sep 1, 2022
@daniel-j-h
Copy link

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.

@AbelVM
Copy link

AbelVM commented Oct 5, 2023

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.

@aliidurraniii
Copy link

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

@hheexx
Copy link

hheexx commented Nov 5, 2023

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

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

@rsalzer
Copy link

rsalzer commented Mar 27, 2024

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.

@AbelVM
Copy link

AbelVM commented Apr 2, 2024

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 ?

@HarelM
Copy link
Member

HarelM commented Apr 2, 2024

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.

@rsalzer
Copy link

rsalzer commented Apr 2, 2024

For my usecase (Storymaps) I need this feature, otherwise flyTo is completly useless. As already mentioned the example https://maplibre.org/maplibre-gl-js/docs/examples/scroll-fly-to/ does not really work smoothly without precaching.

My workaround will be to preload the tiles programmatically when the site loads as I have an already specified "flight-path".
If I read @AbelVM's code correctly, it is enough to just fetch the URLs of the tiles so the browser has them cached and not pass them already to maplibre.

@ganesh-rao
Copy link

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.

FWIW, IMO, this should be considered integral to MapLibre. One could even say that the flyTo feature right now is half implemented since it doesn't really work for most users. The cache behaviour is crucial to the UX.

But I do take your point on the bundle size.

@HarelM
Copy link
Member

HarelM commented Apr 2, 2024

Looking at their code will infringe their copyright rules. I would advise against it.

@AbelVM
Copy link

AbelVM commented Apr 2, 2024

Instead of Mapbox, check OpenLayers approach:

openlayers/openlayers@6f005e1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request PR is more than welcomed Extra attention is needed
Projects
None yet
Development

No branches or pull requests