React-leaflet how to resetStyle - reactjs

I'm following Leaflet's Choropleth tutorial
http://leafletjs.com/examples/choropleth.html
and using react-leaflet.
I managed to setStyle without any modification from the original source code and it works.
highlightFeature(e) {
var layer = e.target;
layer.setStyle({
weight: 5,
color: '#666',
dashArray: '',
fillOpacity: 0.7
});
}
The layer has a setStyle property. Now to resetStyle that I'm having propblems.
I tried to access it with
resetHighlight(e) {
this.refs.geojson.resetStyle(e.target);
}
while having GeoJson
<GeoJson
ref="geojson"
data={this.state.data}
style={this.getStyle.bind(this)}
onEachFeature={this.onEachFeature.bind(this)}
/>
but it it doesn't have resetStyle property
Anyone can suggest another way of resetting the style in react-leaflet ?

The solution was to access the leafletElement of geojson which has resetStyle
resetHighlight(e) {
this.refs.geojson.leafletElement.resetStyle(e.target);
}

react-leaflet-choropleth is a way to handle choropleth if you are not wanting to write it from scratch. It is based off of the leaflet-choropleth plugin
import Choropleth from 'react-leaflet-choropleth'
import { Map } from 'react-leaflet'
const style = {
fillColor: '#F28F3B', //default color filll
weight: 2, //normal styling
opacity: 1,
color: 'white',
dashArray: '3',
fillOpacity: 0.5
}
const map = (geojson) => (
<Map>
<Choropleth
data={{type: 'FeatureCollection', features: geojson} /*feature collection or array*/}
valueProperty={(feature) => feature.properties.value /*value for choropleth*/}
visible={(feature) => feature.id !== active.id /*use choropleth color?*/}
scale={['#b3cde0', '#011f4b'] /*color range*/}
steps={7 /*how many different colors to use?*/}
mode={'e' /*use equadistance mode, others include kmeans and quantile*/}
style={style}
onEachFeature={(feature, layer) => layer.bindPopup(feature.properties.label)}
ref={(el) => this.choropleth = el.leafletElement /*get the geojson's layer container*/}
/>
</Map>
)
ReactDom.render(<map geojson={...} />, document.body)

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

Deckgl Wrapper Covering map Preventing Map Interaction

I'm working on a fairly basic implementation of Deckgl with ReactMapGl. I'd like to render a polygon overlay that outlines an array of coordinate and I'm following the documentation as well as examples that I found online. Unfortunately in my current implementation the Deckgl Wrapper div covers the entire map preventing the user from being able to interact with it. See Codesandbox example here:
https://codesandbox.io/s/react-map-5dnkoz
const data = [
{
contour: [
[-86.83446165702009, 36.17150121813963],
[-86.8287327938404, 36.15548883458097],
[-86.85771573862695, 36.15235867540224],
[-86.84962906703987, 36.139957124954705],
[-86.86694827924185, 36.1401699318269],
[-86.86802690445148, 36.15681345538646],
[-86.88323041951918, 36.16074825898015],
[-86.86222486725711, 36.17675839228444],
[-86.8486865848925, 36.16747822232059],
[-86.83960001512133, 36.166995304396785],
[-86.83446165702009, 36.17150121813963]
]
}
];
const layer = new PolygonLayer({
id: "polygon-layer",
data,
pickable: false,
stroked: true,
filled: false,
lineWidthMinPixels: 2,
getPolygon: (d) => d.contour,
getLineColor: [85, 119, 242]
});
export default function IndexPage() {
const viewport = {
latitude: 36.139691,
longitude: -86.803268,
zoom: 11
};
return (
<div>
<Map
initialViewState={viewport}
style={{ width: 800, height: 600 }}
mapStyle="mapbox://styles/mapbox/streets-v9"
mapboxAccessToken={
"pk.eyJ1IjoicGhpbGZlbGl4IiwiYSI6ImNrZTdsc3FkZzA4b3IyeWswbHhueTRkb28ifQ.MtAPCLJVyCsMHIDuXTbQGQ"
}
>
<DeckGL viewState={viewport} layers={[layer]} />
<Marker longitude={-86.803268} latitude={36.139691} color="red" />
</Map>
</div>
);
}

Remove (or at least hide) card on react-admin List

I want to get rid of the Card on the background react-admin's List (v3.4.2). I get the desired effect if I define a string on the component property:
<List component={"some string"}/>
But this spams the console with an error:
And I don't want to have that error. On top of that, I think I shouldn't be changing the component property (I can't find it on the official docs).
The code should be the following: https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/list/List.js
How should I do this? Is it possible to pass a style? Is there any component that works out of the box? Or should I just go custom?
You can hide the background using styling:
import { makeStyles } from '#material-ui/core/styles'
const useListStyles = makeStyles(theme => ({
content: {
boxShadow: 'none',
backgroundColor: 'inherit',
},
main: {
// backgroundColor: 'red',
},
root: {
// backgroundColor: 'red',
},
}))
const MyList = (props) => {
const classes = useListStyles()
return (
<List classes={classes} {...props} >
...
</List>
)
}

Clearing map layers with react-leaflet and hooks

I'm building a custom plugin for react-leaflet to locate the user using leaflet's locate method.
It works basically, but with the one problem of the layer not clearing between turning location off and back on again. Each time the locate button is toggled, locate should start fresh.
Here is a codesandbox of the problem. As you toggle the button, the circle becomes darker as the layers are stacked on top of each other.
Here is the component:
import React, { useEffect, useState, useRef } from 'react'
import L from 'leaflet'
import { useLeaflet } from 'react-leaflet'
import LocationSearchingIcon from '#material-ui/icons/LocationSearching';
import MapButton from './MapButton'
function Locate() {
const { map } = useLeaflet();
const [locate, setLocate] = useState(false);
function toggleLocate() {
setLocate(!locate)
}
console.log(locate)
const layerRef = useRef(L.layerGroup());
useEffect(() => {
if (locate) {
map.removeLayer(layerRef.current)
map.locate({ setView: false, watch: true, enableHighAccuracy: true }) /* This will return map so you can do chaining */
.on('locationfound', function (e) {
L.circleMarker([e.latitude, e.longitude], {
radius: 10,
weight: 3,
color: 'white',
fillColor: 'blue',
fillOpacity: 1
}).addTo(layerRef.current);
L.circle([e.latitude, e.longitude], e.accuracy / 2, {
weight: 1,
color: 'blue',
fillColor: 'blue',
fillOpacity: 0.2
}).addTo(layerRef.current);
window.localStorage.setItem('accuracy', e.accuracy)
map.setView([e.latitude, e.longitude], 16)
layerRef.current.addTo(map)
})
.on('locationerror', function (e) {
alert("Location error or access denied.");
})
} if (!locate) {
map.stopLocate();
map.removeLayer(layerRef.current);
}
}, [locate, map]);
return (
<MapButton
title={locate ? 'Click to disable location' : 'Click to locate'}
onClick={toggleLocate}
left
top={102}
>
<LocationSearchingIcon fontSize="small" style={{ color: locate ? 'orange' : '' }} />
</MapButton>
)
}
export default Locate
I would appreciate any solution or tips to stop the layers stacking, and clear properly went the button is toggled. Thanks
As Falke Design's mentions in a comment above:
Add the circle to a featureGroup and then every time locationfound is fired, you call featuregroup.clearLayers() and then add the circle new to the featureGroup
This solved the problem.
The working codesandbox is here

Resources