How to get instance of MapView in ArcGIS and use between two useEffect hooks in react? - reactjs

import MapView from "#arcgis/core/views/MapView";
useEffect(() => {
const map = new Map({
basemap: "arcgis-topographic", // Basemap layer service
});
const view = new MapView({
map: map,
center: [longitude, latitude], // Longitude, latitude
zoom: 11, // Zoom level
container: container.current, // Div element
popups: false, // Popups
});
}, []);
useEffect(() => {
//I need to use that MapView here
}, []);
I've save the view variable to the state/ref. But didnt work. How to access the instance of MapView as global variable

Hi #Dostonbek,
As I explain in the above comment. You could declare the view variable globally. Either, you could use ref hook To make the MapView instance available to both useEffect hooks. Also, you can use useState hook as wall.
import { useRef } from 'react';
import MapView from "#arcgis/core/views/MapView";
const mapViewRef = useRef();
useEffect(() => {
const map = new Map({
basemap: "arcgis-topographic", // Basemap layer service
});
mapViewRef.current = new MapView({
map: map,
center: [longitude, latitude], // Longitude, latitude
zoom: 11, // Zoom level
container: container.current, // Div element
popups: false, // Popups
});
}, []);
useEffect(() => {
const mapView = mapViewRef.current;
// Use the mapView instance here
}, []);

Related

react openlayers popup offsetWidth issue

I am trying to implement openlayers popup in react.
It is implemented in plain js here: https://openlayers.org/en/latest/examples/popup.html
Here is my code:
import {createRef, useEffect, useRef, useState} from 'react';
import './Map.css';
import Map from '../node_modules/ol/Map.js';
import Overlay from '../node_modules/ol/Overlay.js';
import TileLayer from '../node_modules/ol/layer/Tile.js';
import View from '../node_modules/ol/View.js';
import XYZ from '../node_modules/ol/source/XYZ.js';
import {toLonLat} from '../node_modules/ol/proj.js';
import {toStringHDMS} from '../node_modules/ol/coordinate.js';
const MapExample = () => {
const [popupContent, setPopupContent] = useState('');
const containerRef = createRef();
const contentRef = createRef();
const closerRef = createRef();
const key = 'CvOgKFhRDDHIDOwAPhLI';
const overlay = new Overlay({
element: containerRef.current,
autoPan: {
animation: {
duration: 250,
},
},
});
const handleCloser = () => {
overlay.setPosition(undefined);
closerRef.current.blur();
return false;
};
useEffect(() => {
const map = new Map({
layers: [
new TileLayer({
source: new XYZ({
url: 'https://api.maptiler.com/maps/streets/{z}/{x}/{y}.png?key=' + key,
tileSize: 512,
}),
}),
],
overlays: [overlay],
target: 'map',
view: new View({
center: [0, 0],
zoom: 2,
}),
});
map.on('singleclick', (evt) => {
const coordinate = evt.coordinate;
const hdms = toStringHDMS(toLonLat(coordinate));
setPopupContent('You clicked here: ' + hdms)
overlay.setPosition(coordinate);
});
}, [])
return (
<div>
<div id="map" className="map"></div>
<div ref={containerRef} className="ol-popup">
<a ref={closerRef} href="#" className="ol-popup-closer" onClick={handleCloser}></a>
<div ref={contentRef}>{popupContent}</div>
</div>
</div>
)
}
export default MapExample;
I've got an issue with offset width:
picture with an issue: Cannot read properties of null (reading 'offsetWidth') The problem is popup appears on the left bottom corner, not in the clicked place of the map.
How I understand this bug appears, because popup's properties are null. I tried to fix this bug, but could not find a working solution. Will be really grateful for any help )))
There are a number of React lifecycle issues in your code. Do not use createRef in functional components as it will create a new reference at every render - while the useEffect code will run only once. This means that the overlay will lose its connection to the map - especially since you create a new Overlay at each render.
Also I strongly advise you to not use useEffect to create Openlayers components - the reasons are very subtle - but the main problem is that your components won't be reusable or even rerendable.
As the author of rlayers I can tell you that proper wrapping of Openlayers components is not easy - you must manually manage the components' lifecycle. This works best with class-based components on which you should override all lifecycle methods.

