How to add multiple markers using react-leaflet upon api call? - reactjs

In context to my previous question,
MapContainer, TileLayer, Marker, Popup .. React Leaflet
How can I add multiple markers of places
Use case is that, when cycle is travelling from one place to other place, I need to show markets along the distance that bicycler has travelled.

Here is the example. On MapsComp:
Declare a state variable to keep track of the markers
Fetch the markers when the comp mounts and save them to the variable
Loop over the markers under TileLayer to visualize them when markers variable has data
class MapsComp extends React.Component {
state = { markers: [] };
componentDidMount() {
fetch("https://api-adresse.data.gouv.fr/search/?q=paris&type=street")
.then((response) => response.json())
.then((response) => {
console.log(response);
this.setState({ markers: response.features });
});
}
...
here loop overs markers data to visualzie them
{this.state.markers.length > 0 &&
this.state.markers.map((marker) => (
<Marker
position={[
marker.geometry.coordinates[1],
marker.geometry.coordinates[0]
]}
icon={icon}
>
<Popup>{marker.properties.label}</Popup>
</Marker>
))}
}
Note that this is a free api used just for demonstrating the example.
Edit
I managed to reproduce your code.
you don't need a service to fetch the json because you have it locally. Just import it.
import json from "../data/data.json";
and then assign it to the state variable (or even you could avoid that and use it directly, even better)
this.state = {
geoData: json.Sheet1,
};
Inside renderMarkers method you have a dictionary so you need its values so use Object.values to extract the coordinates
renderMarkers = () => {
let latLong: Array<Array<any>> = [];
Object.values(this.state.geoData).map((val, index) => {
let dt1: Array<any> = [];
dt1.push(Number(val.lat), Number(val.lng));
latLong.push(dt1);
});
return latLong;
};
last but not least visualize the points as Circles and not as Markers use preferCanvas flag on map container because you have 26000 markers. Leaflet cannot handle such an amount of markers so render them as circle markers. You will still see that the performance is not the best but for sure better than using markers and not canvas.
I am not going to get into the reasons of this behavior as it is out of this questions' scope, as you have not mentioned that you have such a big amount of points in the first place.
<MapContainer
...
preferCanvas
>
...
{this.renderMarkers().length > 0 &&
this.renderMarkers().map((value, index) => {
return (
<CircleMarker center={[value[1], value[0]]} key={index}>
<Popup>{index} Sydney, Hi!!!</Popup>
</CircleMarker>
);
})}
This is the result of the rendering:

Related

How to auto center coordinates and zoom for multiple markers in react-google-maps/api

I want to show multiple dynamic markers on the map. What I mean by dynamic markers is that the markers are determined by the user's past activity. I want to determine the appropriate zoom level and center coordinates so that the user does not have to scroll through the map. 
I have checked this documentation https://react-google-maps-api-docs.netlify.app/ but can't find any example related to the specification I need.
I found an algorithm to count the center coordinate of multiple markers here Find center of multiple locations in Google Maps but only for the center coordinate (not with the best zoom level) and it's written with the original google maps api #googlemaps/js-api-loader.
How could I do search for the center coordinate and zoom for multiple markers with this #react-google-maps/api library?
https://www.npmjs.com/package/#react-google-maps/api Here in the example, they have used the onLoad function with a useCallback hook. From there, I got the understanding of use of the onLoad function to load all the markers and use the fitBounds function on markers to get the centre of all markers, and the zoom level for markers
Note: The fitBounds function is from the Google Maps library.
Here's an example:
import React from 'react';
import {GoogleMap, useLoadScript, MarkerF } from "#react-google-maps/api";
import { useSelector } from 'react-redux';
const HomePageMap = () => {
const { isLoaded } = useLoadScript({ googleMapsApiKey: "your-api-key-here" }); // store api key in env file for better security
let approvedDocs = useSelector(state => state.approvedSightingLocation.approvedSightings)
// here redux is used to store data, you can use api also to fetch data
if(!isLoaded) {
return <div>Loading...</div>
} else {
return (
<>
<div className='box-shadow' style={{margin:"20px",padding:"1px"}}>
<GoogleMap options={{streetViewControl:false,zoomControl:true}} mapContainerClassName="main-map-image"
onLoad={(map) => {
const bounds = new window.google.maps.LatLngBounds();
approvedDocs.forEach((location) => {
bounds.extend({lat:parseFloat(location.lat),lng:parseFloat(location.lng)});
})
map.fitBounds(bounds);
}}
>
{approvedDocs.map((location) => {
return <MarkerF key={location.docid} position={{lat: parseFloat(location.lat),lng: parseFloat(location.lng)}}/>
})}
</GoogleMap>
</div>
</>
)
}
}
export default HomePageMap

