-
Notifications
You must be signed in to change notification settings - Fork 34
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
Conversation
src/App.tsx
Outdated
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]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
if (prevViewPort) | ||
mapWrapper.setViewPort(prevViewPort) |
There was a problem hiding this comment.
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
useEffect(() => map?.fitBounds(bbox), [bbox, map]) | ||
useEffect(() => map?.fitBounds(bbox), [bbox]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Btw not sure if we already need this, but the Mapbox documentation also describes how to keep the The code is here: https://github.com/mapbox/mapbox-react-examples/tree/master/basic |
Yes, I think so.
I think we can do this in this.map = new Map({
// ...
maxBounds: [[-180, -90], [180, 90]],
renderWorldCopies: false
}) See also here:
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.) |
I just disabled the world copies here: b29801b, but feel free to adjust |
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
There was a problem hiding this 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
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]) |
There was a problem hiding this comment.
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
src/map/layer-group-solid.svg
Outdated
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="layer-group" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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