Getting updated coordinates after dragging in Leaflet - reactjs

I am using react-leaflet and Leaflet.Path.Drag to drag GeoJSON. I would need to keep GeoJSON coordinations in React state. The issue is that updated coordinations on dragEnd are not corrent and GeoJSON position is not correct after dragging.
codesandbox.io
App.js
import React from "react";
import { MapContainer, GeoJSON, TileLayer, useMap } from "react-leaflet";
import "./styles.css";
import "leaflet/dist/leaflet.css";
require("leaflet-path-drag");
export default function App() {
const geoJSONRef = React.useRef(null);
const [coordinates, setCoordinates] = React.useState([
[-104.98569488525392, 39.63431579014969],
[-104.98569488525392, 39.64165260123419],
[-104.97161865234376, 39.64165260123419],
[-104.97161865234376, 39.63431579014969]
]);
const handleFeature = (layer) => {
layer.makeDraggable();
layer.dragging.enable();
layer.on("dragend", function (e) {
const latLngs = e.target.getLatLngs()[0];
console.log({ latLngs });
const coordinates = latLngs.map((point) => [point.lng, point.lat]);
geoJSONRef.current.eachLayer((geoLayer) => {
console.log(geoLayer.getLatLngs()[0]);
});
setCoordinates(coordinates);
});
};
const object = {
polygon: {
type: "FeatureCollection",
features: [
{
type: "Feature",
properties: {},
geometry: {
type: "Polygon",
coordinates: [[...coordinates]]
}
}
]
}
};
return (
<MapContainer center={[39.63563779557324, -104.99234676361085]} zoom={12}>
<TileLayer
attribution='&copy OpenStreetMap contributors'
url="https://{s}.tile.osm.org/{z}/{x}/{y}.png"
/>
<GeoJSON
key={`${coordinates}`}
ref={geoJSONRef}
data={object.polygon}
style={() => ({
color: "green",
weight: 3,
opacity: 0.5
})}
draggable={true}
pmIgnore={false}
onEachFeature={(feature, layer) => handleFeature(layer)}
></GeoJSON>
</MapContainer>
);
}

I am not sure why but e.target._latlngs[0] and e.target.getLatLngs()[0]; give a different result. Therefore if you try using the first expression it works as expected without moving further the dragged polygon
layer.on("dragend", function (e) {
const latLngs = e.target._latlngs[0]; // here replace the expression
const coordinates = latLngs.map((point) => [point.lng, point.lat]);
geoJSONRef.current.eachLayer((geoLayer) => {
// console.log(geoLayer.getLatLngs()[0]);
});
setCoordinates(coordinates);
});

Related

Changing color on layer React-Mapbox-gl

