Mapbox in React without Class Components - javascript

I am trying to use MapBox-GL with React. I am trying to avoid using a wrapper.
I have sucessfully created the map using Class Components, but want to transform it to only using functions to take advantage of hooks. Displaying the map in a function works great:
const map = () => {
new mapboxgl.Map({
container: 'mapContainer',
style: 'mapbox://styles/mapbox/light-v9',
center: [7.32, 60.44],
zoom: 6,
})
};
const Map = () => {
const style = {
position: 'absolute',
top: 0,
bottom: 0,
width: '100%',
height: '100vh'
};
useEffect(()=>{
map();
});
return (
<Row type="flex" gutter="50">
<Col xs={{ span: 18 }}>
<div style={style} id="mapContainer" />
</Col>
</Row>
);
}
However, I want to add controllers and do things with the map. I do this normally in ComponentDidMount().
I have tried adding map.addControl(geocoder); for example to the useEffect, as well as outside the Map function. I am only getting errors:
TypeError: map.addControl is not a function

A counterpart to componentDidMount is useEffect with zero inputs.
map.addControl(geocoder) assumes that map is an instance of Map while it's a function and it doesn't return a value.
It should be:
const getMap = () => {
return new mapboxgl.Map({ ... })
};
const Map = () => {
useEffect(()=>{
const map = getMap();
map.addControl(geocoder);
}, []);
...
};

Related

How do I get to the updated path of an editable PolyLine from react-google-maps/api?