How to identify, inside an onClick event handler, which out of many polygons plotted on the same map (using react-leaflet) was clicked?

Situation:
I am plotting a country and all its state boundaries on a map using the react-leaflet. I am plotting each state boundary as a polygon (definitions are passed in a JSON file.) using the function . All the state boundary definition is passed in a single JSON file as different JSON objects. Each object has a unique id.
My Code:
import React from 'react'
import { MapContainer, TileLayer, GeoJSON } from 'react-leaflet'
import * as L from "leaflet";
const Map = (props) => {
let cordinates = [14.716, -14.467] // Dispaly coordinates
return (
// Cordinates of map
<MapContainer center={cordinates} zoom={7} scrollWheelZoom={false}>
{/* Display map */}
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{/* <GeoJSON id={Math.random()} data={country} /> */}
{/* Highlited area in map */}
<GeoJSON data={props.state} onEachFeature={props.handleEachFeature} />
</MapContainer>
)
}
export default Map
I am passing the JSON file and handleEachFeature function(returning the whole JSON file in the console) as props.
What I want to do:
When a user clicks on the map, I want to clear up the entire map and only plot the state within which the click was. Basically, the state will be zoomed and I also want to plot its district boundaries (I have definitions for the district boundaries for each state as well).
Approach I am taking:
I am trying to capture the id associated with the polygon (corresponding to the state) that was clicked inside the onClick event. I can then erase the existing map and using the captured id I can plot the state (and its districts) clicked. However, no matter which state is clicked, the onClick event is returning me the whole data of all the polygons. Following is my code:
On click handleEachFeature function:
function handleEachFeature(feature, layer) {
layer.on("click", L.bind(handleClick, null, layer));
}
// This is returning whole json file in console. But, I want only Polygon id on which user clicked.
function handleClick(e) {
console.log(e);
}
Things I already tried:
I used a single JSON file that contains multiple polygons. However, onClick event I get the whole JSON file, not any unique value to identify the polygon.
I also tried using different JSON files for each polygon (state) and add them to the map one by one but got the same result.
Please suggest any approach using react-leaflet or some other library.
You can do this by storing the unique identifier (cartodb_id in the provided example) in a variable and then use it to change the style of the geojson and render the clicked district with a specific style.
Using onEachFeature function you can derive the unique id and zoom to the clicked district. Once you store it in a var you can then filter the geojson by showing only this object that contains the unique id. Since react-leaflet's geojson comp data property is immutable you have to play with the reference (ref). You can use leaflet's eachLayer to attach specific style to all objects apart from the clicked. The latter will be achieved by setting the clicked layer style once you filter the geojson via a useeffect (see code below). Then using leaflet's addData you can readd the filtered geojson on the map.
export default function Map() {
const [map, setMap] = useState(null);
const geojsonRef = useRef();
const [featureId, setFeatureId] = useState(null);
const handleEachFeature = (feature, layer) => {
layer.on({
click: (e) => {
setFeatureId(feature.properties.cartodb_id);
map.fitBounds(e.target.getBounds());
}
});
};
useEffect(() => {
if (!featureId || !geojsonRef.current) return;
geojsonRef.current.eachLayer((layer) => {
layer.setStyle({
opacity: 0.5,
weight: 0
});
}); // inherited from LayerGroup
const newDistricts = districts.features.filter((district) => {
return district.properties.cartodb_id === featureId;
});
geojsonRef.current.addData(newDistricts);
}, [featureId]);
return (
<MapContainer
center={position}
zoom={9}
style={{ height: "100vh" }}
ref={setMap}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{map && (
<GeoJSON
ref={geojsonRef}
data={districts}
onEachFeature={handleEachFeature}
/>
)}
</MapContainer>
);
}
You could erase it entirely using clearLayers method but that makes no sense for me because you will end up with showing only the clicked district once you click it. I tried another approach by changing the style of all other districts apart from the clicked one. This way you can click a new one and revert the style of the previous one.
A simple free geojson is used to present the result.
Demo

React ref.current is still null in componentDidUpdate

