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

Improve handling of bbox state #77

Merged
merged 17 commits into from
Jun 7, 2021
Merged

Improve handling of bbox state #77

merged 17 commits into from
Jun 7, 2021

Conversation

easbar
Copy link
Member

@easbar easbar commented May 19, 2021

Handling the bbox state (or more generally the view port state) is a bit tricky, because we partly control it from our application (using /info, selectedPath.bbox etc.) and partly it is maintained by direct user interactions with the map (zooming, scrolling etc.).

In some cases we just want to set the bounds of the map (fitBounds), for example when we want to zoom to a calculated route. This means we want to control the viewport ourselves. In other cases we want to keep the current viewport, for example when we change the map layer or clear a query input box. This means we want to neglect the bbox state of our application and the internal map state should take precedence.

Here I refactored some of the bbox state handling and fixed #72. Unfortunately with my current solution the map no longer zooms to the calculated route when the page is loaded initially. See more comments inline. (<- this is fixed now)

Fixes: #72

src/App.tsx Outdated
Comment on lines 38 to 47
const [bbox, setBbox] = useState<Bbox>([-180, -90, 180, 90])
useEffect(() => {
// make sure the path bbox takes precedence over the info bbox
if (!route.selectedPath.bbox)
setBbox(info.bbox)
}, [info])
useEffect(() => {
if (route.selectedPath.bbox)
setBbox(route.selectedPath.bbox)
}, [route])
Copy link
Member Author

@easbar easbar May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using these two effects seems better than the previous code, because now we only update the bbox when the route or info changed, whereas previously we always had to determine some bbox even when we did not want to change anything. This way we also no longer need the shouldUseInfoBbox flag.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is much better. Should we maybe combine those into a single effect though? Since you are using route in both effects maybe switch to something like

useEffect(() => setBbox(route.selectedPath.bbox ? route.selectedPath.bbox : info.bbox), [route, info])

