How to access arcgis map via components? - reactjs

The code below is taken directly from arcgis via react on how to display a map.
If i wanted to say, zoom in to a set of coordinates, but the code for that was set in another component, how can i get that component to talk to the map here in this component?
import Map from '#arcgis/core/Map';
import MapView from '#arcgis/core/views/MapView';
const map = new Map({
basemap: "topo-vector"
});
const view = new MapView({
container: "viewDiv",
map: map
});

I resolved this by using redux toolkit to set the map as a global state object.
The entire map view is set in a useEffect, once i initialize each of the views, i dispatch the map view to a reducer in rtk.
dispatch(updateMapViewState(view));
updateMapViewState: (state, action) => {
state.view = action.payload
},
Then, when i want to use the map in a separate component, i do:
const view = useSelector((state) => state.MapSlice.view);
In this way, all components can access the map outside of the useEffect in the map component, and can manipulate it without creating a new map view. This worked for me. I assume you could probably do this with context api, but we aren't using that as a global state manager.

This may not be the recommended or best way to achieve this, but I had success by passing the view object from the map component back to the parent component, then saving it to the parent component's state.
// in App.js
saveViewToState(view){
this.setState({view: view})
}
<MapComponent saveViewToState={this.saveViewToState}/>
// in MapComponent.js
let view = new View()
this.props.saveViewToState(view)
Then I was able to interact with the view object from the parent:
// in App.js
this.state.view.extent = {xmin: 1, ymin: 1, xmax: 2, ymax: 2}
This doesn't work perfectly (for some reason I can't call view.goTo, but view.extent works). I'd be keen to hear if there is a better way to achieve this.

Still looking for a clear answer to this problem.
Have not found an example separating the map/view creation and adding layers into differing components.

Related

Open Layer Renders Map component twice in react

I'm using Open Layer to render map functionality in a react application. I used the simple code snippets to display the map in the application. Unfortunately the application renders the map twice as below.
Here is the code snippet I used to display the map:
export default function MapView() {
const view = new View({
center: [0, 0],
zoom: 2,
});
useEffect(() => {
new Map({
layers: [
new TileLayer({source: new OSM()}),
],
view: view,
target:'map',
})
},[])
return (
<>
<div id="map" style={{width:'100%',height:'400px'}}></div>
</>
)
}
And the parent component is as below
function App() {
return (
<div>
<MapView />
</div>
);
}
Your component may be mounted multiple times; useEffect with an empty deps array runs each time the component is mounted, but instantiating View/Map appears to mutate the DOM directly and inject the map. If you look at my example, the message "I'm mounting!" is printed to the console twice. Because useEffect is running once per mount, you're instantiating multiple Map objects and attaching them to the element with id map. Instead, you should consider using a ref to hold your Map reference so that only one map object is instantiated per instance of your component.
Additionally, rather than referencing an ID by string in your map mounting, you should instead use a ref for the component's rendered node, instead, and pass that reference to target (which does appear to accept an HTMLElement node directly).
See this sandbox link for an example.
This ref handle also gets you a persistent reference to the map object, which you can then use to perform imperative operations on the map in response to prop changes (see my example zoom-sensitive useEffect).

Avoid re-rendering of open layers map when context changes

