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

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

Related

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 find the number of views of the react native page?

I'm making an app. I want to find the number of views of a forum application and posts shared in this application and send them to the firebase database? What path should I follow?
There are 2 ways to count post views.
Inside Post Feed List
When post posts are rendered inside scrollable components like ScrollView or FlatList, Use components like - https://www.npmjs.com/package/#skele/components .
This is a higher-order component that tracks if the child component is inside viewport then you can use this information to update the post view counts.
import { Viewport } from "#skele/components";
import { FlatList, View, Text } from "react-native";
const ViewportAwareView = Viewport.Aware(View);
function HomeScreen() {
const onViewportEnter = async () => {
// Logic to increase view counts here
};
const onViewportLeave = async () => {};
const renderItem = ({ item }) => {
return (
<ViewportAwareView
onViewportEnter={onViewportEnter}
onViewportLeave={onViewportLeave}
retainOnceInViewport={false}
>
<Text> .... </Text>
</ViewportAwareView>
);
};
return (
<Viewport.Tracker>
<FlatList scrollEventThrottle={16} renderItem={renderItem} />
</Viewport.Tracker>
);
}
Count View inside Post Detail Screen
When users visit post screen, increase post view count when screen mounted.
function postDetailsScreen() {
useEffect(() => {
// Logic to increase post view counts
}, []);
return <View> Post details here... </View>;
}

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.

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:

How to add an SVG/d3 map to React Native?

Use Case
I want to create a React Native app that displays coordinates stored in a PostGIS database on a world map with a special map projection.
What I have done so far
I successfully loaded the map to a React environment for my web version of the app. This is the code I implemented (only including the component that holds the map):
import React, { useEffect, useRef } from 'react';
import Aux from '../../../../hoc/Auxiliary';
import styles from './Backmap.module.css';
import * as d3 from "d3";
import { feature } from "topojson-client";
import landmass from "./../../../../land-50m";
const Backmap = (props) => {
const mapContainer = useRef(null);
let width = props.windowSize.windowWidth;
let height = props.windowSize.windowHeight;
useEffect(() => {
const svg = d3.select(mapContainer.current)
.attr("width", width)
.attr("height", height)
const g = svg.append("g")
let features = feature(landmass, landmass.objects.land).features
let projection = d3.geoAzimuthalEqualArea()
.center([180, -180])
.rotate([0, -90])
.fitSize([width, height], { type: "FeatureCollection", features: features })
.clipAngle(150)
let path = d3.geoPath().projection(projection)
g.selectAll("#landmass")
.data(features)
.enter().append("path")
.attr("id", "landmass")
.attr("d", path);
}, [])
return (
<Aux>
<svg
className={styles.Backmap}
width={props.windowSize.windowWidth}
height={props.windowSize.windowHeight}
ref={mapContainer}
>
</svg>
</Aux>
)
}
export default Backmap;
This is the resulting map from above code:
I also researched how to implement SVG shapes and even maps to React Native and came across several ways of doing so:
react-native-svg(SVG)
React Native ART(SVG)
react-native-simple-maps(map)
Problem
However, I could not implement any map using these solutions. The former two in the above list don't mention any map implementation and the latter one seems to be a very early beta version. So I am wondering if this is even currently possible in React Native. Does anybody know an approach to inserting an SVG map in React native?
It is possible to create d3 maps in react native. The thing is you cant use Dom selectors like d3.select().
You need to use react-native-svg.
Read the documentation here learn how to install and use it. Its implementaion is really close to the browser SVG API.
You import the Svg, G, and Path components
import Svg, { G, Path } from "react-native-svg";
You create a projection and path in the usual d3 manner.
let projection = d3.geoAzimuthalEqualArea()
.center([180, -180]).rotate([0, -90])
.fitSize([width, height], { type: "FeatureCollection", features: features })
.clipAngle(150)
let path = d3.geoPath().projection(projection)
The features are also extracted the same way.
let features = feature(landmass, landmass.objects.land).features
But we dont append components using d3. We append the Svg and G components directly.
return (
<Svg width='100%' height='100%'>
<G>
</G>
</Svg> )
The width and height can be styled as required.
Next the paths are appended but mapping the featurers Array to create path components.
return (
<Svg width='100%' height='100%'>
<G>
{features.map((feature, index) => {
return (<Path d={path(feature)} key={index} stroke="black" fill="grey"></Path>)
})}
</G>
</Svg> )
If you need help in rotating the svg ask in the comments to this answer.

Resources