deck.gl icon indicating copy to clipboard operation
deck.gl copied to clipboard

Viewport not aligned with Mapbox base map when view state is updated

Open srtena opened this issue 4 years ago • 8 comments

Description

Sometimes when first adding a layer it's completely out of sync to the basemap until I adjust the zoom or move the map. The layer used is a GeoJsonLayer but I've seen this happen too with the MVTLayer

Screenshot 2021-09-17 at 15 52 17

Expected Behavior

The layer should start in the right place, as after the first interaction.

Screenshot 2021-09-17 at 15 55 13

Repro Steps

[WIP]

Environment

  • Framework Version: deck.gl 8.5.6
  • Browser Version: Chrome 93
  • OS: Mac OS X 11.5.1

srtena avatar Sep 17 '21 13:09 srtena

I've also seen this bug before with MVTLayer too. I have a 2K external monitor and the bug appears there quite often, not very frequent on the laptop screen.

@felixpalmer would be great if you can take a look

alasarr avatar Sep 17 '21 14:09 alasarr

This looks like an issue fixed by https://github.com/visgl/deck.gl/pull/5727 in 8.5.0.

Which flavor are you using, is it React + managed view state?

Pessimistress avatar Sep 17 '21 15:09 Pessimistress

@srtena would it be possible for you to post a code example or codepen?

felixpalmer avatar Sep 21 '21 09:09 felixpalmer

Hi @felixpalmer. We're experimenting this within our carto for react solution, where we use React & mapbox, and we deal with viewstate via redux. We'll run some checks to confirm where the issue manifests

VictorVelarde avatar Sep 21 '21 16:09 VictorVelarde

Here is a minimal example of managed view state: https://codesandbox.io/s/deck-gl-managed-view-state-yfh9p?file=/src/App.js

If you comment out the line setViewState(e.viewState); you can see that the maps become misaligned. Mapbox normalizes the viewport to avoid white space in the viewport. On the first render, deck.gl performs the same normalization and calls onViewStateChange with the view state that would match the base map. If your application fails to update its state at this point, the deck.gl layer will not align with the base map.

Pessimistress avatar Sep 21 '21 19:09 Pessimistress

I can confirm that the problem indeed stems from the fact that the deck.gl viewState has a zoom value that is lower than what Mapbox has normalized it to. In the example @srtena reported they are respecting the normalized viewState that is sent back on startup.

However the bug occurs if the viewState is later updated to include a zoom that is too small. @Pessimistress if you add this to your codesandbox example you can repro the issue:

React.useEffect(() => {
  setTimeout(() => {
    if (viewState.zoom === 0) {
      setViewState({...viewState, zoom: 0.01});
    }
  }, 5000);
});

It appears to be the case that the onViewStateChange is not called in this case. Is that expected?

felixpalmer avatar Sep 23 '21 15:09 felixpalmer

You are right, we don't normalize the incoming viewState outside of the initialization sequence.

I don't want to blindly try normalize all the time, because the vast majority of view state updates are piped back from onViewStateChange, whose arguments are already normalized. Normalization is not a super expensive operation in itself, so if we have to it's probably fine, it just feels excessive.

Do you have any suggestions?

Pessimistress avatar Sep 23 '21 23:09 Pessimistress

We get the mismatch when the map height is greater than the tile size (of 512) - assuming an unrotated&untilted map. Specifically, the zoom gets capped like so:

zoom = Math.max(zoom, Math.log2(viewState.height / 512))

This explains why the issue is more often seen on a large screen, as @alasarr reported. But the equation above only holds when the map isn't tilted or rotated, otherwise the full normalizeViewportProps from math.gl/web-mercartor is required.

Perhaps we could try to limit the normalization somehow, using some cheap heuristic that would only invoke the full normalization in for low zoom levels? E.g.

if (zoom < Math.log2(viewState.height / 512) - 3)

felixpalmer avatar Sep 24 '21 11:09 felixpalmer

I ran into this problem implementing a simple functionality for a button that resets a view to the initial state.

The initial state in my case is based on a bounding box of a feature (say, a country). If the feature is very large (such as Russia in the example below), the lat/lon/zoom calculated with WebMercatorViewport.fitBounds would cause blank space at the top of the map so it gets normalized by Deck during initialization. But not later when the button is used.

See Codesandbox demonstrating two cases where sanitizing viewState on every update would be useful / expected:

  • the "Reset to initial view" button
  • a simple arrows control for panning the map

It's quite easy to ensure in such simple controls that each component of the modified view state is valid in general (e.g. -90 < latitude < 90) but it's more complex to ensure there will be no shift between the react-map-gl and deck.gl layers.

mz8i avatar Mar 12 '23 19:03 mz8i