I am using an Open Layers map inside React. I am attaching it to the DOM using setTarget like so:
const mapObject = useMap(maplayers);
useEffect(() => {
mapObject.setTarget(mapRef.current);
return () => mapObject && mapObject.setTarget(undefined);
},[mapObject]);
Inside the useMap.js hook I am writing changes to the center and zoom of the map back to the context:
const {
zoom,
setZoom,
center,
setCenter,
} = useContext(MapContext);
let mapObject = null;
var projection = new olProj.Projection({
code: 'EPSG:25832',
units: 'm',
});
let options = {
target: 'map',
controls: olControl.defaults({
rotate: false,
zoom: false,
attributionOptions: {
collapsible: false,
},
}),
layers: maplayers,
view: new ol.View({
projection: projection,
center: center,
zoom: zoom
}),
};
mapObject = new ol.Map(options);
mapObject.on('moveend', (e) => {
console.log('moveend');
setCenter(() => e.target.getView().getCenter());
setZoom(() => e.target.getView().getZoom());
});
The problem now is that the whole map gets rerendered every time the center and zoom change. How can I avoid this? The only idea I have is to use useEffect with a dependency array that misses center and zoom but I know this is bad practise and should not be done.
You just discovered why one should not use useEffect to create Openlayers components in React. I answer this question once per week here - I will probably end up writing a very long and detailed answer and then start marking those questions as duplicates. Or maybe write a tutorial for Openlayers + React.
What you are trying to achieve is not easy and no one got it right the first time. I am the author of rlayers - https://github.com/mmomtchev/rlayers - a set of components for using Openlayers in React. The only reliable way to do everything that React expects is to use class-based components and to completely rewrite the React component life-cycle. You are free to use my components - either directly, either as an inspiration for writing your own.
In your particular case, the problem is that you create a new mapObject every time the hook is called. React detects this change and decides that the component must be rerendered. Built-in React hooks work around this by memoizing the value. Your hook should declare the dependencies of the mapObject and then use React.useMemo to always return the same object:
const mapObject = React.useMemo(() => {
// code to generate the mapObject
return mapObject;
}, [ projection, url, /* and all other items that require rerender */ ]
);
This way your hook will always return the same mapObject and you will continue to use a useEffect with only the mapObject as a dependency - according to the React guidelines.

Fit map to feature layer bounds in react-leaflet

What I want to achieve:
Have a <Map><FeatureGroup><Circle />[1 or more]...</FeatureGroup></Map> hierarchy and fit the map bounds to the feature group so that all the circles are in the viewport.
If there is only one circle, it should fit the bounds (ie: zoom in on) to that circle.
What I've tried:
giving FeatureGroup a ref and calling getBounds on it to pass onto Map. Because of the lifecycle FeatureGroup doesn't exist at the time componentDidMount is called - it gets rendered later (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328).
Storing Circle in state and calling getBounds on that (assuming, in this case, that there is only one circle. That didn't work either.
I think I might need to do something with the React Context but I'm not sure that I fully understand it right now, so I need some help.
Other information
I'm using react-leaflet#2.1.2
Thanks for any help offered!
Because the contents of the Map are unavailable at componentDidMount-time (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328) you cannot get the bounds of the FeatureGroup at that point, and out of all the refs you assign, only the Map ref will be available in this.refs.
However, as per this GitHub comment: https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-366263225 you can give a FeatureGroup an onAdd handler function:
<FeatureGroup ref="features" onAdd={this.onFeatureGroupAdd}>...
and you can then use the Map refs to access the leafletElement and call fitBounds with the bounds of the incoming event target, which will be the FeatureGroup:
onFeatureGroupAdd = (e) => {
this.refs.map.leafletElement.fitBounds(e.target.getBounds());
}
This will then "zoom" the map into the bounds of your FeatureGroup, as desired.
Update
I modified my React component so that zoom and centre are controlled by query parameters. The problem with the above solution was that if you zoomed in on a MarkerClusterGroup by clicking on it, for example, it would update the zoom in the url, re-render the map and re-call onFeatureGroupAdd, thus undoing all the marker cluster goodness.
What I needed was to access the zoom level required to keep the newly drawn circle nicely in bounds, then update the url with the correct zoom level and center.
onDrawCircle = (e) => {
...
var targetZoom = this.refs.map.leafletElement.getBoundsZoom(e.layer.getBounds());
// Call function to update url here:
functionToUpdateUrl(targetZoom, e.layer.getBounds().getCenter());
}
}
In order to be able to control the whole map I also call functionToUpdateUrl in onZoomEnd and onDragEnd event handlers, like so:
onChangeView = (e) => {
functionToUpdateUrl(e.target._zoom, this.refs.map.leafletElement.getCenter());
}
and one for handling cluster clicks:
onClusterClick = (e) => {
// This time we want the center of the layer, not the map?
functionToUpdateUrl(e.target._zoom, (e.layer ? e.layer.getBounds().getCenter() : e.target.getBounds().getCenter()));
}
Then, when rendering the Map element, pass these properties:
<Map
center={center}
ref='map'
zoom={zoom}
maxZoom={18}
onZoomEnd={this.onChangeView}
onDragEnd={this.onChangeView}
>
....
</Map>
And remember to give any MarkerClusterGroups their onClusterClick callback:
<MarkerClusterGroup onAdd={this.onMarkerGroupAdd} onClusterClick={this.onClusterClick}>
Have you tried doing getBounds in the componentDidMount function instead of componentWillMount? If that doesn't work then I'd suggest extending the FeatureGroup component and adding an onLoaded function as as prop and call that function in the componentDidMount function of your extended component. And by extending the FeatureGroup component I actually mean copying/pasting it from here. (if you care about why you need to copy that whole file check this thread)
This isn't tested but your code will probably look something like
import { FeatureGroup } from 'leaflet';
import { withLeaflet, Path } from 'react-leaflet';
class CustomFeatureGroup extends Path {
createLeafletElement(prop) {
const el = new FeatureGroup(this.getOptions(props));
this.contextValue = {
...props.leaflet,
layerContainer: el,
popupContainer: el,
};
return el;
}
componentDidMount() {
super.componentDidMount();
this.setStyle(this.props);
/*
Here you can do your centering logic with an onLoad callback or just
by using this.leafletElement.map or whatever
*/
this.props.onLoaded();
}
}
export default withLeaflet(CustomFeatureGroup)
Note: If you are using react-leaflet V1 this is actually way easier and I can edit this answer with that code if needed.