I'm using the react-google-maps/api library, and I have an application where I need the user to edit a Polyline.
The problem I'm having is grabbing the path of the polyline after the user has finished editing.
If I use native react components, the path returned on the props from the polyline is the original path of the line - not the one edited by the user.
The code below is a cutdown version of where I'm trying to get the path of the line from the react component. If you try it and edit the line, the return array is the original path. I've seen some examples using the getPath() method, but I just can't seem to get this to work on the React component (ie polylineRef.current.getPath() returns a no function error.
How should I be getting the path information of the edited line?
import React, { Fragment, useRef } from "react";
import { GoogleMap, Polyline, useLoadScript } from "#react-google-maps/api";
const MapTest = (props) => {
const polylineRef = useRef();
const mapRef = useRef();
const mapContainerStyle = {
width: "80vw",
height: "80vh",
};
const showPath = () => {
console.log(polylineRef.current.props.path); //What should be here to show the edited path if its possible to access?
};
const { isLoaded, loadError } = useLoadScript({
googleMapsApiKey: process.env.REACT_APP_GOOGLE_KEY,
});
const centre = { lat: 51.999889, lng: -0.98807 };
if (loadError) return "Error loading Google Map";
if (!isLoaded) return "Loading Maps....";
console.log(polylineRef.current.props.path);
return (
<Fragment>
<GoogleMap
mapContainerStyle={mapContainerStyle}
ref={mapRef}
zoom={10}
center={centre}
>
<Polyline
ref={polylineRef}
key={1}
path={[
{ lat: 51.9298274729133, lng: -1.0446431525421085 },
{ lat: 51.98483618577529, lng: -1.2423970587921085 },
]}
options={{ editable: true, strokeColor: "#ff0000" }}
/>
</GoogleMap>
<button
onClick={(event) => {
showPath(event);
}}
>
Show Path in Console
</button>
</Fragment>
);
};
export default MapTest;
If I use the native google API, then I can see the updated path, but I can't get a reference to the map created by the map to place the polyline onto.
If I can't access the edited path through the react component, how should I provide a reference to the google maps native API, so when I do
polyline = new google.maps.Polyline(//polyline options)
polyline.setMap(map) //Where do I get the handle for this map to put it on the map above?
/*I've tried using mapRef.current (not a map instance) and
mapRef.current.getInstance() - this makes the original map disappear, for reasons I don't understand*/
When I build this using the native API, I can access the edited path using the getPath() method, but I can't render this polyline on the component rendered above.
Other than building the map out of the native API I'm struggling to do this at the moment - but the benefits of the ease of rendering of React make me want to continue down this path for a while longer - is anyone able to help please?
I think this is what you are trying to achieve:
https://codesandbox.io/s/snowy-night-ony59?file=/src/App.js
My answer is based on:
https://codesandbox.io/s/reactgooglemapsapi-editing-a-polygon-popr2?file=/src/index.js:2601-2845
which I found by googling: react-google-maps-api editable polygon
Basically just copying and pasting the code referred by Daniele Cordano
import React, { useState, useRef, useCallback } from "react";
import ReactDOM from "react-dom";
import { LoadScript, GoogleMap, Polygon } from "#react-google-maps/api";
import "./styles.css";
// This example presents a way to handle editing a Polygon
// The objective is to get the new path on every editing event :
// - on dragging the whole Polygon
// - on moving one of the existing points (vertex)
// - on adding a new point by dragging an edge point (midway between two vertices)
// We achieve it by defining refs for the google maps API Polygon instances and listeners with `useRef`
// Then we bind those refs to the currents instances with the help of `onLoad`
// Then we get the new path value with the `onEdit` `useCallback` and pass it to `setPath`
// Finally we clean up the refs with `onUnmount`
function App() {
// Store Polygon path in state
const [path, setPath] = useState([
{ lat: 52.52549080781086, lng: 13.398118538856465 },
{ lat: 52.48578559055679, lng: 13.36653284549709 },
{ lat: 52.48871246221608, lng: 13.44618372440334 }
]);
// Define refs for Polygon instance and listeners
const polygonRef = useRef(null);
const listenersRef = useRef([]);
// Call setPath with new edited path
const onEdit = useCallback(() => {
if (polygonRef.current) {
const nextPath = polygonRef.current
.getPath()
.getArray()
.map(latLng => {
return { lat: latLng.lat(), lng: latLng.lng() };
});
setPath(nextPath);
}
}, [setPath]);
// Bind refs to current Polygon and listeners
const onLoad = useCallback(
polygon => {
polygonRef.current = polygon;
const path = polygon.getPath();
listenersRef.current.push(
path.addListener("set_at", onEdit),
path.addListener("insert_at", onEdit),
path.addListener("remove_at", onEdit)
);
},
[onEdit]
);
// Clean up refs
const onUnmount = useCallback(() => {
listenersRef.current.forEach(lis => lis.remove());
polygonRef.current = null;
}, []);
console.log("The path state is", path);
return (
<div className="App">
<LoadScript
id="script-loader"
googleMapsApiKey=""
language="en"
region="us"
>
<GoogleMap
mapContainerClassName="App-map"
center={{ lat: 52.52047739093263, lng: 13.36653284549709 }}
zoom={12}
version="weekly"
on
>
<Polygon
// Make the Polygon editable / draggable
editable
draggable
path={path}
// Event used when manipulating and adding points
onMouseUp={onEdit}
// Event used when dragging the whole Polygon
onDragEnd={onEdit}
onLoad={onLoad}
onUnmount={onUnmount}
/>
</GoogleMap>
</LoadScript>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
For some reason, the useRef hook doesn't accept the function getPath/getPaths() with the typescript error : TS2339: Property 'getPath' does not exist on type 'MutableRefObject'
const handleNewPolygonPath = useCallback(() => {
const newPath = polygonRef.getPath();
dispatch(setNewPolygonPath(newPath));
console.log(newPolygonPath);
}, [dispatch, newPolygonPath]);

Access state in esri event using loadModules ReactJS

I am using lazy loading "loadModules " from esri-loader in order to load esri modules. The problem here is that I can't access the state to store the longitude and latitude values when 'search-complete' event fires.
I am also not able to override the "allPlaceholder" value when creating the Search widget
https://codesandbox.io/s/pedantic-hellman-sbcdz?
Any idea what I could be doing wrong ? is it possible to access the Search widget outside of componentDidMount ?
Thanks !
import React , { Component, Fragment } from 'react';
import { loadModules } from 'esri-loader';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row';
class MapSearch extends Component {
constructor(props) {
super(props);
this.mapRef = React.createRef();
this.searchRef = React.createRef();
this.state = {
mysearch: null,
longitude: 0,
latitude: 0,
searchTerm: ""
};
}
componentDidMount() {
loadModules(['esri/Map', 'esri/views/MapView', 'esri/widgets/Search'], { css: true })
.then(([ArcGISMap, MapView, Search]) => {
const map = new ArcGISMap({
basemap: 'osm'
});
this.view = new MapView({
container: this.mapRef.current,
map: map,
center: [-85, 35],
zoom: 14
});
var mysearch = new Search({
view: this.view,
allPlaceholder: "TESTESTTEST", // this doesn't work
container: this.searchRef.current
});
mysearch.on("search-complete", function(event){
console.log(event);
console.log(this.state);
})
}
);
}
render() {
return (
<Fragment>
<Row >
<Col lg={7}><div className="arcmap" style={{"height": "50vh"}} ref={this.mapRef}></div></Col>
<Col lg={5}><div className="zobya-search" style={{ "wdith": "100%" }} ref={this.searchRef} ></div></Col>
</Row>
</Fragment>
);
}
}
export default MapSearch;
This turned out to be very simple, hopefully it will help someone else
just add a binding in the constructor such as
this.handleSearchComplete = this.handleSearchComplete.bind(this);
and create a new function
handleSearchComplete(event) {
this.setState({
longitude: event.results[0].results[0].feature.geometry.longitude ,
latitude: event.results[0].results[0].feature.geometry.latitude
});
}
then call this callback function such as
mysearch.on("search-complete", this.handleSearchComplete)
This turned out to be very simple, hopefully it will help someone else
just add a binding in the constructor such as
this.handleSearchComplete = this.handleSearchComplete.bind(this);
and create a new function
handleSearchComplete(event) {
this.setState({
longitude: event.results[0].results[0].feature.geometry.longitude ,
latitude: event.results[0].results[0].feature.geometry.latitude
});
}
then call this callback function such as
mysearch.on("search-complete", this.handleSearchComplete)

ReactJS and Google Maps - Displaying Markers

I'm React newbie so maybe asking a stupid question, but this has got me perplexed. As part of my learning I'm building a three component application - a parent About, and two children (GoogleMap and MapMarkerDetails). The parent does the data coordination and one child shows a google map with two markers as default, the other child shows details of the markers when it is clicked.
I am now adding functionality to add a new marker when the map is clicked. Most of the functionality works - the maps is draw, the default markers are added, and when one of the markers is clicked, this calls a function on the parent class which updates its state and this is propagated to the MapMarkerDetails element and a simple message is displayed.
Here is the parent class which I have commented to help understanding:
import React, { Component } from 'react';
import GoogleMap from './GoogleMap'
import MapMarkerDetails from './MapMarkerDetails'
class About extends Component {
state = {
markers: [{
position: { lat: 51.438759, lng: -2.864514 },
label: 'A',
map: null,
}, {
position: { lat: 51.433636, lng: -2.868734 },
label: 'B',
map: null,
}],
greeting: 'HelloA'
}
showMarkerInfo = (label) => {
this.setState({greeting: ('Hello ' + label)})
}
/*
Adding a new Marker
This function is called from the child element GoogleMap
However, the setState(...) dosn't seem to propogate down to the GoogleMap element.
*/
addMarker = (latlng) => {
let newMarker = [{
position: { lat: latlng.lat(), lng: latlng.lng() },
label: 'New',
map: null,
}];
/* Creates a new array for updating state. Is this the best way to do this */
let markers = [...this.state.markers, ...newMarker]
this.setState({markers});
console.log(this.state) // This shows the added marker
}
render() {
return (
<div className="container">
<h4 className="center">About</h4>
<MapMarkerDetails details={this.state.greeting}/>
<GoogleMap markers={this.state.markers} clickedMarker={this.showMarkerInfo} addMarker={this.addMarker}/>
</div>
)
}
}
export default About;
Here is the class that displays Google Map and the markers:
import React, { Component } from 'react';
class GoogleMap extends Component {
constructor(props) {
super(props);
this.googleMapRef = React.createRef(); // Create a referance for Google Map to draw to
console.log('Constructore')
}
componentDidMount(){
console.log('componentDidMount')
/* Create the Map */
let googleMap = new window.google.maps.Map(this.googleMapRef.current, {
zoom: 15,
center: {
lat: 51.436411,
lng: -2.861980,
},
disableDefaultUI: true,
})
this.placeMMarkers(googleMap) // Place the markers
this.addMapListner(googleMap) // Define a click listener to place new markers
}
/* Place the markers */
placeMMarkers = (googleMap) => {
this.props.markers.forEach((m) => {
m.map = googleMap;
let marker= new window.google.maps.Marker(m)
marker.addListener('click', () => { this.props.clickedMarker(m.label); });
}
);
}
/* Map listeners */
addMapListner = (googleMap) => {
googleMap.addListener('click', (e) => {
this.props.addMarker(e.latLng)
})
}
render() {
console.log('render: ' + this.props.markers) // This is showing the added markers
return (
<div
id="google-map"
ref={this.googleMapRef}
style={{ width: '800px', height: '400px', float: 'left' }}>
</div>
)
}
}
export default GoogleMap
I've added console logging to each function so I can follow what is happening.
Here is the MapMarkerDetails which displays a simple message when an marker is clicked. This all works fine.
import React, { Component } from 'react';
class MapMarkerDetails extends Component {
render(){
return (
<div style={{width: '100px', height: '400px', backgroundColor: 'gray', float: 'left'}}>
{this.props.details}
</div>
)
}
}
export default MapMarkerDetails
Description of the Problem
When the user clicks on the map (not a marker) this invokes the function addMarker which is passed down from the parent About class (snippet below). In the addMarker function of About the lat/lng is passed in. This represents where the user clicked. This is converted into a marker data object, then a new array is created which contains the default markers and the new one. I'm not sure if my new array creation is done in the best way - if not let me know.
Once the new array is created, we update the components state with this.setState({markers}). I thought this would lead to a re-render() and an redrawing of the map with the added marker. But not.
addMarker = (latlng) => {
let newMarker = [{
position: { lat: latlng.lat(), lng: latlng.lng() },
label: 'New',
map: null,
}];
/* Creates a new array for updating state. Is this the best way to do this */
let markers = [...this.state.markers, ...newMarker]
this.setState({markers});
console.log(this.state) // This shows the added marker
}
Something happens that results in the render() function of GoogleMap being called, but only the original markers are shown. The data is passed down to the GoogleMap component because I can see the output of console.log('render: ' + this.props.markers). But how do I get ALL the markers to load?
Please advise on what is the best way to for About to pass data to GoogleMap such that it can add in the new marker.
Just like you use componentDidMount to imperatively add the markers when the map is first loaded, you should use componentDidUpdate to do the same thing when the props change. In your GoogleMap component:
componentDidUpdate() {
this.placeMMarkers()
}
Rather than passing googleMap as an argument, I would set it as an instance variable in componentDidMount:
this.googleMap = new window.google.maps.Map(...
and then change placeMMarkers to use this.googleMap:
placeMMarkers = () => {
this.props.markers.forEach((m) => {
m.map = this.googleMap;
// ...
Since you are attaching an event handler in placeMMarkers, you should also add some logic to distinguish between new markers and existing ones, to avoid adding multiple event handlers to existing markers.
In response to your question about how best to set the state, I think what you've got is fine but you don't need to put the new marker inside an array:
let newMarker = {
position: { lat: latlng.lat(), lng: latlng.lng() },
label: 'New',
map: null,
};
let markers = [...this.state.markers, newMarker]

React-mapbox gl accessing Map's "zoom" in a stateless functional component

I need to be able to access the current "Zoom" level of the map for dynamically rendering content. Is there a way I can do this in a stateless functional component? I have seen this question, which only applies to a class-component.
I've looked at current issue and tried giving the map to props, neither of those solutions seem to work.
const Map = ReactMapboxGl({
accessToken: AT
});
const MapPortion = (props) => {
const [mapOptions, setMapOptions] = useState({zoom: [4]});
const someZoomFunction = () => {
// do stuff to setMapOptions({})
}
return (
<Map
zoom={zoom}
containerStyle={{
height: "90%",
width: "100%"
}}
/>
)
}
I'd like to not have to rewrite everything I have just to access zoom. Thanks in advance!
Resolved this. See this issue: https://github.com/alex3165/react-mapbox-gl/issues/763

Possible to render react component within mapboxgl.Popup() in .setHTML or .setDOMContent?

I am wondering if it is possible to render a react component within a mapboxgl.Popup(). Something like this:
componentDidMount() {
new mapboxgl.Popup()
.setLngLat(coordinates)
.setHTML(`<div>${<MapPopup />}<p>${moreText}</p></div>`)
//.setDOMContent(`${<MapPopup />}`) ?????
.addTo(this.props.mapboxMap)
})
Or should this be done using ReactDOM.render?
ReactDOM.render(<MapPopup />, document.getElementById('root'))
This project will have buttons and inputs in the popup that connect to a redux store.
Thanks for any input!
This works:
addPopup(el: JSX.Element, lat: number, lng: number) {
const placeholder = document.createElement('div');
ReactDOM.render(el, placeholder);
const marker = new MapboxGl.Popup()
.setDOMContent(placeholder)
.setLngLat({lng: lng, lat: lat})
.addTo(map);
}
(Where I've used typescript to illustrate types, but you can just leave these out for pure js.) Use it as
addPopup(<h1>Losers of 1966 World Cup</h1>, 52.5, 13.4);
You can try to implement React component:
export const Popup = ({ children, latitude, longitude, ...mapboxPopupProps }) => {
// this is a mapbox map instance, you can pass it via props
const { map } = useContext(MapboxContext);
const popupRef = useRef();
useEffect(() => {
const popup = new MapboxPopup(mapboxPopupProps)
.setLngLat([longitude, latitude])
.setDOMContent(popupRef.current)
.addTo(map);
return popup.remove;
}, [children, mapboxPopupProps, longitude, latitude]);
return (
/**
* This component has to have 2 divs.
* Because if you remove outter div, React has some difficulties
* with unmounting this component.
* Also `display: none` is solving that map does not jump when hovering
* ¯\_(ツ)_/¯
*/
<div style={{ display: 'none' }}>
<div ref={popupRef}>
{children}
</div>
</div>
);
};
After some testing, I have realized that Popup component was not rendering properly on the map. And also unmounting the component was unsuccessful. That is why there are two divs in return. However, it may happen only in my environment.
See https://docs.mapbox.com/mapbox-gl-js/api/#popup for additional mapboxPopupProps
useEffect dependencies make sure that MapboxPopup gets re-created every time something of that list changes & cleaning up the previous popup instance with return popup.remove;
I've been battling with this as well. One solution I found was using ReactDOM.render(). I created an empty popup then use the container generated by mapboxgl to render my React component.
marker.setPopup(new mapboxgl.Popup({ offset: 18 }).setHTML(''));
markerEl.addEventListener('mouseenter', () => {
markerEl.classList.add('enlarge');
if (!marker.getPopup().isOpen()) {
marker.getPopup().addTo(this.getMap());
ReactDOM.render(
component,
document.querySelector('.mapboxgl-popup-content')
);
}
});
const mapCardNode = document.createElement("div");
mapCardNode.className = "css-class-name";
ReactDOM.render(
<YourReactPopupComponent / > ,
mapCardNode
);
//if you have a popup then we remove it from the map
if (popupMarker.current) popupMarker.current.remove();
popupBox.current = new mapboxgl.Popup({
closeOnClick: false,
anchor: "center",
maxWidth: "240px",
})
.setLngLat(coordinates)
.setDOMContent(mapCardNode)
.addTo(map);
I used MapBox GL's map and popup events (to improve upon #Jan Dockal solution) which seemed to improve reliability. Also, removed the extra div wrapper.
import { useWorldMap as useMap } from 'hooks/useWorldMap'
import mapboxgl from 'mapbox-gl'
import { FC, useRef, useEffect } from 'react'
export const Popup: FC<{
layerId: string
}> = ({ layerId, children }) => {
const map = useMap() // Uses React Context to get a mapboxgl map (could possibly be null)
const containerRef = useRef<HTMLDivElement>(null)
const popupRef = useRef<mapboxgl.Popup>()
const handleClick = (
e: mapboxgl.MapMouseEvent & {
features?: mapboxgl.MapboxGeoJSONFeature[] | undefined
} & mapboxgl.EventData
) => {
// Bail early if there is no map or container
if (!map || !containerRef.current) {
return
}
// Remove the previous popup if it exists (useful to prevent multiple popups)
if (popupRef.current) {
popupRef.current.remove()
popupRef.current = undefined
}
// Create the popup and add it to the world map
const popup = new mapboxgl.Popup()
.setLngLat(e.lngLat) // could also use the coordinates from a feature geometry if the source is in geojson format
.setDOMContent(containerRef.current)
.addTo(map)
// Keep track of the current popup
popupRef.current = popup
// Remove the tracked popup with the popup is closed
popup.on('close', () => {
popupRef.current = undefined
})
}
useEffect(() => {
if (map && layerId) {
// Listen for clicks on the specified layer
map?.on('click', layerId, handleClick)
// Clean up the event listener
return () => {
map?.off('click', layerId, handleClick)
popupRef.current?.remove()
popupRef.current = undefined
}
}
}, [map, layerId])
return <div ref={containerRef}>{children}</div>
}
Try to do with onClick event, instead of creating a button. After that put your react component in onClick events add event listener refrence link
[1]: https://stackoverflow.com/a/64182029/15570982

Categories

Resources