I am new to React-mapbox GL. I have tried for a while now to look at the examples but can't figure out how to change the layer's color on enter/hover. I have 2 questions so far.
map.on('mouseenter', 'clusters', () => {
map.getCanvas().style.cursor = 'pointer';
});
How can I define the the cluster element for each function in Reactmapbox gl? I don't quite understand how the interactiveLayerIds works I suppose?
question 2.
const onMouseEnter = useCallback(event =>{
if (event.features[0].layer.id==="unclustered-point"){
/* console.log(event.features[0].layer.paint.'circle-color') */
}
})
I have attempted this so far(the whole code is below) but it tells me that circle-color is a unexpected token. OnEnter this unclustered-point layer I want to change the color of the element so the user can clearly see what element they are hovering over? How would I go about doing this in React mapbox gl if I cant change the circle color?
THE WHOLE CODE:
import React, { useContext, useEffect, useRef,useState,useCallback } from 'react';
import './MapViews.css';
import { useNavigate } from 'react-router-dom';
import ReactMapGL, { Marker, Layer, Source } from 'react-map-gl';
import SourceFinder from '../../Apis/SourceFinder';
import { SourceContext } from '../../context/SourceContext';
import { clusterLayer, clusterCountLayer, unclusteredPointLayer } from './Layers';
const MapView = () => {
const navigate = useNavigate()
const { sources, setSources } = useContext(SourceContext)
const [viewport, setViewport] = React.useState({
longitude: 10.757933,
latitude: 59.91149,
zoom: 12,
bearing: 0,
pitch: 0
});
const mapRef = useRef(null);
function getCursor({isHovering, isDragging}) {
return isDragging ? 'grabbing' : isHovering ? 'pointer' : 'default';
}
useEffect(() => {
const fetchData = async () => {
try {
const response = await SourceFinder.get("/sources");
setSources(response.data.data.plass);
} catch (error) { }
};
fetchData();
}, [])
const onMouseEnter = useCallback(event =>{
if (event.features[0].layer.id==="unclustered-point"){
/* console.log(event.features[0].layer.paint.'circle-color') */
}
})
const ShowMore = event => {
if(event.features[0].layer.id==="unclustered-point"){
const feature = event.features[0];
console.log(feature)
mapRef.current.getMap().getCanvas().style.cursor="pointer"
}else{
const feature = event.features[0];
const clusterId = feature.properties.cluster_id;
const mapboxSource = mapRef.current.getMap().getSource('stasjoner');
mapboxSource.getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) {
return;
}
setViewport({
...viewport,
longitude: feature.geometry.coordinates[0],
latitude: feature.geometry.coordinates[1],
zoom,
transitionDuration: 500
});
});
}
};
return (
<ReactMapGL {...viewport} width="100%" height="100%" getCursor={getCursor} onMouseEnter={onMouseEnter} interactiveLayerIds={[clusterLayer.id,unclusteredPointLayer.id]} mapboxApiAccessToken={"SECRET"} clickRadius={2} onViewportChange={setViewport} mapStyle="mapbox://styles/mapbox/streets-v11" onClick={ShowMore} ref={mapRef}>
<Source id="stasjoner" type="geojson" data={sources} cluster={true} clusterMaxZoom={14} clusterRadius={50} >
<Layer {...clusterLayer} />
<Layer {...clusterCountLayer} />
<Layer {...unclusteredPointLayer}/>
</Source>
</ReactMapGL>
);
};
export default MapView;
LAYERS.JS
//Hvergang vi skal ha 2 eller flere baller
export const clusterLayer = {
id: 'clusters',
type: 'circle',
source: 'stasjoner',
filter: ['has', 'point_count'],
paint: {
'circle-color': ['step', ['get', 'point_count'], '#51bbd6', 100, '#f1f075', 500, '#f28cb1'],
'circle-radius': ['step', ['get', 'point_count'], 20, 100, 30, 750, 40],
}
};
//Dette er tallene som er inne i ballene
export const clusterCountLayer = {
id: 'cluster-count',
type: 'symbol',
source: 'stasjoner',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-font': ['DIN Offc Pro Medium', 'Arial Unicode MS Bold'],
'text-size': 12,
}
};
//Per punkt
export const unclusteredPointLayer = {
id: 'unclustered-point',
type: 'circle',
source: 'stasjoner',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#11b4da',
'circle-radius': 8,
'circle-stroke-width': 1,
'circle-stroke-color': '#fff',
}
};

How to place my dispatch function with React and React Leaflet for return coords lat + lng