I'm trying to zoom-in on a set of coordinates using react-map-gl. To do so, the library encourages us to use React's Refs. When instantiating the component in render, I'm passing down a ref that should hold the underlying MapBox map object. I managed to make the behavior work. Only problem: it's highly inconsistent.
I'm calling a method called setCamera in componentDidUpdate. But it only works on the initial load. If I reload the page, I've got an error from Gatsby. If I close the error it will work again. Up until the next reload.
The error is that this.mapRef.current is null. I tried to put a conditional to verify that the value isn't null before trying to access it, however this cause the animation to just never work. No matter how many times I reload, it will never be performed. Whereas without the conditional it could at least work half the time before crashing. So this already is a mystery in itself and if someone has an idea for why such behavior can happen, I'm all ears.
Still, I wasn't discouraged and tried to put the call to setCamera in a setTimeout and yes, it works! Even putting a very low timeout like 1 makes the code work 95% of the time. But I'm unsatisfied with it, because I understand that putting that kind of timers isn't what I'm supposed to do and to make things worse, it doesn't fix the issue consistently.
My understanding of the problem is that MapRef is still not set in componentDidUpdate for some reason. It's being set sometimes later. I don't know if React supports threading or if some kind of async witchcraft is deceiving me behind the scenes, but what I'm wondering is when am I guaranteed for my ref to be properly set? Where or how should I write this code?
Thank you in advance to anyone who can help me on that. 🙂
Here's my sample code:
import React, {Component} from 'react';
import MapGL, {NavigationControl, Source, Layer} from 'react-map-gl';
import 'mapbox-gl/dist/mapbox-gl.css';
import gpxParser from 'gpxparser';
import bbox from '#turf/bbox';
const MAPBOX_TOKEN = 'exampleToken'
class HikeMapbox extends Component {
constructor(props) {
super(props)
this.state = {
gpxData: '',
};
}
// Read et get the GPX file, return it as a bunch of text
componentDidMount() {
const gpxPath = `https:${this.props.gpxPath}`;
// Simple GET request using fetch
fetch(gpxPath)
.then(response => response.text())
.then(text => this.setState({ gpxData: text }));
}
componentDidUpdate() {
// Set the camera on didUpdate
setTimeout(() => {
const geoJsonData = this.getGeoJson();
this.setCamera(geoJsonData);
}, 10);
}
// Get Max and Min Lat and Lon to Zoom on the GPX trace
setCamera(geoJsonData) {
if (geoJsonData) {
const [minLng, minLat, maxLng, maxLat] = bbox(geoJsonData);
this.mapRef.current.fitBounds(
[
[minLng, minLat],
[maxLng, maxLat]
],
{padding: 40}
);
}
}
// Take the text and parse it as geoJSON
getGeoJson() {
const { gpxData } = this.state;
let gpx = new gpxParser();
try {
gpx.parse(gpxData);
} catch (err) {
console.log(err);
}
const geoJson = gpx.toGeoJSON();
return geoJson
}
// Return the GPX trace in Mapbox
showGpxFile() {
const GeoJsonData = this.getGeoJson();
// Choose the style of the GPX trace
const layerStyle = {
id:'contours',
type:'line',
source:'contours',
paint: {
'line-color': 'blue',
'line-width': 3
}
};
return (
<>
{
// Return the React Mapbox GL code to show the GPX file on the map
GeoJsonData && (
<Source type="geojson" data={GeoJsonData}>
<Layer {...layerStyle} />
</Source>
)
}
</>
)
}
render() {
this.mapRef = React.createRef();
return (
<>
<MapGL
ref={this.mapRef}
initialViewState={{
latitude: this.props.latitude,
longitude: this.props.longitude,
zoom: 8,
}}
style={{width: "100%", height: "100%"}}
mapStyle="mapbox://styles/mapbox/outdoors-v11"
mapboxAccessToken={MAPBOX_TOKEN}
>
<NavigationControl />
{this.showGpxFile()}
</MapGL>
</>
)
}
}
export default HikeMapbox;
By the way, I'm running this code on my computer using gatsby develop. I don't know if that could be related, but I thought it could be relevant.
I've found a solution!
My issue was that setCamera was dependent on two conditions and these two conditions could happen in any order.
Fetch is successful and we have data to display.
The map is loaded and we have a ref to it.
Instead of initializing mapRef, in the constructor or in render, I've made a function…
onMapRefChange = node => {
this.setState({mapRef: node});
// I'm calling my method now
this.setCamera()
};
… That I'm passing in in the ref parameter
<MapGL
ref={this.onMapRefChange}
...
Eventually onMapRefChange will receive an actual map object and then the code in setCamera will be able to access it.

Render Marker at certain interval