React Redux, replacing state history

I am developing an app where I have one Canvas.
Users can choose to duplicate this canvas to save their achievement.
My question is when they come back to this canvas, is it possible to erase the actual state history and replace it by the canvas one in react-redux ?
Is there an efficient way to replace a state history by another in redux?
I could try by directly changing it in the store, but it's a bit dirty...
Here is some snippets of my code:
Store
const store = createStore(combineReducers({
historyCanvas,
canvasDrawing
}));
Reducers
var stateCanvasDrawing = {
lines = [{id:1, strokes: [..] }, {id:2, strokes: [..] }]
}
var stateHistoryCanvas = {
history = [{id: history1, state: {present: [..], past: [..], future:[..]}]
I store states in the historyCanvas reducers:
Then by clicking on a icon, I am able to retrieve the state I want to replace:
var tempState = state.stateHistoryCanvas[0]['state'];
And then I want to do something like that:
store.getState()['stateCanvasDrawing'] = tempState;
in order to replace the actual state by the state I retrieve.
That would be easy with just redux.
But I'm using redux-undo, so how could I copy the entire history I have stored in 'stateHistoryCanvas' to the new state? I don't want to copy only the
but also the state['future'] state['past']
Hope it's clear,
Thank you all,

in react Redux how to structure app to decouple component from state atom

in an redux app, using connect to fetch data from state is the way to go. problem is i find my self tighly coupling the component with the state atom.
in case i want to change the structure of the state tree, all components that used to consume such state will break.
so how to decouple them ?
example
initialState = {
users: { ids:[1,2] , byId:{1:{name:'user 1'},2:{name:'user 2'} }
posts: { ids:[1,2] , byId:{1:{title:'post 1'},2:{title:'post 1'} }
access : {1:[1,2],2:[1,2]} //post_id : [user_id who can see post]
}
in this simple state, i'm descriping that i have 2 users, and 2 posts, both posts are visible to both users..
in a component that list posts and users the connect can be
render(){
let {posts,access,currentUser} = this.props;
let my_posts = posts.ids.map(post_id=>posts.byId[post_id])
.filter(post=>(access[post.id].indexOf(currentUser.id)>-1)
//above map will return posts, and filter will filterout posts user dont have access to.
}
connect( (state,prop)=>{currentUser:users[prop.user_id],posts,access})(Component);
<Component user_id={1} />
the problem here is that the render function of the component do lots of manipulation with the state to render correct data. it would be much better if i can do something like
render(){
let my_posts = Posts.ofUser(currentUser.id)
//now Posts should be a service that has access to state and return the needed data.
}
how can i create such Object that deals with the state and expose an api that components and connect functions contact for information.
i read about reselect alot, but how to implement it ?
The easiest way to decouple state shape from your components is querying any of your state prop through selectors.
It adds a bit of boilerplate code, but once is done, you'll get a fully testable bridge between your components and application state.
To get started with selectors, take a look to Redux Docs Computing derivated data page.
Reselect is just an utility to create memoized selectors.

Resources