React component built on top of React-Leaflet and Google Map Layer with controls for drawing figures and markers
Install
npm install react-leaflet-draw
npm install leaflet
npm install react-leaflet
npm install react-leaflet-google-layer
npm install lodash-es
index.html
<head>
...
<!-- MAP -->
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.2/normalize.min.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/leaflet/1.3.1/leaflet.css" />
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.3/leaflet.draw.css" />
...
</head>
map.css
.leaflet-container {
width: 100%;
height: 50vh;
}
EditControl.js
import { PropTypes } from 'prop-types';
import isEqual from 'lodash-es/isEqual';
import { MapControl, withLeaflet } from 'react-leaflet';
import leaflet, { Map, Control } from 'leaflet';
const eventHandlers = {
onEdited: 'draw:edited',
onDrawStart: 'draw:drawstart',
onDrawStop: 'draw:drawstop',
onDrawVertex: 'draw:drawvertex',
onEditStart: 'draw:editstart',
onEditMove: 'draw:editmove',
onEditResize: 'draw:editresize',
onEditVertex: 'draw:editvertex',
onEditStop: 'draw:editstop',
onDeleted: 'draw:deleted',
onDeleteStart: 'draw:deletestart',
onDeleteStop: 'draw:deletestop',
};
class EditControl extends MapControl {
static propTypes = {
...Object.keys(eventHandlers).reduce((acc, val) => {
acc[val] = PropTypes.func;
return acc;
}, {}),
onCreated: PropTypes.func,
onMounted: PropTypes.func,
draw: PropTypes.shape({
polyline: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
polygon: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
rectangle: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
circle: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
marker: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
}),
edit: PropTypes.shape({
edit: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
remove: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
poly: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
allowIntersection: PropTypes.bool,
}),
position: PropTypes.oneOf([
'topright',
'topleft',
'bottomright',
'bottomleft'
]),
leaflet: PropTypes.shape({
map: PropTypes.instanceOf(Map),
layerContainer: PropTypes.shape({
addLayer: PropTypes.func.isRequired,
removeLayer: PropTypes.func.isRequired
})
})
};
createLeafletElement(props) {
return createDrawElement(props);
}
onDrawCreate = (e) => {
const { onCreated } = this.props;
const { layerContainer } = this.props.leaflet;
layerContainer.addLayer(e.layer);
onCreated && onCreated(e);
};
componentDidMount() {
super.componentDidMount();
const { map } = this.props.leaflet;
const { onMounted } = this.props;
for (const key in eventHandlers) {
if (this.props[key]) {
map.on(eventHandlers[key], this.props[key]);
}
}
map.on(leaflet.Draw.Event.CREATED, this.onDrawCreate);
onMounted && onMounted(this.leafletElement);
}
componentWillUnmount() {
super.componentWillUnmount();
const { map } = this.props.leaflet;
map.off(leaflet.Draw.Event.CREATED, this.onDrawCreate);
for (const key in eventHandlers) {
if (this.props[key]) {
map.off(eventHandlers[key], this.props[key]);
}
}
}
componentDidUpdate(prevProps) {
// super updates positions if thats all that changed so call this first
super.componentDidUpdate(prevProps);
if (isEqual(this.props.draw, prevProps.draw) || this.props.position !== prevProps.position) {
return false;
}
const { map } = this.props.leaflet;
this.leafletElement.remove(map);
this.leafletElement = createDrawElement(this.props);
this.leafletElement.addTo(map);
return null;
}
}
function createDrawElement(props) {
const { layerContainer } = props.leaflet;
const { draw, edit, position } = props;
const options = {
edit: {
...edit,
featureGroup: layerContainer
}
};
if (draw) {
options.draw = { ...draw };
}
if (position) {
options.position = position;
}
return new Control.Draw(options);
}
export default withLeaflet(EditControl);
LeafletMap.js
import React, { Component } from 'react';
import { Map, TileLayer, Circle, FeatureGroup, Marker, Popup, LayersControl } from 'react-leaflet';
import "./map.css";
import L from 'leaflet';
import EditControl from './EditControl';
import ReactLeafletGoogleLayer from "react-leaflet-google-layer";
// work around broken icons when using webpack, see https://github.com/PaulLeCam/react-leaflet/issues/255
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0/images/marker-icon.png',
iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0/images/marker-icon.png',
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.0.0/images/marker-shadow.png',
});
export default class LeafletMap extends Component {
// see http://leaflet.github.io/Leaflet.draw/docs/leaflet-draw-latest.html#l-draw-event for leaflet-draw events doc
_onEdited = (e) => {
let numEdited = 0;
e.layers.eachLayer((layer) => {
numEdited += 1;
});
console.log(`_onEdited: edited ${numEdited} layers`, e);
this._onChange();
}
_onCreated = (eobjCreated) => {
let type = eobjCreated.layerType;
let layer = eobjCreated.layer;
if (type === 'marker') {
// Do marker specific actions
console.log("_onCreated: marker created", eobjCreated);
}
else {
console.log("_onCreated: something else created:", type, eobjCreated);
}
this._onChange();
}
_onDeleted = (e) => {
let numDeleted = 0;
e.layers.eachLayer((layer) => {
numDeleted += 1;
});
console.log(`onDeleted: removed ${numDeleted} layers`, e);
this._onChange();
}
_onMounted = (drawControl) => {
console.log('_onMounted', drawControl);
}
_onEditStart = (e) => {
console.log('_onEditStart', e);
}
_onEditStop = (e) => {
console.log('_onEditStop', e);
}
_onDeleteStart = (e) => {
console.log('_onDeleteStart', e);
}
_onDeleteStop = (e) => {
console.log('_onDeleteStop', e);
}
render() {
return (
<>
<Map
center={[38.132060, 13.331277]}
zoom={12}
>
<TileLayer
attribution='© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"
/>
{/* watercolor Style
<TileLayer
url="http://a.tile.stamen.com/watercolor/{z}/{x}/{y}.jpg"
attribution='© <a id="home-link" target="_top" href="../">Map tiles</a> by <a target="_top" href="http://stamen.com">Stamen Design</a>, under <a target="_top" href="http://creativecommons.org/licenses/by/3.0">CC BY 3.0</a>. Data by <a target="_top" href="http://openstreetmap.org">OpenStreetMap</a>, under <a target="_top" href="http://creativecommons.org/licenses/by-sa/3.0">CC BY SA</a>'
/> */}
{/*
GIS Style
<TileLayer
url="https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}.png"
attribution='© <a href="Esri &mdash">Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community</a> contributors'
/>
*/}
<FeatureGroup ref={(reactFGref) => { this._onFeatureGroupReady(reactFGref); }}>
<EditControl
position='topright'
onEdited={this._onEdited}
onCreated={this._onCreated}
onDeleted={this._onDeleted}
onMounted={this._onMounted}
onEditStart={this._onEditStart}
onEditStop={this._onEditStop}
onDeleteStart={this._onDeleteStart}
onDeleteStop={this._onDeleteStop}
edit={{
remove: true,
edit: true
}}
draw={{
rectangle: true,
polyline: true,
polygon: true,
circle: true,
marker: true,
polygon: {
allowIntersection: false,
shapeOptions: { color: "red" },
edit: false,
showLength: true,
metric: false,
feet: false,
showArea: true
},
circle: {
shapeOptions: { color: "blue" },
showLength: true,
metric: false,
feet: false,
showArea: true
},
}}
/>
</FeatureGroup>
<ReactLeafletGoogleLayer
googleMapsLoaderConf={{ KEY: "YOUR_TOKEN_GOOGLE_MAP" }}
type={"satellite"}
/>
</Map>
</>
);
}
_editableFG = null
_onFeatureGroupReady = (reactFGref) => {
// populate the leaflet FeatureGroup with the geoJson layers
let leafletGeoJSON = new L.GeoJSON(getGeoJson());
let leafletFG = reactFGref.leafletElement;
leafletGeoJSON.eachLayer((layer) => {
leafletFG.addLayer(layer);
});
// store the ref for future access to content
this._editableFG = reactFGref;
}
_onChange = () => {
// this._editableFG contains the edited geometry, which can be manipulated through the leaflet API
const { onChange } = this.props;
if (!this._editableFG || !onChange) {
return;
}
const geojsonData = this._editableFG.leafletElement.toGeoJSON();
onChange(geojsonData);
}
}
// data taken from the example in https://github.com/PaulLeCam/react-leaflet/issues/176
function getGeoJson() {
return {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "LineString",
"coordinates": [
[
13.25441561846926,
38.162839676288336
],
[
13.269521819641135,
38.150961340209484
],
[
13.250982390930197,
38.150961340209484
],
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
13.250982390930197,
38.150961340209484
],
[
13.269521819641135,
38.150961340209484
],
[
13.25441561846926,
38.162839676288336
],
[
13.24342929034426,
38.150691355539586
]
]
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
13.277074920227072,
38.18335224460118
],
[
13.30660067706301,
38.18389197106355
],
[
13.278104888488791,
38.165808957979515
]
]
]
}
},
{
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
13.309476005126974,
38.13233005362,
],
[
13.337628470947287,
38.135030534863766
],
[
13.31805907397463,
38.11153300139878
]
]
]
}
}
]
}
}