Render MarkerClusterer with Custom Marker and Dynamic location - javascript

I have a issue when rendering markers and clustering them. When i fetch location data list from api completely, map doesn't re-render marker.
When i use custom marker child component, that can re-render map when api is fetch completely. But I can't Clustering these custom marker.
As i know that onGoogleApiLoaded just called once times at the first render so now I have no solution to solve this issue.
Here my code below. Thanks for any help.
const get_list_unit_location = useCallback(() => {
if (units.length) {
const listLocation = [];
units.forEach((unit) => {
if (unit.lat && unit.lng) {
listLocation.push({
lat: unit.lat,
lng: unit.lng,
subUnitQuantity: unit.sub_units.length,
});
}
});
setUnitLocations(listLocation);
}
}, [units]);
useEffect(() => {
get_list_unit_location();
}, [get_list_unit_location]);
const setGoogleMapRef = useCallback(
(map, maps) => {
if (unitLocations.length) {
const markers = unitLocations.map((location) => {
return new maps.Marker({ position: location, map });
});
// eslint-disable-next-line no-unused-vars
const markerCluster = new MarkerClusterer({ map, markers });
}
},
[unitLocations]
);
<GoogleMapReact
bootstrapURLKeys={{ key: process.env.REACT_APP_GOOGLE_MAP_API_KEY }}
defaultCenter={center}
defaultZoom={zoom}
options={{
fullscreenControl: false,
zoomControl: false,
}}
yesIWantToUseGoogleMapApiInternals
onGoogleApiLoaded={({ map, maps }) => setGoogleMapRef(map, maps)}
>
{unitLocations.map((location) => (
// eslint-disable-next-line react/jsx-key
<Marker
lat={location.lat}
lng={location.lng}
text={location.subUnitQuantity}
/>
))}
</GoogleMapReact>

