I am using google-maps-react module and everything until the last stretch is fine. Using this package, I am able to click on map, set multiple markers and define my route "visually". I did not think that Polygon would not take actual streets and map geometry into consideration (stupid me). Could someone help me out and suggest on how to provide properly drawn routes on map, instead of straight lines connecting marker X to marker Y? This is what I have so far visually:
And this is the coordinates array that I am forming in my application and drawing polygon by:
I am using Google maps api and google-maps-react package.
As was correctly mentioned in comment, Directions API needs to be utilized for that purpose:
Directions are displayed as a polyline drawing the route on a map, or
additionally as a series of textual description within a element
(for example, "Turn right onto the Williamsburg Bridge ramp")
The following example demonstrates how to integrate Google Maps API Directions Service into google-maps-react to display a route.
It is assumed data prop contains coordinates represented in format
specified in provided question. Directions Service code has been adapted
from this example
Example
class MapContainer extends React.Component {
constructor(props) {
super(props);
this.handleMapReady = this.handleMapReady.bind(this);
}
handleMapReady(mapProps, map) {
this.calculateAndDisplayRoute(map);
}
calculateAndDisplayRoute(map) {
const directionsService = new google.maps.DirectionsService();
const directionsDisplay = new google.maps.DirectionsRenderer();
directionsDisplay.setMap(map);
const waypoints = this.props.data.map(item =>{
return{
location: {lat: item.lat, lng:item.lng},
stopover: true
}
})
const origin = waypoints.shift().location;
const destination = waypoints.pop().location;
directionsService.route({
origin: origin,
destination: destination,
waypoints: waypoints,
travelMode: 'DRIVING'
}, (response, status) => {
if (status === 'OK') {
directionsDisplay.setDirections(response);
} else {
window.alert('Directions request failed due to ' + status);
}
});
}
render() {
return (
<div className="map-container">
<Map
google={this.props.google}
className={"map"}
zoom={this.props.zoom}
initialCenter={this.props.center}
onReady={this.handleMapReady}
/>
</div>
);
}
}
Demo
You might want to check out this page
https://developers.google.com/maps/documentation/roads/snap
The response from that API call with the data you've already gotten from the lines drawn should give you the JSON data you need to map your path to the roads. This might not be the most elegant solution seeing as you might need to add a button or something to calculate the route to roads or something similar.
Alternatively you might be able to send out the API call after you have two points and have the map update after every line segment is placed. That would require a lot of API calls though. Hope that helps!
Related
I am attempting to get the elevation of certain coordinates on the map using mapbox.
Based on the documentation, i can use queryTerrainElevation.
Sample Code:
map.on("click", (data) => {
console.log(data);
const elevation = map?.queryTerrainElevation(data.lngLat, {
exaggerated: true,
});
console.log("Elevation: " + elevation)
});
Console logs:
Using the mapbox tilequery with the same coordinates:
https://api.mapbox.com/v4/mapbox.mapbox-terrain-v2/tilequery/95.9345,41.2565.json?access_token=<mapbox_token>
There is an elevation value in the response:
You must be adding the custom layer after just loading the style. So, the terrain value isn't updated at that time. That's why you get null.
Do it like this and it should work.
map.on('idle', () => {
const elevation = map.queryTerrainElevation(coordinates, {exaggerated: false});
})
This runs the layer code after the map is loaded. Instead of just the style.
I am currently working on a project and have tried multiple times to include clickable markers into my map. I have use google-maps-react library, however I have not managed to get a marker showing information and being able to click on a button on this information window. I have read that Marker by default isn't clickable but is there an other way to do so ? or are there any tutorials or code that explain so
thank you in advance...
You need to use .addListener() on a marker and tell it to open an infowindow (or whichever action you want to take on marker click).
You can find an example in the Google API documentation here
// Add a marker to the map
const marker = new google.maps.Marker({
// position of the marker on map
position: { lat: 53.3, lng: -6.3 },
// the map object you want the marker on
map: map,
});
// add a listener to the marker which listens for a 'click'
// and runs the callback when it 'hears' one
marker.addListener('click', () => {
// set the content of the info window. This can be formatted using HTML
infowindow.setContent('I have been clicked!');
// tells the infowindow on which map to open and which marker to anchor to
infowindow.open(map, marker);
});
Edit: According to the guideline, there is a method called getMetadata(). How can I use this to return the props?
Description:
Using the React-google-maps package, I've been able to load Google Maps with my own kml file. That KML file contains multiple shapes each with metadata behind it. What I want is that when a user clicks on one of these shapes, he sees the data behind the shape in for example a pop up.
Example
Let's say I have a Google map with a kml file that shows two countries. The user hovers over one of these countries and sees a pop up that shows which country he's over over. He hovers over the second country and gets the same. When he clicks on the kml shape over the country, he receives more information.
This requires me to do know few things:
- How to create a hover effect over the KML shape that shows data based on the shape
- How to create a click event on the KML shape that shows data based on the shape
However, I fail to understand how I can make this KML file interactive.
This is what I have until now:
import React, { Component } from 'react';
import { withScriptjs, withGoogleMap, GoogleMap, Marker, KmlLayer } from "react-google-maps"
const MyMapComponent = withScriptjs(withGoogleMap((props) =>
<GoogleMap
defaultZoom={8}
defaultCenter={{ lat: 50.5010789, lng: 4.4764595 }}
>
<KmlLayer
url='https://example.be/kmlfile.kml'
options={{ preserveViewport : false}}
/>
{props.isMarkerShown && <Marker position={{ lat: 50.5010789, lng: 4.4764595 }} />}
</GoogleMap>
))
export default class GoogleMaps extends Component {
render(){
return(
<MyMapComponent
isMarkerShown
googleMapURL="https://maps.googleapis.com/maps/api/js?key=MYKEY&v=3.exp&libraries=geometry,drawing,places"
loadingElement={<div style={{ height: `100%` }} />}
containerElement={<div style={{ height: `100%` }} />}
mapElement={<div style={{ height: `100%` }} />}
/>
)
}
}
So I've fixed this my way following a few tutorials. I'm completely ignoring the react-google-maps package and I'm just using plain Javascript. Anyone who's looking for a way to add & switch between KMLayers and to add click and hover actions to it, this is how I did it and is my advice to fellow developers:
TIPS
1: Replace KML by GEOJSON
First of all, I am not using KMLayer but Datalayer now. This allows me more control and has much more documentation on Google. Therefore, you have to convert your KML to a GeoJson. I find that #Mapbox toGeoJSON does a fine job as it also keeps your custom data (which is very important if you want to have more freedom with your data!). Plus they also share their code to integrate in your application so you don't have to convert manually each time.
2: Analyze Google Api Data Layer documentation
Sounds straightforward, but still worth mentioning. As I said, Google shares a lot of information about implementing Data Layer. How to add click and mouseover events, how to style each individual shape and get that specific information, ...
Google doc on Data Layer
3: Replace loadGeoJson() by addGeoJson()
If your applications needs to switch between different Data Layers or simply has to add and remove one, you'll quickly find yourself stuck when using the loadGeoJson(). Hence the addGeoJson(), which will allow you to use map.data.remove() to remove the current Data layer.
Credit: #mensi for his answer on how to remove Data Layer
FINAL CODE
import React, { Component } from 'react';
import { SearchConsumer } from '../App.js';
import Icon from '../library/icons/Icon';
var map = ''
var dataLayer = ''
export default class mapSelection extends Component {
constructor(props){
super(props)
this.onScriptLoad = this.onScriptLoad.bind(this)
}
onScriptLoad() {
// CREATE YOUR GOOGLE MAPS
map = new window.google.maps.Map(
document.getElementById('map'),
{
// ADD OPTIONS LIKE STYLE, CENTER, GESTUREHANDLING, ...
center: { lat: 50.5, lng: 4 },
zoom: 8,
gestureHandling: 'greedy',
disableDefaultUI: true,
});
}
dataHandler = (getJson) => {
// FIRST I REMOVE THE CURRENT LAYER (IF THERE IS ONE)
for (var i = 0; i < dataLayer.length; i++) {
map.data.remove(dataLayer[i])
}
// THEN I FETCH MY JSON FILE, IN HERE I'M USING A PROP BECAUSE
// I WANT TO USE THIS DATAHANDLER MULTIPLE TIMES & DYNAMICALLY
// I CAN NOW DO SOMETHING LIKE THIS:
// onClick(this.dataHandler(www.anotherlinktojsonfile.com/yourjsonfile.json))
// ON EACH BUTTON AND CHOOSE WHICH JSON FILE NEEDS TO BE FETCHED IN MY DATAHANDLER.
fetch(getJson)
.then(response => response.json())
.then(featureCollection => {
dataLayer = map.data.addGeoJson(featureCollection)
// ADD SOME NEW STYLE IF YOU WANT TO
map.data.setStyle({strokeWeight: 0.5, fillOpacity: 0 });
}
);
map.data.addListener('mouseover', (event) => {
map.data.revertStyle();
// ADD A STYLE WHEN YOU HOVER OVER A SPECIFIC POLYGON
map.data.overrideStyle(event.feature, {strokeWeight: 1, fillOpacity: 0.1 });
// IN CONSOLE LOG, YOU CAN SEE ALL THE DATA YOU CAN RETURN
console.log(event.feature)
});
map.data.addListener('mouseout', (event) => {
// REVERT THE STYLE TO HOW IT WAS WHEN YOU HOVER OUT
map.data.revertStyle();
});
}
componentDidMount() {
// LOADING THE GOOGLE MAPS ITSELF
if (!window.google) {
var s = document.createElement('script');
s.type = 'text/javascript';
s.src = 'https://maps.google.com/maps/api/js?key=' + process.env.REACT_APP_MAPS_API_KEY;
var x = document.getElementsByTagName('script')[0];
x.parentNode.insertBefore(s, x);
// Below is important.
//We cannot access google.maps until it's finished loading
s.addEventListener('load', e => {
this.onScriptLoad()
this.dataHandler('https://linktoyourjson.com/yourjsonfile.json')
})
} else {
this.onScriptLoad()
}
}
render () {
return (
<div id='mapContainer'>
<div style={{ width: '100%', height: '100%' }} id='map' />
</div>
);
}
};
EXTRA CREDITS
I also want to thank cuneyt.aliustaoglu.biz for his wel explained tutorial in using Google Maps wihout any package.
And thanks to everyone who helped me with a minor problem
QUESTIONS OR SUGGESTIONS?
If there are any questions or if I missed something, you can always ask or tell me and I'll edit this post if necessary.
I'm attempting to completely recreate or reorganize the functionality of the LayersControl component in its own separate panel using react-leaflet.
I have several filtered into their own and it works fine, but I'd like to customize the look and location of the Control element.
I've hosted the current version of my Leaflet app on github pages here. You can see the control on the right, which is the basic Leaflet control, but I'd like to the Icon on the left (the layers icon) to accomplish the same thing instead with custom react components.
Just wondering if anyone can point me in the right direction to beginning to accomplish this!
This is my current render for my react-leaflet map:
render() {
const types = [...new Set(data.map(loc => loc.type))];
const group = types.map(type =>
data.filter(loc => loc.type === type)
.map(({id, lat, lng, name}) =>
<LayersControl.Overlay name={startCase(toLower(type))}>
<LayerGroup>
<Marker key={id} position={[lat, lng]} icon=
{locationIcon}>
<Tooltip permanent direction="bottom" opacity={.6}>
{name}
</Tooltip>
</Marker>
</LayerGroup>
</LayersControl.Overlay>
));
return (
<>
<ControlPanel />
<Map
zoomControl={false}
center={this.state.center}
zoom={this.state.zoom}
maxBounds={this.state.maxBounds}
maxZoom={10}
>
<LayersControl>
<TileLayer
url='https://cartocdn-gusc.global.ssl.fastly.net//ramirocartodb/api/v1/map/named/tpl_756aec63_3adb_48b6_9d14_331c6cbc47cf/all/{z}/{x}/{y}.png'
/>
<ZoomControl position="topright" />
{group}
</LayersControl>
</Map>
</>
);
}
So theres still a few bugs in this but i've managed get most of the way (self taught react) using material UI as an example, can be seen in this sandbox link:
https://codesandbox.io/embed/competent-edison-wt5pl?fontsize=14
The general bassis is that we extend MapControl which means we have to define createLeafletElement, this has to return a generic leaflet (not react) control from the original javascript leaflet package. Essentially making a div with the domutil provided by leaflet and then portaling our react components through that div with react portals.
Again with another class extension we extend some of the classes provided by react-leaflet for layers, i pulled it out and just made a generic layer that you could define a group for, that way you could render any layer (polygon, baselayer etc) and specify the group to tell it where to go in the layer control i.e no need for specific components or overlays. As we are extending the class we need implement and pass down the methods we want to use, like addLayer, remove layer etc. During these implementations i've just added them to state to track what layers are active and such.
Not sure if there are better practices throughout everything i've implemented but this is definitely a start, hopefully in the right direction.
Bugs - The first layer in each group won't turn on correctly without the 2nd item ticked, something to do with state i think but didn't have the time to track it down
Thanks Dylan and Peter for this nice React Leaflet custom control approach. I assumed there was still a bug in the toggleLayer function. It's checked multiple checkboxes and the layers won't change properly. So I restructered a little bit and now it should work fine.
toggleLayer = layerInput => {
const { name, group } = layerInput;
let layers = { ...this.state.layers };
layers[group] = layers[group].map(l => {
l.checked = false;
this.removeLayer(l.layer);
if (l.name === name) {
l.checked = !l.checked;
this.props.leaflet.map.addLayer(l.layer);
}
return l;
});
this.setState({
layers
});
};
Just to elaborate on the bug that is mentioned in Dylans answer...
If you have more then one ControlledLayerItem, none items are added to the map until the very last item is checked. To fix this, the toggleLayer method in ControlLayer2.js has to be slightly modified:
toggleLayer = layerInput => {
const { layer, name, checked, group } = layerInput;
let layers = { ...this.state.layers };
layers[group] = layers[group].map(l => {
if (l.name === name) {
l.checked = !l.checked;
l.checked
? this.props.leaflet.map.addLayer(layer)
: this.removeLayer(layer);
}
return l;
});
this.setState({
layers
});
};
Thanks Dylan for the code, it was really helpfull.
What I want to achieve:
Have a <Map><FeatureGroup><Circle />[1 or more]...</FeatureGroup></Map> hierarchy and fit the map bounds to the feature group so that all the circles are in the viewport.
If there is only one circle, it should fit the bounds (ie: zoom in on) to that circle.
What I've tried:
giving FeatureGroup a ref and calling getBounds on it to pass onto Map. Because of the lifecycle FeatureGroup doesn't exist at the time componentDidMount is called - it gets rendered later (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328).
Storing Circle in state and calling getBounds on that (assuming, in this case, that there is only one circle. That didn't work either.
I think I might need to do something with the React Context but I'm not sure that I fully understand it right now, so I need some help.
Other information
I'm using react-leaflet#2.1.2
Thanks for any help offered!
Because the contents of the Map are unavailable at componentDidMount-time (https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-161594328) you cannot get the bounds of the FeatureGroup at that point, and out of all the refs you assign, only the Map ref will be available in this.refs.
However, as per this GitHub comment: https://github.com/PaulLeCam/react-leaflet/issues/106#issuecomment-366263225 you can give a FeatureGroup an onAdd handler function:
<FeatureGroup ref="features" onAdd={this.onFeatureGroupAdd}>...
and you can then use the Map refs to access the leafletElement and call fitBounds with the bounds of the incoming event target, which will be the FeatureGroup:
onFeatureGroupAdd = (e) => {
this.refs.map.leafletElement.fitBounds(e.target.getBounds());
}
This will then "zoom" the map into the bounds of your FeatureGroup, as desired.
Update
I modified my React component so that zoom and centre are controlled by query parameters. The problem with the above solution was that if you zoomed in on a MarkerClusterGroup by clicking on it, for example, it would update the zoom in the url, re-render the map and re-call onFeatureGroupAdd, thus undoing all the marker cluster goodness.
What I needed was to access the zoom level required to keep the newly drawn circle nicely in bounds, then update the url with the correct zoom level and center.
onDrawCircle = (e) => {
...
var targetZoom = this.refs.map.leafletElement.getBoundsZoom(e.layer.getBounds());
// Call function to update url here:
functionToUpdateUrl(targetZoom, e.layer.getBounds().getCenter());
}
}
In order to be able to control the whole map I also call functionToUpdateUrl in onZoomEnd and onDragEnd event handlers, like so:
onChangeView = (e) => {
functionToUpdateUrl(e.target._zoom, this.refs.map.leafletElement.getCenter());
}
and one for handling cluster clicks:
onClusterClick = (e) => {
// This time we want the center of the layer, not the map?
functionToUpdateUrl(e.target._zoom, (e.layer ? e.layer.getBounds().getCenter() : e.target.getBounds().getCenter()));
}
Then, when rendering the Map element, pass these properties:
<Map
center={center}
ref='map'
zoom={zoom}
maxZoom={18}
onZoomEnd={this.onChangeView}
onDragEnd={this.onChangeView}
>
....
</Map>
And remember to give any MarkerClusterGroups their onClusterClick callback:
<MarkerClusterGroup onAdd={this.onMarkerGroupAdd} onClusterClick={this.onClusterClick}>
Have you tried doing getBounds in the componentDidMount function instead of componentWillMount? If that doesn't work then I'd suggest extending the FeatureGroup component and adding an onLoaded function as as prop and call that function in the componentDidMount function of your extended component. And by extending the FeatureGroup component I actually mean copying/pasting it from here. (if you care about why you need to copy that whole file check this thread)
This isn't tested but your code will probably look something like
import { FeatureGroup } from 'leaflet';
import { withLeaflet, Path } from 'react-leaflet';
class CustomFeatureGroup extends Path {
createLeafletElement(prop) {
const el = new FeatureGroup(this.getOptions(props));
this.contextValue = {
...props.leaflet,
layerContainer: el,
popupContainer: el,
};
return el;
}
componentDidMount() {
super.componentDidMount();
this.setStyle(this.props);
/*
Here you can do your centering logic with an onLoad callback or just
by using this.leafletElement.map or whatever
*/
this.props.onLoaded();
}
}
export default withLeaflet(CustomFeatureGroup)
Note: If you are using react-leaflet V1 this is actually way easier and I can edit this answer with that code if needed.