I have an input (on children component) that return coordinates by props (text):
import React, { useEffect, useState, useRef, useMemo } from 'react'
import { useEnderecoValue } from '../../contexts/EnderecoContext'
import 'leaflet/dist/leaflet.css'
import Leaflet from 'leaflet'
import { MapContainer, Marker, useMap, TileLayer, Popup } from 'react-leaflet'
export default function App(text: any) {
const [lat, setLat] = useState(48.856614)
const [lng, setLng] = useState(2.3522219)
const [state, dispatch] = useEnderecoValue()
const icon = new Leaflet.DivIcon({
className: 'custom-div-icon',
html:
"<div style='background-color:#c30b82;' class='marker-pin'></div><i class='material-icons'><img src='img/marker-icon.png'></i>",
iconSize: [30, 42],
iconAnchor: [15, 42],
popupAnchor: [-3, -42]
})
useEffect(() => {
if (text.text) {
setLat(text.text.features[0].geometry.coordinates[1])
setLng(text.text.features[0].geometry.coordinates[0])
}
}, [text])
function SetViewOnClick({ coords }: any) {
const map = useMap()
map.flyTo(coords, map.getZoom())
return null
}
My Marker is draggable and the popup display address and coords if I search address on input, or if the Marker is dragded:
const markerRef = useRef(null)
const eventHandlers = useMemo(
() => ({
dragend() {
const marker = markerRef.current
if (marker != null) {
const { lat, lng } = marker.getLatLng()
setLat(lat)
setLng(lng)
}
}
}),
[]
)
const popup = () => {
if (text.text) {
return text.text.query + ' ' + `lat: ${lat}, long: ${lng}`
}
return (
"Address by default" +
' ' +
`lat: ${lat}, long: ${lng}`
)
}
return (
<MapContainer
center={[lat, lng]}
attributionControl={false}
zoomControl={false}
zoom={18}
style={{
height: '350px',
position: 'relative',
outline: 'none',
maxWidth: '696px',
display: 'block',
margin: '15px auto',
width: '100%'
}}
>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker
position={[lat, lng]}
icon={icon}
draggable={true}
ref={markerRef}
eventHandlers={eventHandlers}
>
<Popup>
<span>{popup()}</span>
</Popup>
<SetViewOnClick coords={[lat, lng]} />
</Marker>
</MapContainer>
)
}
How to place my dispatch function for return coords when I search address and whenthe Marker is dragged ? (just when the value change)
dispatch({
type: 'SET_COORDS',
latitude: lat,
longitude: lng
})
On your Search comp place dispatch inside searchLocation to be able to change lat long.
const searchLocation = async () => {
fetch(
"https://api-adresse.data.gouv.fr/search?" +
new URLSearchParams({
q: state.location,
})
)
.then((data) => data.json())
.then((text) => {
setResp({ text });
dispatch({
type: "SET_COORDS",
latitude: text.features[0].geometry.coordinates[1],
longitude: text.features[0].geometry.coordinates[0],
});
})
.catch(function (error) {
console.log("request failed", error);
});
};
Replace <Map {...resp} /> with <Map text={resp} /> because it causes multiple rerenders and the page becomes unresponsive. Not sure what you were trying to do.
Demo

How to Draw Polyline on mapmyindia using React?

I am just exploring Mapmyindia.
I have gone through the basic location display on the map.
I am not getting how to display polyline on map.
code for location
app.js
import "./App.css";
import SideBar from "./componants/SideBar";
import Map from "mapmyindia-react";
// import MapmyIndia, { MapMarker } from "react-mapmyindia";
function App() {
return (
<div className="App">
Hello
<Map
markers={[
{
position: [21.1588329, 72.7688111],
draggable: true,
zoom: 15,
title: "Marker title",
onClick: (e) => {
console.log("clicked ");
},
onDragend: (e) => {
console.log("dragged");
},
onMouseover: (e) => {
console.log("Mouse over");
},
},
]}
/>
{/* <Map /> */}
<SideBar></SideBar>
</div>
);
}
export default App;
Which result this
Now, Please help with drawing polyline.
I guess you're using the npm package for maps. If you go through the library's code in gitHub, you can see the owner has only added marker functionality. You can simply copy the code from there and add it manually to your project and then add the polyline functionality and then pass the data as props from your app.js file like you're doing for markers.
renderPolylines = () => {
const { polylines = [] } = this.props;
if (!this.map) {
return;
}
polylines.map(m => {
if (m?.position && Array.isArray(m.position)) {
const { position, color, weight, opacity } = m;
let points = [];
position.map(p => {
const { lat, lng } = p;
const center = new L.LatLng(lat, lng);
points.push(
new L.LatLng(center.lat, center.lng))/*array of wgs points*/
})
const polyline = new L.Polyline(points, { color, weight, opacity });
this.map.addLayer(polyline);
this.polylines.push(polyline);
}
});
};
Props for rendering polyline from app.js
polylines = {[
{
position: [
{
lat: 18.5014,
lng: 73.805,
},
{
lat: 18.5414,
lng: 73.855,
},
{
lat: 18.5514,
lng: 73.855,
},
{
lat: 18.5614,
lng: 73.855,
},
],
color: "red",
weight: 4,
opacity: 0.5,
},
]}

