I can't seem to figure out how to properly update the coords of individual markers after storing them in the state. How it works currently is that when you click somewhere on the map, it adds a marker and stores its initial position in the state (in markerData) which is then displayed on the map through a map function. You can move the individual markers around but I'm having difficulty figuring out a possible solution to updating the specific markers position so that eventually I can send and store the marker information in the back end.
Here is my current code.
import React, { Component } from 'react';
import { ImageOverlay, Map, Marker, Popup } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import 'leaflet/dist/leaflet.js';
import L from 'leaflet';
delete L.Icon.Default.prototype._getIconUrl;
L.Icon.Default.mergeOptions({
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
});
export default class CustomMapExample extends Component {
constructor(props) {
super(props);
this.state = {
center: {
lat: 512,
lng: 1024,
},
zoom: 1,
draggable: true,
dimensions: [1024, 2048],
markerData: [],
};
}
toggleDraggable = () => {
this.setState({ draggable: !this.state.draggable })
}
addMarker = (event) => {
const {markerData} = this.state
const coords = event.latlng
markerData.push(coords)
this.setState({markerData})
}
updateMarker = (event) => {
console.log(event)
}
render () {
const boundOrigin = [0, 0];
const bounds = [boundOrigin, this.state.dimensions];
const position = [this.state.center.lat, this.state.center.lng]
return (
<div>
<Map
id="map"
crs={L.CRS.Simple}
minZoom={-1}
bounds={bounds}
center={position}
zoom={this.state.zoom}
onClick={this.addMarker}
>
<ImageOverlay
url='http://www.online-tabletop.com/wp-content/uploads/2017/01/tutoriala.jpg'
bounds={bounds}
/>
{this.state.markerData.map((element, index) =>
<Marker
key={index}
position={element}
draggable={this.state.draggable}
onDragend={this.updateMarker}
>
<Popup>
<span onClick={this.toggleDraggable}>
{this.state.draggable ? `Hello` : 'MARKER FIXED'}
</span>
</Popup>
</Marker>
)}
</Map>
</div>
);
}
}
Since Marker accepts options object as a second parameter, marker index could be introduced to reference which marker is getting updated:
<Marker
key={index}
marker_index={index}
position={element}
draggable={this.state.draggable}
onDragend={this.updateMarker}
/>
And then markerData state could be updated like this once the marker is dragged:
updateMarker = event => {
const latLng = event.target.getLatLng(); //get updated marker LatLng
const markerIndex = event.target.options.marker_index; //get marker index
//update
this.setState(prevState => {
const markerData = [...prevState.markerData];
markerData[markerIndex] = latLng;
return { markerData: markerData };
});
};
Demo
Related
I've been trying to render a Map using #react-google-maps/api": "^2.12.1 in my react-hook-form": "^7.33.1. The requirement was to show the map onClick of the input, and the user should draw a polygon on the Map using Drawing Manager.
This is my code, I've been trying to get this working from past two days and I'm nowhere near to the solution. I've gone through stack overflow questions but most of the answers were outdated or I couldn't find anything since most of the people are using react-google-maps where it has been completely redesigned to #react-google-maps/api": "^2.12.1.
The weird thing is the exact code is working in codesandbox. I don't know what I'm
doing wrong here.
import React from "react";
import ReactDOM from "react-dom";
import { LoadScript, GoogleMap, DrawingManager } from "#react-google-maps/api";
const containerStyle = {
width: "400px",
height: "400px",
};
const API_KEY = "";
export default function GoogleMaps() {
const [state, setState] = React.useState({
drawingMode: "polygon",
});
const noDraw = () => {
setState(function set(prevState) {
return Object.assign({}, prevState, {
drawingMode: "maker",
});
});
};
return (
<div className="App">
<LoadScript
id="script-loader"
googleMapsApiKey={API_KEY}
libraries={["drawing"]}
language="en"
region="us"
>
<GoogleMap
mapContainerClassName={containerStyle}
center={{
lat: 38.9065495,
lng: -77.0518192,
}}
zoom={10}
version="weekly"
>
<DrawingManager
drawingMode={state.drawingMode}
options={{
drawingControl: true,
drawingControlOptions: {
drawingModes: ["polygon"],
},
polygonOptions: {
fillColor: `#2196F3`,
strokeColor: `#2196F3`,
fillOpacity: 0.5,
strokeWeight: 2,
clickable: true,
editable: true,
draggable: true,
zIndex: 1,
},
}}
onPolygonComplete={(poly) => {
/*const polyArray = poly.getPath().getArray();
let paths = [];
polyArray.forEach(function(path) {
paths.push({ latitude: path.lat(), longitude: path.lng() });
});
console.log("onPolygonComplete", polyArray);*/
console.log("onPolygonComplete", poly);
noDraw();
}}
/*onOverlayComplete={poly => {
const polyArray = poly.getPath().getArray();
let paths = [];
polyArray.forEach(function(path) {
paths.push({ latitude: path.lat(), longitude: path.lng() });
});
console.log("onOverlayComplete", polyArray);
}}*/
/>
</GoogleMap>
</LoadScript>
</div>
);
}
I have a collection of latlng objects which also have elevation data. I'm mapping over this array and trying to pass that latlng object as a prop to this component i.e. <LeafletReverseGeocoding /> I created. What I want is to take that latlng I passed in and have the .reverse return the closet street near that latlng object. As some of those coordinates originally fall on buildings or non streets.
My hope is the elevation should be about the same to the original latlng.
In Marker component:
{topographyData.map((topoObject, index) => {
console.log("topoObject ", topoObject);
return (
<LeafletReverseGeocoding latlng={topoObject.latlng} key={index} />
);
})}
I'm following the docs here
Code for LeafletReverseGeocoding component:
import L from "leaflet";
import { useEffect } from "react";
import { useMap } from "react-leaflet";
import "leaflet-control-geocoder/dist/Control.Geocoder.js";
export function LeafletReverseGeocoding({ latlng }) {
const map = useMap();
console.log("latlng ", latlng);
useEffect(() => {
var geocoder = L.Control.Geocoder.nominatim();
let marker;
console.log("geocoder ", geocoder);
map.on("load", () => {
geocoder.reverse(
latlng,
map.options.crs.scale(map.getZoom()),
(results) => {
var r = results[0];
if (r) {
if (marker) {
marker
.setLatLng(r.center)
.setPopupContent(r.html || r.name)
.openPopup();
} else {
marker = L.marker(r.center)
.bindPopup(r.name)
.addTo(map)
.openPopup();
}
}
}
);
});
}, []);
return null;
}
Following this yields no markers. What am I missing.
I am trying to draw about 50K points with the leaflet Marker method and it's impossible du the time rendering and ram needed.
The new method I saw is to use Leaflet-canvas to draw point on-screen ' not in the DOM.
How can I perform this in React leaflet 3.X.
I tried
https://www.npmjs.com/package/react-leaflet-canvas-markers
But it doesn't support the V3 of the leaflet.
Any suggestion?
install and import the library npm i leaflet-canvas-marker
Create a custom component and use a useEffect that will mimic the behavior of vanilla leaflet example
import { useEffect } from "react";
import { useMap } from "react-leaflet";
import "leaflet-canvas-marker";
import L from "leaflet";
export default function LeafletCanvasMarker() {
const map = useMap();
useEffect(() => {
if (!map) return;
var ciLayer = L.canvasIconLayer({}).addTo(map);
ciLayer.addOnClickListener(function (e, data) {
console.log(data);
});
ciLayer.addOnHoverListener(function (e, data) {
console.log(data[0].data._leaflet_id);
});
var icon = L.icon({
iconUrl: "https://unpkg.com/leaflet#1.7.1/dist/images/marker-icon.png",
iconSize: [20, 18],
iconAnchor: [10, 9],
});
var markers = [];
for (var i = 0; i < 50000; i++) {
var marker = L.marker(
[58.5578 + Math.random() * 1.8, 29.0087 + Math.random() * 3.6],
{ icon: icon }
).bindPopup("I Am " + i);
markers.push(marker);
}
ciLayer.addLayers(markers);
}, [map]);
return null;
}
Include your custom component as a MapContainer child
<MapContainer center={position} zoom={10} style={{ height: "100vh" }}>
<TileLayer
attribution='© OpenStreetMap contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
<LeafletCanvasMarker />
</MapContainer>
You should get a result similar to the following picture:
I tried to implement react-leaflet search from https://github.com/tumerorkun/react-leaflet-search but, it didn't worked.
following is my code (without including react-leaflet search). Can anyone help me with it.
Hi, I tried to implement react-leaflet search from https://github.com/tumerorkun/react-leaflet-search but, it didn't worked.
following is my code (without including react-leaflet search). Can anyone help me with it.
import React from "react";
import L from "leaflet";
import 'leaflet-timedimension/dist/leaflet.timedimension.src';
import omnivore from '#mapbox/leaflet-omnivore';
import { Map, Marker, Popup } from 'react-leaflet';
import { ReactLeafletSearch } from 'react-leaflet-search';
// import * as countries from './data/map.geojson';
const config = {};
config.default = {
center: [51.505,-0.09],
zoomControl: true,
zoom: 4,
maxZoom: 19,
minZoom: 11,
scrollWheel: false,
legends: true,
infoControl: true,
attributionControl: true
};
config.tdOptions = {
timeInterval: "2018-09-30/2018-10-30",
period: "PT1H",
};
// ===== Example data sources
config.wmsOne = {
url: "https://demo.boundlessgeo.com/geoserver/ows",
options: { layers: 'nasa:bluemarble', transparent: true },
attribution: "© <a href="http://osm.org/copyright">CHANGE THIS</a> contributors",
};
config.wmsTwo = {
url: "https://demo.boundlessgeo.com/geoserver/ows",
options: { layers: 'ne:ne', transparent: true },
attribution: "© <a href="http://osm.org/copyright">CHANGE THIS</a> contributors",
};
config.osmLayer = {
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
params: {
minZoom: 11,
attribution: "© <a href="http://osm.org/copyright">CHANGE THIS</a> contributors",
id: '',
accessToken: ''
}
};
// ===== END Example data sources
export default class LeafletMap extends React.Component {
constructor() {
super();
this.state = {
lat: config.default.center[0],
lng: config.default.center[1],
zoom: config.default.zoom,
toggleActive: true,
timeInterval: config.tdOptions.timeInterval,
period: config.tdOptions.period,
gpxLayer: null,
gpxTimeLayer: null,
};
this.initMap = this.initMap.bind(this);
this.initTimeDimension = this.initTimeDimension.bind(this);
this.onClick = this.onClick.bind(this); // Necessary?
}
// Executed when component is mounted Leaflet-React MAP component
componentDidMount() {
this.initMap();
// this.initTimeDimension(); // TODO: MAKE WORK GOOD!
}
initMap() {
// Usual Leaflet way, but we're using react-leaflet's <Map> component instead
/*this.map = new L.map("root", {
center: [this.state.lat, this.state.lng],
zoom: config.params.zoom,
timeDimension: true
});*/
console.log("### Initializing Leaflet map");
// Tile layer(s)
const tileLayerA =
L.tileLayer.wms(config.wmsOne.url, config.wmsOne.options);
const tileLayerB =
L.tileLayer.wms(config.wmsTwo.url, config.wmsTwo.options);
const tileLayerC = L.tileLayer.wms(config.osmLayer.url)
.addTo(this.map.leafletElement);
// const countriesLayer = L.geoJson(countries, {});
// Add controls for toggling layers
L.control.layers({
"OSM Layer": tileLayerC,
"Layer Two": tileLayerA,
"Layer One": tileLayerB
}).addTo(this.map.leafletElement);
}
// TODO: Make work!
initTimeDimension() {
console.log("### Init Leaflet TimeDimension");
let gpxTl = L.timeDimension.layer.geoJson(this.state.gpxLayer, {
updateTimeDimension: true,
addlastPoint: true,
waitForReady: true
});
this.setState({gpxLayer: omnivore.gpx('public/running_mallorca.gpx')});
this.setState({gpxTimeLayer: gpxTl});
// TimeDimension layer(s)
const td = new L.timeDimension({
period: "PT5M",
});
this.map.timeDimension = td;
//Player -> Component to animate a map with a TimeDimension, changing the time periodically.
let player = new L.TimeDimension.Player({
transitionTime: 100,
loop: false,
startOver:true
}, td);
L.control.timeDimension({
player: player,
}).addTo(this.map.leafletElement);
// Add timedimension from GPX data
new L.timeDimension.layer.geoJson(this.state.tdData, {
updateTimeDimension: true,
addlastPoint: true,
waitForReady: true
}).addTo(this.map.leafletElement);
}
onClick = () => {
this.setState({
toggleActive: !this.state.toggleActive,
});
console.log('CLICKED MAP');
// this.map.leafletElement.fitBounds(this.state.tdData);
};
render() {
const position = [this.state.lat, this.state.lng];
const timeDimensionOptions = {
timeInterval: this.state.timeInterval,
period: this.state.period
};
return (
<React.StrictMode>
<Map
center={position}
zoom={this.state.zoom}
onClick={this.onClick}
timeDimension={true}
timeDimensionOptions={timeDimensionOptions}
timeDimensionControl={true}
ref={(ref) => { this.map = ref; }}>
{/*<TileLayer
attribution={config.wmsOne.attribution}
url={config.wmsOne.url}
/>*/}
{/*<WMSTileLayer
layers={this.state.toggleActive ? 'nasa:bluemarble' : 'ne:ne'}
url="https://demo.boundlessgeo.com/geoserver/ows" />*/}
<Marker position={position}>
<Popup>
A pretty CSS3 popup (react-leaflet component). <br />Easily customizable.
</Popup>
</Marker>
</Map>
</React.StrictMode>
);
} // END of render()
}
Use this :
import { SearchControl } from 'react-leaflet-search';
It works, although it's not on the Documentation
Can you post a jsfiddle or codepen demonstrating the problem?
Also... incidentally the way you are adding controls and layers to the map is not recommended by react-leaflet.
new L.timeDimension.layer.geoJson(this.state.tdData, {...}).addTo(this.map.leafletElement);
is imperative. Prefer declarative rendering like
import { Map, TileLayer, GeoJSON } from 'react-leaflet';
<Map>
<TileLayer>
<GeoJSON>
</Map>
My app loads all my markers just fine, but crashes when I try and click a marker. I get the following error:
"React.Children.only expected to receive a single React element child."
In the main map component I build the map and bring in all the markers from Firebase. I wanted to componentize the markers and build them in a separate file just to keep things a bit straighter in my head.
Here is my Map component
const MyMapComponent = compose(
withProps({
googleMapURL: "https://maps.googleapis.com/maps/api/js?key=xxxxx&v=3.exp&libraries=geometry,drawing,places",
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `500px` }} />,
mapElement: <div style={{ height: `100%` }} />,
}),
withScriptjs,
withGoogleMap
)((props) =>
<GoogleMap
defaultZoom={15}
defaultCenter={{ lat: 34.6781445, lng: -82.8455519 }}
>
{props.markers.map(marker => (
<LotMarker key={marker.key} index={marker.key} lat={marker.latitude} lng={marker.longitude} />
))}
</GoogleMap>
);
class TheMap extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
test:'test',
lots: [],
isOpen: false
};
}
componentWillMount() {
base.bindToState('lots', {
context: this,
state: 'lots',
asArray: true,
});
}
handleMarkerClick = (key) => {
console.log(key);
}
render() {
return (
<MyMapComponent
isMarkerShown={this.state.isMarkerShown}
onMarkerClick={this.handleMarkerClick}
markers={this.state.lots}
/>
)
}
}
export default TheMap;
Here I put the marker together. I've housed the InfoWindow component inside the marker and then ask it to show when the marker is clicked. However, when I do click a marker it just crashes the whole app and white screens.
This is my LotMarker component:
class LotMarker extends React.Component {
// Constructor.
constructor(props) {
// Super Props.
super(props);
// State.
this.state = {
isOpen: false
}
}
onToggleOpen = () => {
console.log(this);
this.setState({isOpen: !this.state.isOpen});
}
// Render.
render() {
// console.log(this);
// Extract Props.
const {
props: {
index,
lat,
lng,
open
}
} = this
return (
<Marker key={index} position={{ lat: lat, lng: lng }} onClick={this.onToggleOpen}>
{this.state.isOpen && <InfoWindow onCloseClick={this.onToggleOpen}>{index}</InfoWindow>}
</Marker>
)
}
}
export default LotMarker;
Alright, it turns out there was a really simple fix here. The content inside the InfoWindow has to be wrapped in a JSX compliant element.
Rather than this:
<InfoWindow onCloseClick={this.onToggleOpen}>{index}</InfoWindow>
I had to have this:
<InfoWindow onCloseClick={this.onToggleOpen}><span>{index}</span></InfoWindow>