React Leaflet - Update map center - reactjs

I'm using React Leaflet. I have a map and an array with names and corresponding positions. Every name from the array is a button. When I click on a name the position changes and should be updated in the map. That works with the marker but not with the section of the map. How can I update also the section of the map?
const data = [
{
name: 'John',
coordinates: {
langitude: '40.72260370101827',
latitude: '-73.99323791583221',
},
},
{
name: 'Bob',
coordinates: {
langitude: '40.72843542344666',
latitude: '-73.94860440141105',
},
},
{
name: 'Chris',
coordinates: {
langitude: '40.79159996340942',
latitude: '-73.94077957876242',
},
},
];
export default function Map(props) {
const { index, data } = props;
return (
<MapContainer
center={[
data[index].coordinates.langitude,
data[index].coordinates.latitude,
]}
zoom={16}
>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
></TileLayer>
<Marker
position={[
data[index].coordinates.langitude,
data[index].coordinates.latitude,
]}
>
<Popup>{data[index].name}</Popup>
</Marker>
</MapContainer>
);
}

Here's a complete description for this issue in: another thread. Basically the center prop (and all other props, except children) of MapContainer is immutable, so you need to use the provided methods as explained in the answer above.However, here's a quick fix I thought of:
<MapContainer
key={JSON.stringify([data[index].coordinates.langitude, data[index].coordinates.latitude])}
center={[
data[index].coordinates.langitude,
data[index].coordinates.latitude,
]}
zoom={16}
>
Only addition to your code is the key prop, which takes the stringified center position. So when the position changes, another instance of the MapContainer component is created (a fresh copy ;) ), having the proper center value.

From the official docs:
Except for its children, MapContainer props are immutable: changing
them after they have been set a first time will have no effect on the
Map instance or its container.
As a result you have to create a custom component that changes the map view upon coordinate change
function SetViewOnClick({ coords }) {
const map = useMap();
map.setView(coords, map.getZoom());
return null;
}
Use it a child of MapContainer below your Marker comp by passing the coorinates as a prop.
<SetViewOnClick
coords={[
data[index].coordinates.langitude,
data[index].coordinates.latitude
]}
/>
Once the updated index is received map's view will be changed.
Demo

Related

GoogleMaps api : why does my map state become null?