I guess this is only a matter of taste though

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes this looks short and clean. But are you sure it does the same? Considering there were quite a few edge-cases (for which we have no tests currently :() and I was sure this version works and we might change this again in #87 I think I would rather leave it as is for this PR. Not the best reasons, I know...

src/map/Map.tsx Outdated
Comment on lines 36 to 37
if (prevViewPort)
mapWrapper.setViewPort(prevViewPort)
Copy link
Member Author

@easbar easbar May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code is what was missing for #72. When we re-create the map instance we have to restore the previous viewport. However, just doing this is not enough, because without the changes in App.tsx there would be another call to fitBounds that moves the map to the info or route bbox again.

src/map/Map.tsx Outdated
Comment on lines 39 to 43
useEffect(() => map?.fitBounds(bbox), [bbox, map])
useEffect(() => map?.fitBounds(bbox), [bbox])
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the most tricky part to me. Keeping the map dependency means that after a style change the call to setMap will trigger fitBounds again. If we calculate a route, scroll somewhere else and then change the style this means the map will center at the calculated route again.

On the other hand if we do not keep the map dependency map will be undefined here when the page loads initially and the call to fitBounds won't happen. Not sure how to fix this tbh.

Copy link
Member Author

@easbar easbar May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update: With my last commit it is 'working' now: 2fadb17, but honestly this seems way too complicated. I now use useRef to keep track of the latest bbox value so it is available by the time onMapReady is called and we can call fitBounds(prevBbox.current). Is there maybe a better way to construct the map instance before we receive the bbox etc.?

Copy link
Member Author

@easbar easbar May 20, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update2: I think I found a better solution here: 933e958, 3ba7c79 and now I think the PR is ready to merge.

@easbar easbar added this to the 0.3 milestone May 20, 2021
@easbar
Copy link
Member Author

easbar commented May 20, 2021

Btw not sure if we already need this, but the Mapbox documentation also describes how to keep the lat/lng/zoom state of the map in sync with the application state: https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/#store-the-new-coordinates

The code is here: https://github.com/mapbox/mapbox-react-examples/tree/master/basic

@karussell
Copy link
Member

karussell commented May 20, 2021

Thanks for investigating this, this works for me. I think this also supersedes #71?

And is it easy to reduce the world bbox to avoid seeing some continents twice?

Also this is related to #53.

@easbar
Copy link
Member Author

easbar commented May 20, 2021

I think this also supersedes #71?

Yes, I think so.

And is it easy to reduce the world bbox to avoid seeing some continents twice?

I think we can do this in Mapbox.ts:

 this.map = new Map({
            // ...
            maxBounds: [[-180, -90], [180, 90]],
            renderWorldCopies: false
        })

See also here:
https://docs.mapbox.com/mapbox-gl-js/example/restrict-bounds/
https://docs.mapbox.com/mapbox-gl-js/example/render-world-copies/

Also this is related to #53.

Not sure. This PR is mostly about how the bbox is applied to the map, but #53 seems also about determining the right bounding box for a route (use appropriate padding, do not zoom in too much etc.)

@easbar
Copy link
Member Author

easbar commented May 20, 2021

I just disabled the world copies here: b29801b, but feel free to adjust

@easbar
Copy link
Member Author

easbar commented May 20, 2021

I briefly tried avoiding the initial jump when points are specified (also not fixed in #71), but let's do this in a separate PR. I opened #82 for this.

@easbar
Copy link
Member Author

easbar commented May 20, 2021

I also wonder what it would take to setup tests for things like "map does not go back to info box when we clear query point box after route was calculated" etc.

# Conflicts:
#	src/App.tsx
#	src/map/Map.tsx
#	src/map/Mapbox.ts
# Conflicts:
#	src/App.tsx
#	src/api/Api.ts
#	src/stores/QueryStore.ts
@easbar easbar mentioned this pull request May 25, 2021
6 tasks
Copy link
Collaborator

@Janekdererste Janekdererste left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good overall.

src/App.tsx Outdated
Comment on lines 38 to 47
const [bbox, setBbox] = useState<Bbox>([-180, -90, 180, 90])
useEffect(() => {
// make sure the path bbox takes precedence over the info bbox
if (!route.selectedPath.bbox)
setBbox(info.bbox)
}, [info])
useEffect(() => {
if (route.selectedPath.bbox)
setBbox(route.selectedPath.bbox)
}, [route])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is much better. Should we maybe combine those into a single effect though? Since you are using route in both effects maybe switch to something like

useEffect(() => setBbox(route.selectedPath.bbox ? route.selectedPath.bbox : info.bbox), [route, info])

I guess this is only a matter of taste though

<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="layer-group"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this change on purpose?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah no sorry, this was just auto-formatting, I reverted this change.

@@ -19,22 +19,42 @@ type MapProps = {
export default function({ selectedPath, paths, queryPoints, bbox, mapStyle }: MapProps) {
const mapContainerRef: React.RefObject<HTMLDivElement> = useRef(null)
const [map, setMap] = useState<Mapbox | null>(null)
const prevViewPort = useRef<ViewPort | null>(null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the functionality to remeber the viewport when the map instance is replaced can be achieved with a functional state update https://reactjs.org/docs/hooks-reference.html#functional-updates

Here we would get the previous map instance as parameter to useState for the map something like this maybe?

const mapWrapper = new Mapbox(
            mapContainerRef.current!,
            mapStyle,
            () => {
                setMap(prevWrapper => {
                    mapWrapper.setViewPort(prevWrapper.getViewPort())
                    return mapWrapper
                })
                Dispatcher.dispatch(new MapIsLoaded())
            }
        )

On the other hand this might be to late and cause fickering of the map again, because the viewport would be set after the map is loaded.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the code example is for line 35, but it also concerns the useRef

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok interesting. I followed the pattern to use the previous state here: https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state. If you don't mind also here I would leave as is for now and either change this in a follow-up commit/PR or maybe we do not need to replace the map instance anyway using the react wrapper #87.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, for the late answer. I totally agree especially, since we want to replace the map layer anyway.

@easbar easbar merged commit 856b767 into master Jun 7, 2021
@easbar easbar deleted the bbox_state branch June 7, 2021 08:30
ZeroGxMax pushed a commit to minhhpkp/graphhopper-maps that referenced this pull request Nov 12, 2024
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

Successfully merging this pull request may close these issues.

change map layer should not cause map bbox change
3 participants