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

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

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

React Typescript: different clickhandler on specific parts of same div

I'm new to React and Typescript and Coding in general so I'm not sure if that what I'm trying to do is even possible. I have a donut chart with clickable segments. it's from a minimal pie chart: https://github.com/toomuchdesign/react-minimal-pie-chart.
So as you see the chart is round but the container is square. When I click on the segment I can check other statistics. but i want to reset it when I click on some empty place. Right now with the clickawaylistener from material UI or my own clickhandler i have to move the mouse outside of the square and can't just click next to the segments to reset since the clickaway is outside of the element. Any suggestions on how to solve this?
this is my chart with the onClick handler:
<PieChart
className={classes.chart}
onClick={handleSegment}
segmentsStyle={handleSegmentsCSS}
lineWidth={20}
label={handleChartLabels}
labelStyle={{
fontSize: "3px",
fontFamily: "sans-serif",
textTransform: "capitalize",
}}
labelPosition={115}
paddingAngle={5}
radius={30}
data={data}
animate
animationDuration={500}
animationEasing="ease-out"
/>;
And this my Clickhandler:
const handleSegment = (event: any, index: any) => {
const values = Object.values(SegementDataType).map((value, index) => ({
index,
value,
}));
setSegmentValue(values[index].value);
setStyles(segmentStyle);
setSelectedSegmentIndex(
index === selectedSegment ? undefined : index
);
};
And my Clickawaylistener is just a function to set initial values
Ok, honestly, I don't have a lot to work with (the link you posted on the comment is a not-working example).
Though, I managed to understand something from here: https://toomuchdesign.github.io/react-minimal-pie-chart/index.html?path=/story/pie-chart--full-option
Anyways, try to:
Wraps the <PieChart> component into an element (<div>, for instance).
Adds an event listener on that wrapper element.
In the listener, check if a path has been clicked or not. If not, you can deselect the item.
Something like this:
const divRef = useRef();
const handler = (e) => {
const divDOM = divRef.current;
const closestPath = e.current.closest('path');
if (closestPath != null && divDOM.contains(closestPath)) {
// Here, a segment has been clicked.
}
else {
// Here, a segment has NOT been clicked.
}
}
return (
<div onClick={handler} ref={divRef}>
<PieChart ... />
</div>
);
I also check that divDOM contains closestPath so that we are sure we are talking about a path belonging to the <PieChart>.
Though, this solution does not fix the problem that, INSIDE the <PieChart> component, the segment remains clicked. I don't think this can be fixed because of the implementation of the chart (it's a stateful component, unfortunately).
What you can try is to mimic a click on the selected path, but I don't think it will work

React-native-maps fill regions with different colors

I'm using the react-native-maps library to render GeoJSON data to a map. Is there a way to fill regions with different colors? I have a GeoJSON file of countries, and I'm passing it into the <Geojson> component.
<MapView
style={styles.map}
zoomEnabled
provider={PROVIDER_GOOGLE}
>
<Geojson
fillColor="red"
geojson={geojson}
/>
</MapView>
This is the result:
Similar question: react-native-maps fill color to each region.
The simplest way to do this is to add fill property to the features in your GeoJSON object before passing it to the <Geojson /> component.
const colorizedGeojson = {
...geojson,
features: geojson.features.map(feature => ({
...feature,
properties: {
...feature.properties,
fill: getColor(feature.properties.values.population)
}
})),
};
You'll have to implement your own getColor function, depending on the data you want to represent. The population value used here is just an example. Using d3 interpolation to get the color for the value you want to represent is a common choice.
Once you've added the fill property to your GeoJSON data, you can pass that object to the component instead of the raw GeoJSON. Make sure you don't pass the fillColor property to the component, as that would override the feature colors.
<Geojson geojson={colorizedGeojson} />
Et voila! The react-native-maps component will render each region/country with the color you choose:
It is possible to use multiple <Geojson /> component and set different fillColor prop to them. Something like that.
{geojson.features.map(feature => {
const insertedObject = {
features: [feature]
};
if (feature.id === 'AGO' ||feature.id === 'AFG' || feature.id === 'RUS') {
return <Geojson
geojson={insertedObject}
fillColor={'orange'}
/>
}
return <Geojson
geojson={insertedObject}
fillColor={'red'}
/>
})}

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

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:

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