Help appreciated, I'm stuck !
What I try to do
I display a Google map with a set of marker.
When I click on a marker, I want to add a Google circle to the map.
What happens
When I click on a first marker, no circle is displayed.
But when I click on a second marker and more, they are displayed !
Why it does not work
I've tracked the map state value with console.log.
The problem is that when I first go to the MarkkerClicked function, for an unknown reason, the "map" state's value is "null" ! So no circle is created.
And, even stranger, the map state contains a map instance when the map is first loaded, and also when I click a second marker.
Can you tell me what I have done wrong, that makes the map value set to null when the first marker is clicked ?
My component :
import { GoogleMap, MarkerF, useJsApiLoader } from "#react-google-maps/api";
import Box from '#mui/material/Box';
import { useSelector } from 'react-redux'
let mapCircle1 = null
export default function MapPage() {
// The array of markers is in the REDUX store
const selectMarkersArray = state => state.markersArray
const markersArray = useSelector(selectMarkersArray)
// This state contains the selected marker (or null if no marker selected)
const [selectedMarker, setSelectedMarker] = useState(null);
// Options for GoogleMaps
let center = {
lat: 43.3318,
lng: 5.0550
}
let zoom = 15
const containerStyle = {
width: "100%",
height: "100%"
}
// GoogleMaps loading instructions
const { isLoaded } = useJsApiLoader({
id: 'google-map-script',
googleMapsApiKey: "MY-GOOGLE-KEY"
})
const [map, setMap] = useState(null)
const onLoad = useCallback(function callback(map) {
setMap(map)
console.log('map value in onLoad :')
console.log(map)
}, [])
const onUnmount = useCallback(function callback(map) {
setMap(null)
}, [])
// Function executed when a marker is clicked
function markerClicked(props) {
console.log('map value in markerClicked :')
console.log(map)
// I create a new Circle data
let circleOption1 = {
fillColor: "#2b32ac ",
map: map,
center: {lat:props.marker.spotLatitude, lng:props.marker.spotLongitude},
radius: props.marker.spotCircleRadius,
};
mapCircle1 = new window.google.maps.Circle(circleOption1);
// I update the selecte marker state
setSelectedMarker({...props.marker})
}
return (isLoaded ? (
<Box height="80vh" display="flex" flexDirection="column">
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={zoom}
onLoad={onLoad}
onUnmount={onUnmount}
>
{markersArray.map((marker, index) => {
return (
<MarkerF
key={index.toString()}
position={{lat:marker.spotLatitude, lng:marker.spotLongitude}}
onClick={() => markerClicked({marker:marker})}
>
</MarkerF>
)
})}
</GoogleMap>
</Box>
) : <></>
)
};
And the console.log (first log when the map is loaded, second when the first marker is clicked, third when another marker is clicked):
map value in onLoad :
jj {gm_bindings_: {…}, __gm: hda, gm_accessors_: {…}, mapCapabilities: {…}, renderingType: 'UNINITIALIZED', …}
map value in markerClicked :
null
map value in markerClicked :
jj {gm_bindings_: {…}, __gm: hda, gm_accessors_: {…}, mapCapabilities: {…}, renderingType: 'RASTER', …}```
It coulds be because you are not using State Hooks and the <Circle /> component for rendering / updating circles
I was able to reproduce your code on codesandbox and confirmed that the map is indeed returning null per marker click. I'm still unsure as to why it really is happening, but I managed to fix it and managed to render a circle even on the first marker click, after modifying your code by utilizing State Hooks for your circles, and also using the <Circle /> component as per the react-google-maps/api library docs.
I just managed to hardcode some stuff since I did not use redux to reproduce your code but one of the things I did is create a circles array State Hook:
// this array gets updated whenever you click on a marker
const [circles, setCircles] = useState([
{
id: "",
center: null,
circleOptions: {
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35,
clickable: false,
draggable: false,
editable: false,
visible: true,
radius: 100,
zIndex: 1
}
}
]);
Just make sure to add the useState on your import.
Then for demonstration, I hardcoded a markers array:
// This could be your array of marker using redux,
// I just hardcoded this for demonstration
const markersArray = [
{
id: 1,
position: { lat: 43.333194, lng: 5.050184 }
},
{
id: 2,
position: { lat: 43.336356, lng: 5.053353 }
},
{
id: 3,
position: { lat: 43.331609, lng: 5.056403 }
},
{
id: 4,
position: { lat: 43.328806, lng: 5.058998 }
}
];
Then here's my markerClicked function looks like:
// Function executed when a marker is clicked
const markerClicked = (marker) => {
console.log("map value on marker click: ");
console.log(map);
// This stores the marker coordinates
// in which we will use for the center of your circle
const markerLatLng = marker.latLng.toJSON();
// this will update our circle array
// adding another object with different center
// the center is fetched from the marker that was clicked
setCircles((prevState) => {
return [
...prevState,
{
id: "",
center: markerLatLng,
circleOptions: {
strokeColor: "#FF0000",
strokeOpacity: 0.8,
strokeWeight: 2,
fillColor: "#FF0000",
fillOpacity: 0.35,
clickable: false,
draggable: false,
editable: false,
visible: true,
radius: 100,
zIndex: 1
}
}
];
});
// this is for you to see that the circles array is updated.
console.log(circles);
};
Please do note that the onClick attribute for <MarkerF /> automatically returns a parameter that includes the marker's coordinates and other stuff, you can just try to use console.log if you wanna check. Also make sure that the value you put on the onClick is equals to the name of the function alone. In this case, it is markerClicked, you'll see later on.
Note: I also added a mapClicked function for you to be able to clear the circles array.
Please see proof of concept sandbox below.
Then this is how I rendered the <GoogleMap /> component with <MarkerF /> and <Circle /> components as its children.
<GoogleMap
mapContainerStyle={containerStyle}
center={center}
zoom={zoom}
onLoad={onLoad}
onUnmount={onUnmount}
onClick={mapClicked}
>
{markersArray.map((marker, index) => {
return (
<MarkerF
key={index.toString()}
position={{ lat: marker.position.lat, lng: marker.position.lng }}
onClick={markerClicked}
></MarkerF>
);
})}
{/* This maps through the circles array, so when the array gets updated,
another circle is added */}
{circles.map((circle, index) => {
return (
<Circle
key={index.toString()}
// required
center={circle.center}
// required
options={circle.circleOptions}
/>
);
})}
</GoogleMap>
With all these, the map value on marker click does not return null, and a circle gets rendered even on first marker click.
Here's a proof of concept sandbox for you to check and see how it works ( Make sure to use your own API key to test ): https://codesandbox.io/s/proof-of-concept-show-circle-on-marker-click-s175wl?file=/src/Map.js
Note: There's some weird offset between the marker and the circle if you zoom out too far, but seems fine when you zoom in. I have encountered this on some previous questions here in SO when rendering polylines and I don't know why is that or how to fix it.
With that said, hope this helps!

polylinedacorator with react leaflet 4

I am trying to include arrows to the Polyline in react-leaft. For that I am using polylinedecorator plugin. There is a similar post on this platform. However, it uses withLeaflet module which is not supported in react-leaflet 4.0. How can I make it run without using 'withLeaflet'.
I have tried to implement it with the hooks. However, it does not work and need some assistance, how can I make it run.
export default function App(): JSX.Element {
const polylineRef = useRef<any>(null);
const arrow = [
{
offset: "100%",
repeat: 0,
symbol: L.Symbol.arrowHead({
pixelSize: 15,
polygon: false,
pathOptions: { stroke: true }
})
}];
useEffect(()=>{
L.polylineDecorator(polylineRef.current,{
patterns: arrow
})
}, [polylineRef]);
return (
<MapContainer center={center} zoom={13} scrollWheelZoom={true} style={{height: 'calc(100% - 30px)'}}>
<TileLayer
attribution='© OpenStreetMap contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
{currentData?.movingActors.map(line =>(<Polyline key={line.id}
positions={[line.startLocation, line.endLocation] } ref={polylineRef}
color={modeColor(line.mode)}
/>
))}
</MapContainer>
</>);}
CHANGES MADE TO THE ACCEPTED ANSWER TO MAKE IT RUN
function PolylineDecorator({ patterns, polyline,color }) {
const map = useMap();
useEffect(() => {
if (!map) return;
L.polyline(polyline, {color}).addTo(map); // added color property
L.polylineDecorator(polyline, {
patterns,
}).addTo(map);
}, [map]);
return null;
}
{currentData?.movingActors.map(line =>(<PolylineDecorator key={line.id} patterns ={arrow} polyline={position} color = {modeColor(line.mode)} />) ) } //here I used color parameters to dynamically add colors
What you need is a custom react functional component that returns null and has a useEffect with the code to initialize the plugin:
function PolylineDecorator({ patterns, polyline }) {
const map = useMap();
useEffect(() => {
if (!map) return;
L.polyline(polyline).addTo(map);
L.polylineDecorator(polyline, {
patterns
}).addTo(map);
}, [map]);
return null;
}
and then use it like:
<MapContainer...>
<TileLayer url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<PolylineDecorator patterns={arrow} polyline={polyline} />
</MapContainer>
Demo

Rendering a MapContainer on several subpages results in error

I'm having some issues with MapContainer from react-leaflet throwing errors at me in my app.
I have a tabel of events, that links to a page with event details, this details page implements a map with the following component:
export function MapWithPoints(props: Props) {
const { points } = props
return (
<MapContainer scrollWheelZoom={false} style={{ height: 300, width: '100vw' }}>
<InnerMap points={points} />
</MapContainer>
)
}
InnerMap is defined as:
function InnerMap(props: Props) {
const { points } = props
const leafletMap = useMap()
React.useEffect(() => {
if (leafletMap && points.length) {
const pointArray: L.LatLngTuple[] = points.map((item) => [
item.lat,
item.lng,
])
const bounds = L.latLngBounds(pointArray)
leafletMap.fitBounds(bounds)
}
}, [points, leafletMap])
return (
<React.Fragment>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{points
.filter((poi) => poi.lat && poi.lng)
.map((poi) => (
<Marker
position={[poi.lat, poi.lng]}
key={`${poi.id}_${poi.lat}_${poi.lng}`}
icon={poi.icon ? poi.icon : DefaultIcon}
>
{poi.description ? <Popup>{poi.description}</Popup> : null}
</Marker>
))}
</React.Fragment>
)
}
The map works when I go to the first (random) entry in my list of events, the event page loads like it should, but if I go back and then choose another event page, MapContainer throws an error:
Uncaught Error: Map container is already initialized.
at NewClass._initContainer (Map.js:1092:1)
at NewClass.initialize (Map.js:136:1)
at new NewClass (Class.js:24:1)
at MapContainer.js:31:1
at commitAttachRef (react-dom.development.js:23645:1)
at safelyAttachRef (react-dom.development.js:22891:1)
at reappearLayoutEffectsOnFiber (react-dom.development.js:23545:1)
at reappearLayoutEffects_complete (react-dom.development.js:24838:1)
at reappearLayoutEffects_begin (react-dom.development.js:24826:1)
at commitLayoutEffects_begin (react-dom.development.js:24649:1)
I'm using react 18, and react-leaflet 4.0.0
Is there something I have missed in the setup?
There is a bug in 4.0.0, but even when using the latest version, I still get the error now and then. A workaround is to have a unique key on Map Container, for example timestamp. But this result in a memory leak, as each time a map is loaded it consumes more memory

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

How to add default Options for markers

I want to add a different icon to the markers that are created when i click with the marker drawing control.(react-google-maps package)
I tried addding a markerOptions prop in the DrawingManager component but it doesnt seem to work like polygonOptions work.
<GoogleMap defaultZoom={13} defaultCenter={{ lat: 38.022871, lng: 23.790431 }}>
<DrawingManager
ref={props.onDrawingManagerMounted}
defaultDrawingMode={this.state.currentDrawingMode}
defaultOptions={{
drawingControl: true,
drawingControlOptions: {
position: google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
google.maps.drawing.OverlayType.POLYGON,
google.maps.drawing.OverlayType.MARKER
]
},
polygonOptions: this.colorOptions(),
markerOptions: {
icon: {
url: require("../../../assets/images/helipadIcon.png"),
anchor: new google.maps.Point(5, 58)
}
}
}}
Quick tipp: name the package that you are using before you ask your question. It took me a while to find the react-google-maps package on npm.
Check out the official documentation of the named package: https://tomchentw.github.io/react-google-maps/
You will find out that the DrawingManager component doesn't offer a prop named markerOptions or polygonOptions. Instead use the Marker component (https://tomchentw.github.io/react-google-maps/#marker) which offers a property icon of type any.
<Marker icon={} .../>
If you want to change/edit markers that have been drawn using the DrawingManager, you can use the onMarkerComplete callback function that will return the marker object. You can use the marker object to change the icon. See: https://developers.google.com/maps/documentation/javascript/reference/drawing#DrawingManager.markercomplete
const icon = {
url: require("../../../assets/images/helipadIcon.png"),
anchor: new google.maps.Point(5, 58)
};
const onMarkerComplete = (marker) => {
marker.setIcon(icon);
}
return (
<DrawingManager onMarkerComplete={onMarkerComplete} ...>
...
</DrawingManager>
);

Resources