OpenLayers6 and React

I am trying to implement OpenLayers6 ("ol": "^6.14.1") into a ReactJS project, but all the documentation is created in normal JS files and I can't find any good examples or tutorials with functional components and OpenLayers6.
I have troubles figuring out how to implement the majority of things, because the docs seem to ignore the lifecycle of React.
What I've managed to do until now is to add a marker and a popup right above the marker; to close the popup and delete the marker by deleting the whole vector layer(which seems overkill).
import { useState, useEffect, useRef } from 'react';
// import ModalUI from '../UI/ModalUI';
import classes from './MapUI.module.css';
import { drawerActions } from '../../store/drawer-slice';
import 'ol/ol.css';
import { Map, View, Overlay, Feature } from 'ol';
import Point from 'ol/geom/Point';
import { Vector as VectorLayer } from 'ol/layer';
import VectorSource from 'ol/source/Vector';
import { fromLonLat, toLonLat } from 'ol/proj';
import { toStringHDMS } from 'ol/coordinate';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';
import PopUp from './PopUp';
import { useDispatch } from 'react-redux';
export default function MapUI() {
const mapRef = useRef();
const popup = useRef();
const [coordinates, setCoordinates] = useState('');
const [newMarker, setNewMarker] = useState(
new Feature({
geometry: new Point([[]]),
name: '',
})
);
const [newMarkersLayer, setNewMarkersLayer] = useState(
new VectorLayer({
properties: { name: 'newMarkers' },
source: new VectorSource({
features: [newMarker],
}),
})
);
const closePopup = () => {
map.getOverlayById('map-popup').setPosition(undefined);
map.removeLayer(newMarkersLayer);
};
const [map] = useState(
new Map({
target: '',
layers: [
new TileLayer({
source: new OSM(),
}),
new VectorLayer({
properties: { name: 'existingMarkers' },
source: new VectorSource({
// features: [marker],
}),
}),
],
view: new View({
center: fromLonLat([26.08, 44.46]),
zoom: 15,
minZoom: 10,
maxZoom: 20,
}),
})
);
useEffect(() => {
const overlay = new Overlay({
element: popup.current,
id: 'map-popup',
autoPan: {
animation: {
duration: 250,
},
},
});
// console.log('useEffect in MapUI.jsx');
map.addOverlay(overlay);
map.setTarget(mapRef.current);
map.on('singleclick', function (evt) {
map.addLayer(newMarkersLayer);
newMarker.getGeometry().setCoordinates(evt.coordinate);
setCoordinates(toStringHDMS(toLonLat(evt.coordinate)));
overlay.setPosition(evt.coordinate);
});
}, [map]);
return (
<>
<div
style={{ height: '100%', width: '100%' }}
ref={mapRef}
className='map-container'
/>
<div id='map-popup' className={classes['ol-popup']} ref={popup}>
<PopUp coordinates={coordinates} closePopup={closePopup} />
</div>
</>
);
}
The project in the end will have an array of markers that will be requested from a back-end and will populate the given map while also keeping the ability to add new markers to the map (and to the back-end).
The general issue that I face is with how all ol objects are used in the documentation. Everything is just created in a file using const and then operated upon.
But in React I have to use useEffect() and useState() and can't just create dependencies or manipulate state however the docs say.
I am looking for some guidelines on how to properly use OpenLayers6 in React. On this note I have some questions:
How can I remove a marker without removing the whole layer ?
How can I make a marker stay on the map ?
How can I render an array or markers on the map ?
Is it correct the way I use useState() to create the initial map ?
Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
Try using this library, Rlayers
this was very helpfull for me to combine with another gis library, like turf and d3
How can I remove a marker without removing the whole layer ?
You can use react-query as hooks (useMutation) fuction to call inside useCallback
How can I make a marker stay on the map ?
What are you meaning about stay? is like show and not hide? just get state and put that's value into true
How can I render an array or markers on the map ?
again use react-query
Is it correct the way I use useState() to create the initial map ?
Yes if you have another map on your application
Is it correct the way I use useState() to keep the marker and the VectorLayer on which the marker will be placed ?
Yes
As you know, OpenLayers uses an imperative API where you add and remove features to layers, etc., while in React land, we generally do things declaratively. You'll need some imperative glue code that takes your React state and mutates the OpenLayers state to match what your React state is - that's what React itself does when it reconciles the DOM.
You can do that in a couple of different ways:
useEffects to do what useEffect does: imperative side effects (updating OL state) based on declarative inputs (React state); described below
The other option, used by e.g. react-leaflet, is that you describe the whole Leaflet state as React elements, and the library reconciles the Leaflet state based on that. It's a lot more work, but feels more React-y.
Here's a pair of custom hooks that make things a bit easier, loosely based on your example. It's in TypeScript, but if you need plain JS, just remove the TypeScript specific things (by e.g. running this through the typescriptlang.org transpiler).
useOpenLayersMap is a hook that sets up an OL Map in a ref and returns it, and takes care that a given container renders the map.
useOpenLayersMarkerSource is a hook that receives a list of marker data (here quite a limited definition, but you could amend it as you like), returns a VectorSource and also takes care that modifications to the list of marker data are reflected into the VectorSource as new Features.
Now, all the MapUI component needs to do is use those two hooks with valid initial data; it can modify its markerData state and the changes get reflected into the map.
import React, { useRef, useState } from "react";
import "ol/ol.css";
import { Feature, Map, View } from "ol";
import Point from "ol/geom/Point";
import { Vector as VectorLayer } from "ol/layer";
import VectorSource from "ol/source/Vector";
import { fromLonLat } from "ol/proj";
import TileLayer from "ol/layer/Tile";
import OSM from "ol/source/OSM";
import { MapOptions } from "ol/PluggableMap";
interface MarkerDatum {
lat: number;
lon: number;
name: string;
}
function convertMarkerDataToFeatures(markerData: MarkerDatum[]) {
return markerData.map(
(md) =>
new Feature({
geometry: new Point(fromLonLat([md.lon, md.lat])),
name: md.name,
}),
);
}
const centerLat = 60.45242;
const centerLon = 22.27831;
function useOpenLayersMap(
mapDivRef: React.MutableRefObject<HTMLDivElement | null>,
getInitialOptions: () => Partial<MapOptions>,
) {
const mapRef = useRef<Map | null>(null);
React.useEffect(() => {
if (!mapRef.current) {
// markersSourceRef.current = new VectorSource();
mapRef.current = new Map({
target: mapDivRef.current ?? undefined,
...getInitialOptions(),
});
}
}, []);
React.useLayoutEffect(() => {
if (mapRef.current && mapDivRef.current) {
mapRef.current.setTarget(mapDivRef.current);
}
}, []);
return mapRef.current;
}
function useOpenLayersMarkerSource(markerData: MarkerDatum[]) {
const [markersSource] = useState(() => new VectorSource());
React.useEffect(() => {
// TODO: this would do better to only remove removed features,
// modify modified features and add new features
markersSource.clear(true);
markersSource.addFeatures(convertMarkerDataToFeatures(markerData));
}, [markerData]);
return markersSource;
}
export default function MapUI() {
const mapDivRef = useRef<HTMLDivElement>(null);
const [markerData, setMarkerData] = React.useState<MarkerDatum[]>([
{
lon: centerLon,
lat: centerLat,
name: "turku",
},
]);
const markerSource = useOpenLayersMarkerSource(markerData);
const map = useOpenLayersMap(mapDivRef, () => ({
layers: [
new TileLayer({
source: new OSM(),
}),
new VectorLayer({
properties: { name: "existingMarkers" },
source: markerSource,
}),
],
view: new View({
center: fromLonLat([centerLon, centerLat]),
zoom: 12,
minZoom: 12,
maxZoom: 20,
}),
}));
const addNewFeature = React.useCallback(() => {
setMarkerData((prevMarkerData) => [
...prevMarkerData,
{
lat: centerLat - 0.05 + Math.random() * 0.1,
lon: centerLon - 0.05 + Math.random() * 0.1,
name: "marker " + (0 | +new Date()).toString(36),
},
]);
}, []);
return (
<>
<button onClick={addNewFeature}>Add new feature</button>
<div
style={{ height: "800px", width: "800px" }}
ref={mapDivRef}
className="map-container"
/>
</>
);
}