InvalidValueError: setMap: not an instance of Map; and not an instance of StreetViewPanorama #react-google-maps/api

I am using #react-google-maps/api and loading the script from cdn to my public directory index.html file.
I get InvalidValueError: setMap: not an instance of Map; and not an instance of StreetViewPanorama and the markers do not appear on my map. Weird thing is that it is totally random to get this error. Sometimes it works without any problem. That is what I do not understand in the first place.
import React, { useEffect } from 'react';
import { GoogleMap, Marker } from '#react-google-maps/api';
import mapStyles from './mapStyles';
//google map height 100
import './MapDisplay.css';
import { connect } from 'react-redux';
const mapContainerStyle = {
height: '100%',
width: '100%',
};
const options = {
styles: mapStyles,
disableDefaultUI: true,
zoomControl: true,
};
const MapDisplay = ({
userLocation,
mouseHoverIdR,
selectedInfoR,
searchedResults,
unAuthNoSearchResults,
selectedLocationInfoWindowS,
}) => {
const onClickMarker = (e) => {
selectedLocationInfoWindowS({
selectedInfoR: e,
type: 'infoWindowLocations',
});
};
const onClickMap = () => {
selectedLocationInfoWindowS({
type: '',
selectedInfoR: {
_id: '',
},
});
};
return (
<div id='map'>
<GoogleMap
id='map_canvas'
mapContainerStyle={mapContainerStyle}
zoom={12}
center={userLocation}
options={options}
onClick={() => onClickMap()}
>
{!unAuthNoSearchResults &&
searchedResults.map((searchedResult) => {
if (
mouseHoverIdR === searchedResult._id ||
selectedInfoR._id === searchedResult._id
) {
var a = window.google.maps.Animation.BOUNCE;
}
return (
<Marker
position={searchedResult.coordinates}
key={searchedResult._id}
clickable
onClick={() => onClickMarker(searchedResult)}
animation={a}
id={'hello'}
/>
);
})}
</GoogleMap>
</div>
);
};
How can I fix the error that I get when I try to display the marker on the map?

Update in real time tooltip with react-leaflet when changing language with i18n