In context to my previous question,
I'm looking at rendering marker on map at certain intervals, like 10sec or 20 sec.
Below is the code ...
{
setInterval(() => {
this.tmcount += 1;
console.log(this.tmcount);
console.log(this.state.geoData[this.tmcount]);
return (
<Marker key={this.tmcount} position={[this.state.geoData[this.tmcount].lat, this.state.geoData[this.tmcount].lng]}>
<Popup>
{this.tmcount} Hi!!!
</Popup>
</Marker>
)
}, 1000)
}
I tried in above way but its not rendering..
Your question needs a lot of work. I have no idea of the context of where this setInterval is running, or where your actual map component is. That being said, you need to think of this differently. You want your map to update periodically, which means you need to have some state variable that keeps track of what's updating, and the map should respond in kind. Let's set up a component that has an array of markers in its state, and a map that renders what's in that array at any given time:
const MapComponent = () => {
const [markers, setMarkers] = useState([]) // start with nothing
return (
<MapContainer center={center} zoom={zoom}>
{markers.map((marker, index) => {
const key = {index} // bad idea here
const {lat, lng} = marker
return <Marker key ={} position={} />
})}
</MapContainer>
)
}
Initially, you'll have no markers, because your state variable is an empty array. But your component expects that if there are markers, they will have a .lat and .lng property on each entry in that array. Now, assuming you have some datasource which is an array of objects containing location info, you can set up a useEffect to progressively update your state variable with new entries from that dataset:
const [markers, setMarkers] = useState([])
useEffect(() => {
const interval = setInterval(() => {
// clone state variable
const updatedMarkers = [...markers]
// add next marker entry from source dataset
updatedMarkers.push(geoData[markers.length])
// setMarkers(updatedMarkers)
}, 1000) // 1 second interval
return () => { clearInterval(interval) } // clean up interval on unmount
}, [])
return ( ... )
So now, on component mount, a timer is set up to update the state every second with the next entry from your source data. The map will respond in kind and add the next marker every second. I haven't tested this, but hopefully its enough to give you an idea of what to do.

cannot make react-leaflet to update markers dynamically: it gives TypeError: Cannot read property 'leafletElement' of undefined

I have the table and I have the react-leaflet component drawn in a single tab (rc-tabs). They are not connected but Redux.
I have rows in the table with coordinates. When I click on the row, coordinates are passed into Tab component and then via props are moved to map and are drawn.
Well, they should be - when I pass the whole array of rows with coordinates - they are drawn just fine, but when I am passing single values - I meet some troubles.
I have testsData - where all rows are stored and, depending on what row is clicked, I find the index. When I pass to Map testData[0] - it is drawn fine. When I try to change the index with the help of redux - I got an error. I use approach with index and before that I used another - where I passed the whole row into the props - no luck.
const Map = (props) => {
return (
<LeafMap
preferCanvas={true}
zoom={zoom}
style={mapHeightStyle}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
/>
{props.tests.map(item => (
<Marker
className={item.id}
key={item.id}
position={item.coordinates[0], item.coordinates[1]}
}
>
</Marker>
))}
</FeatureGroup>
</LeafMap>
)
};
And here is my Tabs component (I cut some code!)
const Tabs = () => {
let clickedTestRow = useSelector(state => state.deviceTestsTable.rowClicked);
let testsData = useSelector(state => state.fetchTestsData.testsData);
let [markers, setMarkers] = useState([]);
let clickedTestRowIndex = 0;
if (Object.keys(clickedTestRow).length) {
clickedTestRowIndex = testsData.findIndex(x => x.id === clickedTestRow.id);
if (!markers.includes(clickedTestRow)) {
setMarkers(testsData[clickedTestRowIndex]]);
}
}
// initial value - showing the first row on map
useEffect(() => {
if (testsData.length > 0) {
setMarkers([testsData[clickedTestRowIndex]]);
}
}, [testsData]);
let props = {
tests: markers
};
const tabs = [
{key: 'Map', component: <Map {...props}/>, disabled: false},
];
What am I missing? Every time I have TypeError: Cannot read property 'leafletElement' of undefined
Got it! Sorry, this was really annoying - the problem was in my
mapRef.current.leafletElement.getBounds().contains(markerRef.current[item.id].leafletElement.getLatLng()))
where I referred to item, to see if it in within bound. For some reason leaflet didn't like it when I was update the state and data dynamically. I feel so sorry to have this error - I didn't type it above, because I was sure the problem is in another place...
Thanks, #rfestag and #kboul for comments!

Resources