How to resolve "React Hook useEffect has a missing dependency: 'currentPosition'"

When I include currentPosition in the useEffect dependency array or when I delete it, the code turns into an infinite loop. Why?
I have the same problem with map but when I place map in the dependency array it's ok.
import { useState, useEffect } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import icon from "./../constants/userIcon";
const UserMarker = () => {
const map = useMap();
const [currentPosition, setCurrentPosition] = useState([
48.856614,
2.3522219,
]);
useEffect(() => {
if (navigator.geolocation) {
let latlng = currentPosition;
const marker = L.marker(latlng, { icon })
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
navigator.geolocation.getCurrentPosition(function (position) {
const pos = [position.coords.latitude, position.coords.longitude];
setCurrentPosition(pos);
marker.setLatLng(pos);
map.panTo(pos);
});
} else {
alert("Problème lors de la géolocalisation.");
}
}, [map]);
return null;
};
export default UserMarker;
The comment from DCTID explains the reason why including the state in the useEffect hook creates an infinite loop.
You need to make sure that this does not happen! You have two options:
add a ignore comment and leave it as it is
create a additional redundant variable to store the current value of the variable currentPosition and only execute the function if the value actually changed
An implementation of the second approach:
let currentPosition_store = [48.856614, 2.3522219];
useEffect(() => {
if (!hasCurrentPositionChanged()) {
return;
}
currentPosition_store = currentPosition;
// remaining function
function hasCurrentPositionChanged() {
if (currentPosition[0] === currentPosition_store[0] &&
currentPosition[1] === currentPosition_store[1]
) {
return false;
}
return true;
}
}, [map, currentPosition]);
Thank you, i have resolved the conflict how this:
import { useEffect } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import icon from "./../constants/userIcon";
const UserMarker = () => {
const map = useMap();
useEffect(() => {
const marker = L.marker;
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
const latlng = [position.coords.latitude, position.coords.longitude];
marker(latlng, { icon })
.setLatLng(latlng)
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
});
} else {
alert("Problème lors de la géolocalisation.");
}
}, [map]);
return null;
};
export default UserMarker;
the reason why you are getting infinite loop if currentPosition inside dependency array:
const [currentPosition, setCurrentPosition] = useState([
48.856614,
2.3522219,
]);
you have initially have a value for currentPosition and, then you are changing inside useEffect, that causes your component rerender, and this is happening infinitely. You should not add it to the dependency array.
The reason you are getting "missing-dependency warning" is,if any variable that you are using inside useEffect is defined inside that component or passed to the component as a prop, you have to add it to the dependency array, otherwise react warns you. That's why you should add map to the array and since you are not changing it inside useEffect it does not cause rerendering.
In this case you have to tell es-lint dont show me that warning by adding this://eslint-disable-next-line react-hooks/exhaustive-deps because you know what you are doing:
useEffect(() => {
if (navigator.geolocation) {
let latlng = currentPosition;
const marker = L.marker(latlng, { icon })
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
navigator.geolocation.getCurrentPosition(function (position) {
const pos = [position.coords.latitude, position.coords.longitude];
setCurrentPosition(pos);
marker.setLatLng(pos);
map.panTo(pos);
});
} else {
alert("Problème lors de la géolocalisation.");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [map]);
that comment will turn of the dependency check on that line of code.
To make it easy to understand I will point out the reason first then come to solution.
Why? I have the same problem with map but when I place map in the dependency array it's ok.
Answer: The reason is useEffect is re-run based on it dependencies. useEffect first run when Component render -> component re-render (cuz it's props change...) -> useEffect will shallow compare and re-run if its dependencies change.
In your case, map Leaflet Map I bet react-leaflet will return same Map instance (same reference) if your component simply re-render -> when you component re-render -> map (Leaflet Map instance) don't change -> useEffect not re-run -> infinity loop not happen.
currentPosition is your local state and you update it inside your useEffect setCurrentPosition(pos); -> component re-render -> currentPosition in dependencies change (currentPosition is different in shallow compare) -> useEffect re-run -> setCurrentPosition(pos); make component re-render -> infinity loop
Solution:
There are some solutions:
Disable the lint rule by add // eslint-disable-next-line exhaustive-deps right above the dependencies line. But this is not recommended at all. By doing this we break how useEffect work.
Split up your useEffect:
import { useState, useEffect } from "react";
import { useMap } from "react-leaflet";
import L from "leaflet";
import icon from "./../constants/userIcon";
const UserMarker = () => {
const map = useMap();
const [currentPosition, setCurrentPosition] = useState([
48.856614,
2.3522219,
]);
// They are independent logic so we can split it yo
useEffect(() => {
if (navigator.geolocation) {
let latlng = currentPosition;
const marker = L.marker(latlng, { icon })
.addTo(map)
.bindPopup("Vous êtes ici.");
map.panTo(latlng);
} else {
alert("Problème lors de la géolocalisation.");
}
}, [map, currentPosition]);
useEffect(() => {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function (position) {
const pos = [position.coords.latitude, position.coords.longitude];
setCurrentPosition(pos);
marker.setLatLng(pos);
map.panTo(pos);
});
}
}, [map]);
return null;
};
export default UserMarker;
There is a great article about useEffect from Dan, it's worth to check it out: https://overreacted.io/a-complete-guide-to-useeffect/#dont-lie-to-react-about-dependencies

Testing react-map-gl with jest and react testing library timeout

I am currently trying to test a Component wrapped with third party library which is react-map-gl marker.
It keep throwing error like project undefined. so I looked into the react-map-gl code.
let [x, y] = this._context.viewport.project([longitude, latitude]);
Then, I went to see how react-map-gl test its own stuff. Then, I tried to miminc how they do.
But it just timeout no matter how much time I put. Am I missing something?
import {_MapContext as MapContext} from 'react-map-gl'
import WebMercatorViewport from 'viewport-mercator-project'
const mockStaticContext = {
viewport: new WebMercatorViewport({
width: 800,
height: 600,
longitude: -122.58,
latitude: 37.74,
zoom: 14
})
};
const mockInteractiveContext = Object.assign({}, mockStaticContext, {
eventManager: {
on: sinon.spy(),
off: sinon.spy(),
watch: sinon.spy()
}
})
describe('<ListingMarker />', () => {
it('should not explode', async (t) => {
render(
<MapContext.Provider value={mockInteractiveContext}>
<AppMarker cluster={marker} />
</MapContext.Provider>
)
}, 30000)
})
I tried to follow their way of testing.
https://github.com/visgl/react-map-gl/blob/master/test/src/components/marker.spec.js

An example of using the 'react-leaflet' new useLeaflet() hook?

I'm trying to get a reference to the leaflet object using the hook, so I can query the new map boundaries on different events (like Map.getBoundaries()). I'm pretty new to reac-leaflet, and this approach might be completely wrong, but this is what I've got for now...
What I'm trying to do is to get the map boundaries on each moveend event, if that's helpful...
First of all, you can only use the hook in a component that's inside the Map element:
<Map>
<YourComponent />
</Map
And then inside your component you can do something like:
const YourComponent = () => {
const { map } = useLeaflet();
const [bounds, setBounds] = React.useState({});
React.useEffect(() => {
const eventHandler = event => {
setBounds(event.target.getBounds());
doSomethingElse();
}
map.on("moveend", eventHandler);
return () => {
map.off("moveend", eventHandler); // Remove event handler to avoid creating multiple handlers
}
}, [setBounds, map]);
return {
// Use bounds for whatever you need
<div>Lat: {bounds.lat}; long: {bounds.lng}</div>
}
}
Handler:
const onMoveEnd = (event) => {
const bounds = event.target.getBounds()
console.log(bounds)
}
// _northEast: LatLng {lat: 47.51470804161579, lng: 19.071493148803714}
// _southWest: ...
Component:
<Map onmoveend={onMoveEnd}></Map>

Resources