I am currently displaying a Map, thanks to react-leaflet, with a GeoJSON Component. I'm also displaying some tooltips on hover over some countries and cities(for example, when I hover France, a tooltip display "France"). I'm also using i18n for internationalization.
The internationalization works fine for the country tooltips, they are updated in real time.
I have a function updateDisplay, that switch between a GeoJson component for the countries, or a list of Marker for the cities, on zoom change.
The problem is, that when i'm switching languages, it works fine for the whole page, but not for the city tooltips. They are updated only when I zoom (so when the updateDisplay is called).
I would have the expected behaviour : regardless of the zoom, I would like that the city tooltips update in real time, when i switch language.
I hope I've made myself clear
Here is my code :
/**
* Display a Leaflet Map, containing a GeoJson object, or a list of Markers, depending on the zoom
*/
export default function CustomMap(): ReactElement {
const { t }: { t: TFunction } = useTranslation();
const countryToString = (countries: string[]): string => countries.map(c => t(c)).join(", ");
// Contains the json containing the polygons of the countries
const data: geojson.FeatureCollection = geoJsonData as geojson.FeatureCollection;
let geoJson: JSX.Element = <GeoJSON
key='my-geojson'
data={data}
style={() => ({
color: '#4a83ec',
weight: 1,
fillColor: "#1a1d62",
fillOpacity: 0.25,
})}
onEachFeature={(feature: geojson.Feature<geojson.GeometryObject>, layer: Layer) => {
layer.on({
'mouseover': (e: LeafletMouseEvent) => {
const country = countries[e.target.feature.properties.adm0_a3];
layer.bindTooltip(countryToString(country.tooltip as string[]));
layer.openTooltip(country.latlng);
},
'mouseout': () => {
layer.unbindTooltip();
layer.closeTooltip();
},
});
}}
/>
// Contains a list of marker for the cities
const cityMarkers: JSX.Element[] = cities.map(
(
c: position,
i: number
) => {
return (
// Here are the tooltips that doesn't update in real time, when we switch language
// FIX ME
<Marker key={c.latlng.lat + c.latlng.lng} position={c.latlng}>
<Tooltip>{t(c.tooltip as string)}</Tooltip>
</Marker>
);
}
);
const [state, setState] = useState<state>({
zoom: 3,
display: geoJson,
});
// Update on zoom change
function onZoom(e: LeafletMouseEvent): void {
const zoom = e.target._zoom;
const newDisplay = updateDisplay(zoom);
setState({
...state,
zoom,
display: newDisplay,
});
}
// Called on every zoom change, in order to display either the GeoJson, or the cities Marker
function updateDisplay(zoom: number): Marker[] | any {
if (zoom >= 4) {
return cityMarkers;
} else {
return geoJson;
}
}
return (
<Map
style={{ height: "500px" }}
center={[54.370138916189596, -29.918133437500003]}
zoom={state.zoom}
onZoomend={onZoom}
>
<TileLayer url="https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw" />
{state.display}
</Map>
);
}
You can also look at it here : https://github.com/TheTisiboth/WebCV/blob/WIP/src/components/customMap.tsx
It is on the branch WIP
You can do the following to overcome this issue:
Create a boolean flag to keep in memory if the markers have been added
Add the markers on the map using native leaflet code instead of react'leaflet's wrappers.
If the markers are added and zoom >= 4 set the flag to true
if zoom < 4 remove the markers to be able to show countries, set flag to false
When language is changed, if zoom is bigger, equal than 4 and markers have been added remove the previous, add new ones with the new tooltip
you can achieve all these by holding a reference to the map instance.
Here is the whole code you will need, (parts of cities, markers removed):
import React, { useState, ReactElement } from "react";
import { useTranslation } from "react-i18next";
import { Map, Marker, TileLayer, GeoJSON } from "react-leaflet";
import geoJsonData from "../assets/geoJsonData.json";
import { LatLngLiteral, Layer, LeafletMouseEvent } from "leaflet";
import geojson from "geojson";
import { TFunction } from "i18next";
import L from "leaflet";
interface position {
latlng: LatLngLiteral;
tooltip: string;
}
interface state {
markers: position[];
zoom: number;
display: position[] | any;
geoJson: JSX.Element;
countries: { [key: string]: position };
}
/**
* Display a Leaflet Map, containing a GeoJson object, or a list of Markers, depending on the zoom
*/
export default function CustomMap(): ReactElement {
const mapRef: any = React.useRef();
const { t, i18n }: { t: TFunction; i18n: any } = useTranslation();
const [markersAdded, setMarkersAdded] = useState(false);
i18n.on("languageChanged", (lng: any) => {
if (lng) {
const map = mapRef.current;
if (map && map.leafletElement.getZoom() >= 4 && markersAdded) {
map.leafletElement.eachLayer(function (layer: L.Layer) {
if (layer instanceof L.Marker) map.leafletElement.removeLayer(layer);
});
state.markers.map((c: position, i: number) => {
L.marker(c.latlng)
.addTo(map.leafletElement)
.bindTooltip(t(c.tooltip));
});
}
}
});
// const countryToString = (countries: string[]): string => countries.join(", ");
// List of position and label of tooltip for the GeoJson object, for each country
const countries: { [key: string]: position } = {
DEU: {
latlng: {
lat: 51.0834196,
lng: 10.4234469,
},
tooltip: "travel.germany",
},
CZE: {
latlng: {
lat: 49.667628,
lng: 15.326962,
},
tooltip: "travel.tchequie",
},
BEL: {
latlng: {
lat: 50.6402809,
lng: 4.6667145,
},
tooltip: "travel.belgium",
},
};
// List of position and tooltip for the cities Markers
const cities: position[] = [
{
latlng: {
lat: 48.13825988769531,
lng: 11.584508895874023,
},
tooltip: "travel.munich",
},
{
latlng: {
lat: 52.51763153076172,
lng: 13.40965747833252,
},
tooltip: "travel.berlin",
},
{
// greece
latlng: {
lat: 37.99076843261719,
lng: 23.74122428894043,
},
tooltip: "travel.athens",
},
{
// greece
latlng: {
lat: 37.938621520996094,
lng: 22.92695426940918,
},
tooltip: "travel.corinth",
},
];
// Contains the json containing the polygons of the countries
const data: geojson.FeatureCollection = geoJsonData as geojson.FeatureCollection;
let geoJson: JSX.Element = (
<GeoJSON
key='my-geojson'
data={data}
style={() => ({
color: "#4a83ec",
weight: 1,
fillColor: "#1a1d62",
fillOpacity: 0.25,
})}
// PROBLEM : does not update the tooltips when we switch languages
// FIX ME
onEachFeature={(
feature: geojson.Feature<geojson.GeometryObject>,
layer: Layer
) => {
layer.on({
mouseover: (e: LeafletMouseEvent) => {
const country =
state.countries[e.target.feature.properties.adm0_a3];
layer.bindTooltip(t(country?.tooltip));
layer.openTooltip(country?.latlng);
},
mouseout: () => {
layer.unbindTooltip();
layer.closeTooltip();
},
});
}}
/>
);
const [state, setState] = useState<state>({
markers: cities,
zoom: 3,
geoJson: geoJson,
display: geoJson,
countries: countries,
});
// Update on zoom change
function onZoom(e: LeafletMouseEvent): void {
const zoom = e.target._zoom;
const newDisplay = updateDisplay(zoom);
setState({
...state,
zoom,
display: newDisplay,
});
}
// Called on every zoom change, in order to display either the GeoJson, or the cities Marker
function updateDisplay(zoom: number): Marker[] | any {
const map = mapRef.current;
if (zoom >= 4) {
return state.markers.map((c: position, i: number) => {
console.log(t(c.tooltip));
if (map && !markersAdded) {
console.log(map.leafletElement);
L.marker(c.latlng)
.addTo(map.leafletElement)
.bindTooltip(t(c.tooltip));
setMarkersAdded(true);
}
});
} else {
map.leafletElement.eachLayer(function (layer: L.Layer) {
if (layer instanceof L.Marker) map.leafletElement.removeLayer(layer);
});
setMarkersAdded(false);
return state.geoJson;
}
}
return (
<Map
ref={mapRef}
style={{ height: "500px" }}
center={[54.370138916189596, -29.918133437500003]}
zoom={state.zoom}
onZoomend={onZoom}
>
<TileLayer url='https://api.mapbox.com/styles/v1/mapbox/streets-v11/tiles/{z}/{x}/{y}?access_token=pk.eyJ1IjoibWFwYm94IiwiYSI6ImNpejY4NXVycTA2emYycXBndHRqcmZ3N3gifQ.rJcFIG214AriISLbB6B5aw' />
{state.display}
</Map>
);
}
Eng:
"travel": {
"germany": "Munich, Berlin, Hambourg, Münster, Allemagne",
"munich": "Munchen",
"berlin": "Berlin",
"tchequie": "Tchéquie, Prague",
"belgium": "Belgique",
"athens": "Athènes",
"corinth": "Corinthe",
...
}
Fr:
"travel": {
"germany": "Munich, Berlin, Hamburg, Münster, Germany",
"munich": "Munich",
"berlin": "Berlin",
"tchequie": "Czech Republic, Prague",
"belgium": "Belgium",
"athens": "Athens",
"corinth": "Corinth",
...
}
You can make it more clean by reusing the markers removal code chunk and markers addition code chunk respectively.

Resources