One solution would be to have setGoogleMapRef do nothing more than store map and maps into some state, and then have a separate useEffect that creates the MarkerClusterer. This will ensure that the clusterer isn't created until both map and data are loaded (and will be recreated if the data changes.) Something like:
const [ map, setMap ] = useState();
const [ maps, setMaps ] = useState();
const setGoogleMapRef = useCallback((map, maps) => {
setMap(map);
setMaps(maps);
}, [ setMap, setMaps ]);
useEffect(() => {
if (unitLocations.length && map && maps) {
// create markers and clusterer
}
}, [ unitLocations, map, maps]);
Alternatively, if you're willing to look at a different package, #react-google-maps/api has a component for the clusterer as well as the map, marker, etc:
<GoogleMap ...>
<MarkerClusterer ...>
{clusterer => unitLocations.map((location, index) => {
<Marker position={location} clusterer={clusterer}/>
}
</MarkerClusterer>
</GoogleMap >

Related

How to centre and zoom in on multiple markers with google-maps-react?

I found this stack overflow link that does it with vanilla javascript, however it uses methods that I don't see in the google-maps-react API like LatLngBounds and bounds.extend(), however it does have maps.fitbounds().
Am I missing misreading the API or do those methods really not exist on google-maps-react. If not then how would I go about centering and zooming in on multiple markers on the map.
I don't think google-maps-react has a native way to accomplish this, you must the google maps api directly through the onGoogleApiLoaded prop.
Tidbit from this examples this example:
// Fit map to its bounds after the api is loaded
const apiIsLoaded = (map, maps, places) => {
// Get bounds by our places
const bounds = getMapBounds(map, maps, places);
// Fit map to bounds
map.fitBounds(bounds);
// Bind the resize listener
bindResizeListener(map, maps, bounds);
};
class Main extends Component {
constructor(props) {
super(props);
this.state = {
places: [],
};
}
componentDidMount() {
fetch('places.json')
.then((response) => response.json())
.then((data) => this.setState({ places: data.results }));
}
render() {
const { places } = this.state;
return (
<>
{!isEmpty(places) && (
<GoogleMap
defaultZoom={10}
defaultCenter={LOS_ANGELES_CENTER}
yesIWantToUseGoogleMapApiInternals
onGoogleApiLoaded={({ map, maps }) => apiIsLoaded(map, maps, places)}
>
{places.map((place) => (
<Marker
key={place.id}
text={place.name}
lat={place.geometry.location.lat}
lng={place.geometry.location.lng}
/>
))}
</GoogleMap>
)}
</>
);
}
}
export default Main;

Why can't I see any markers on google maps react?

I have been building the front end of an application that has the google maps API enabled. I want to have the user click on the map to add markers, and store it into an array; However after I implemented an add marker function to the onClick of the google map tag... The markers won't render.
I have had a lot of difficulty implementing the add marker function in react, and watched several tutorials but cannot seem to find a solution. Any help would be greatly appreciated!
const options = {
styles: mapStyles,
disableDefaultUI: true,
zoomControl: true,
}
var markersArray = [];
function addMarker(event){
let marker = new window.google.maps.Marker({
position: event.latLng,
time: new Date(),
draggable: true
});
markersArray.push(marker);
}
const Map = withScriptjs(withGoogleMap(props =>
<GoogleMap
defaultZoom={8}
defaultCenter={{ lat: this.state.mapPosition.lat, lng: this.state.mapPosition.lng }}
options={options}
onClick={(event) => {
console.log(event);
console.log(markersArray);
addMarker(event);
}}
>
{markersArray.map((marker) =>(
<Marker
key={marker.time.toISOString()}
position={{lat: marker.lat, lng: marker.lng }}
/>
))}
<AutoComplete
style={{width: "100%", height:'40px'}}
types={['(regions)']}
onPlaceSelected={this.onPlaceSelected}
/>
</GoogleMap>
));
I believe the issue is with this but i'm not sure how to make it work.
{markersArray.map((marker) =>(
<Marker
key={marker.time.toISOString()}
position={{lat: marker.lat, lng: marker.lng }}
/>
))}
markersArray is a plain array. When you do addMarker(event); you're not updating the state of your component, it's just a plain array. So probably React is not aware of that change. If you're using hooks, you could create a state with your markers like this
const [markers, setMarkers] = useState([])
use that markers array to render the <Marker /> and then on your event to add the marker
onClick={(event) => {
setMarker(previousMarkers => previousMarkers.concat([event]))
}
this will cause a re-render with the new value of markers and you should be able to see them.
If it's not a functional component and it's classes, it's the same, but you would define the markers in your class' state
constructor(props) {
super(props)
this.state = { markers: [] }
this.addMarker = this.addMarker.bind(this)
}
// function class below
addMarker(marker) {
this.setState(previousState => previousState.concat([marker])
}
and in your event, you'd call this.addMarker(marker)

Single custom marker google maps API in react

I have been working with Google Maps & Places API and have successfully got it working using react hooks to allow the user to drop markers, onClick info window, auto-complete place search and Geolocation.
However, what I need is that onClick it will only add a single marker which will change location every time the user clicks a new coord. Not add multiple markers on every click as it currently does.
Code below:
const [markers, setMarkers] = useState([]);
const [selected, setSelected] = useState(null);
const onMapClick = useCallback((event) => {
setMarkers((current) => [
...current,
{
lat: event.latLng.lat(),
lng: event.latLng.lng(),
time: new Date(),
},
]);
}, []);
...
return (
<GoogleMap
mapContainerStyle={mapContainerStyle}
zoom={13}
center={center}
onClick={onMapClick}
onLoad={onMapLoad}
>
{markers.map((marker) => (
<Marker
key={marker.time.toISOString()}
position={{ lat: marker.lat, lng: marker.lng }}
icon={{
url: "/crane-pin.svg",
scaledSize: new window.google.maps.Size(40, 40),
origin: new window.google.maps.Point(0, 0),
anchor: new window.google.maps.Point(15, 15),
}}
onClick={() => {
setSelected(marker);
}}
/>
))}
...
</GoogleMap>
any help would be appreciated!
To achieve your use case, you need to have access to the marker object and use the setPosition method to change the position of the marker to the clicked coordinates in the map.
Are you using any google maps react library? Here is a sample reactjs code that implements this without using any react libraries. Code snippet below:
import React, { Component } from "react";
import { render } from "react-dom";
import Map from "./Map";
import "./style.css";
class App extends Component {
render() {
return (
<div id="container">
<Map
id="myMap"
options={{
center: { lat: 37.769, lng: -122.446 },
zoom: 12,
}}
onMapLoad={(map) => {
let marker = new google.maps.Marker({
position: { lat: 37.769, lng: -122.446 },
map: map,
});
//Changing Marker position for clicked coordinate
map.addListener("click", (event) => {
marker.setPosition(event.latLng);
});
}}
/>
</div>
);
}
}
export default App;

Slow Performance for filtering markers in react-leaflet

I need an advice for the react-leaftlet port of leaflet. I am generating markers on a map and use marker clustering with react-leaflet-markercluster. Each markerdata is associated with some data. I want to filter that data based on the markers in the viewport.
My idea: Get the boundaries of the map and cross-check with each markers. Yes, it works. But the performance is extremly slow (> 4.5secs for calculating), when adding more than 500 markers.
What can I do to increase the performance?
Here is my code:
import React, { Component, Fragment } from 'react';
import CustomMarkers from './components/CustomMarkers';
import { Map, TileLayer } from 'react-leaflet';
import ImageContainer from './components/ImageContainer';
import { checkIfMarkerOnMap, createSampleData } from './utils/helpers';
import L from 'leaflet';
class App extends Component {
constructor(props){
super(props);
this.state = {
viewport: {
width: '100%',
height: '400px',
latitude: 40.00,
longitude: 20.00,
zoom: 5
},
visibleMarkers: {},
markers : {},
}
}
componentDidMount = () => {
const sampleData = createSampleData(1000);
this.setState({ markers: sampleData, visibleMarkers: sampleData });
const mapBoundaries = this.mapRef.contextValue.map.getBounds();
this.setState({ mapBoundaries });
}
getMapBoundaries = () => {
// Get map boundaries
const mapBoundaries = this.mapRef.contextValue.map.getBounds();
if(this.state.mapBoundaries !== mapBoundaries){
console.log("different");
this.setState({ mapBoundaries } );
} else return;
}
checkVisibleMarkers = () => {
console.time("checkVisibleMarkers");
const { markers, mapBoundaries } = this.state;
let visibleMarkers = Object.keys(markers)
.filter(key => (L.latLngBounds([[mapBoundaries._southWest.lat, mapBoundaries._southWest.lng], [mapBoundaries._northEast.lat, mapBoundaries._northEast.lng]]).contains([markers[key].coordinates.latitude,markers[key].coordinates.longitude])))
.map(key => { return { [key] : markers[key] } });
visibleMarkers = Object.assign({}, ...visibleMarkers);
console.log("visibleMarkers", visibleMarkers);
// this.setState({ visibleMarkers })
console.timeEnd("checkVisibleMarkers");
}
handleViewportChanged = () => {
this.getMapBoundaries();
this.checkVisibleMarkers();
}
render() {
console.log("this.mapRef", this.mapRef);
const { viewport, markers, visibleMarkers } = this.state;
const position = [viewport.latitude, viewport.longitude]
return (
<Fragment>
<Map
ref={(ref) => { this.mapRef = ref }}
center={position}
zoom={viewport.zoom}
maxZoom={15}
onViewportChanged={() => this.handleViewportChanged()}
style={{ height: '400px' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors"
/>
<CustomMarkers visibleMarkers={markers} />
</Map>
{/* <ImageContainer visibleMarkers={visibleMarkers} /> */}
</Fragment>
)
}
}
export default App;
CustomMarker.js:
import React, { Component } from 'react';
import { Marker, Tooltip } from 'react-leaflet';
import uuid from 'uuid-v4';
import {
heartIcon,
heartIconYellow,
heartIconLightblue,
heartIconDarkblue } from './../icons/icons';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import L from 'leaflet';
class CustomMarkers extends Component {
render() {
const { visibleMarkers } = this.props;
let markers;
if(Object.keys(visibleMarkers).length > 0) {
markers = Object.keys(visibleMarkers).map(key => {
let latitude = visibleMarkers[key].coordinates.latitude;
let longitude = visibleMarkers[key].coordinates.longitude;
let icon = heartIcon;
if(visibleMarkers[key].category === 'fb') icon = heartIconLightblue;
if(visibleMarkers[key].category === 'blogs') icon = heartIconYellow;
if(visibleMarkers[key].category === 'artisan') icon = heartIcon;
if(visibleMarkers[key].category === 'website') icon = heartIconDarkblue;
return (
<Marker
key={uuid()}
position={ [latitude, longitude] }
icon={icon}
>
<Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>
)
});
}
const createClusterCustomIcon = (cluster) => {
return L.divIcon({
html: `<span>${cluster.getChildCount()}</span>`,
className: 'marker-cluster-custom',
iconSize: L.point(40, 40, true),
});
}
return (
<MarkerClusterGroup
iconCreateFunction={createClusterCustomIcon}
disableClusteringAtZoom={10}
zoomToBoundsOnClick={true}
spiderfyOnMaxZoom={false}
removeOutsideVisibleBounds={true}
maxClusterRadius={150}
showCoverageOnHover={false}
>
{markers}
</MarkerClusterGroup>
)
}
}
export default CustomMarkers;
createSampleData takes the amount of sample data to generate as an input and creates a json structure for sample data { id: 1 { coordinates: {},...}
The bottleneck is the function checkVisibleMarkers. This function calculates if the marker is in viewport. Mathimatically its just two multiplications per marker.
I see a few potential issues - the performance of the checkVisibleMarkers function, and the use of uuid() to create unique (and different) key values on each rerender of a <Marker />.
checkVisibleMarkers
Regarding the checkVisibleMarkers function. There's a few calls and patterns in there which could be optimized. Here's what's currently happening:
Create an array of the markers keys
Loop through the keys, reference the corresponding marker and filter by location using L.latLngBounds().contains()
Loop through the filtered keys to create an array of objects as {key: marker}
Use Object.assign() to create an object from the array of objects
In the end, we have an object with each value being a marker.
I'm unsure of the internals of L.latLngBounds but it could be partly responsible for the bottleneck. Ignoring that, I'll focus on refactoring the Object.assign({}, ...Object.keys().filter().map()) pattern using a for...in statement.
checkVisibleMarkers = () => {
const visibleMarkers = {};
const { markers, mapBoundaries } = this.state;
for (let key in markers) {
const marker = markers[key];
const { latitude, longitude } = marker.coordinates;
const isVisible = mapBoundaries.contains([latitude, longitude]);
if (isVisible) {
visibleMarkers[key] = marker;
}
}
this.setState({ visibleMarkers });
}
A quick check on jsPerf shows the above method is ~50% faster than the method you're using, but it doesn't contain the L.latLngBounds().contains() call so it's not an exact comparison.
I also tried a method using Object.entries(markers).forEach(), which was slightly slower than the for...in method above.
The key prop of <Marker />
In the <Marker /> component you're using uuid() to generate unique keys. While unique, each rerender is generating a new key, and every time a component's key changes, React will create a new component instance. This means every <Marker /> is being recreated on every rerender.
The solution is to use a unique, and permanent, key for each <Marker />. Thankfully it sounds like you already have a value that will work for this, the key of visibleMarkers. So use this instead:
<Marker
key={key}
position={ [latitude, longitude] }
icon={icon}
>
<Tooltip>{visibleMarkers[key].category}</Tooltip>
</Marker>

How to get the bounds of a collection of markers with React-Leaflet

How can I get the bounds of a collection of markers, so that these bounds can be used to show all markers on a react-leaflet map?
This is my render function:
render() {
const position = [51.505, 4.12];
return (
<Map center={position} zoom={3} style={{width: '100%', height: '600px'}} >
<TileLayer
attribution='&copy OpenStreetMap contributors'
url='http://{s}.tile.osm.org/{z}/{x}/{y}.png'
/>
{this.state.markers || null}
</Map>
);
}
The markers are added to this.state.markers with:
this.state.markers.push(
<Marker position={position}>
<Popup>
<span>Hello</span>
</Popup>
</Marker>
);
this.setState({markers: this.state.markers});
The markers are getting displayed, but I want the bounds and centre of the map to be adjusted so the markers fit nicely within the viewport of the map with a certain amount of padding.
Any ideas on how to do this?
Edit: This is my import statement: import {Map, Marker, Popup, TileLayer} from 'react-leaflet';
You can add a bounds parameter to the Map component. It accepts a leaflet latLngBounds argument. So you can do the following:
import {latLngBounds} from 'leaflet'
...
render () {
const bounds = latLngBounds([firstCoord, firstCoord])
markerData.forEach((data) => {
bounds.extend(data.latLng)
})
return (
<Map bounds={bounds} >
...
</Map>
)
}
I'm using the following function in Angular, but I believe it should work for you as well.
fitMapBounds() {
// Get all visible Markers
const visibleMarkers = [];
this.map.eachLayer(function (layer) {
if (layer instanceof L.Marker) {
visibleMarkers.push(layer);
}
});
// Ensure there's at least one visible Marker
if (visibleMarkers.length > 0) {
// Create bounds from first Marker then extend it with the rest
const markersBounds = L.latLngBounds([visibleMarkers[0].getLatLng()]);
visibleMarkers.forEach((marker) => {
markersBounds.extend(marker.getLatLng());
});
// Fit the map with the visible markers bounds
this.map.flyToBounds(markersBounds, {
padding: L.point(36, 36), animate: true,
});
}
}
Using hooks, this worked for me
// example markers
const markers = [
[49.8397, 24.0297],
[52.2297, 21.0122],
[51.5074, -0.0901],
[51.4074, -0.0901],
[51.3074, -0.0901],
]
// hook so this only executes if markers are changed
// may not need this?
const bounds = useMemo(() => {
const b = latLngBounds() // seemed to work without having to pass init arg
markers.forEach(coords => {
b.extend(coords)
})
return b
}, [markers])
...
return (
<Map bounds={bounds} {...rest}> ... </Map>
)
create an an array bounds, push all coordinates of your markers in there and pass it as props to the map:
import { Map, latLngBound } from "react-leaflet";
const Map = () => {
const bounds = []
positions.map((position,i) => {
bounds.push([position.lat, position.lon])
})
return (
<MapContainer
bounds={bounds}
>
...
</MapContainer>
);
};
export default Map;

Categories