I'm displaying a map with react leaflet v3 in my react app.
I just want to add a custom control, but I can't figure out what's the good way to do that.
Actually, i do it like that, but it seems to be not working.
function DptCtl(props) {
// Control
const map = useMap();
// List of dpts and provinces
const dpts = useSelector(dptsSelector);
L.Control.Dpts = L.Control.extend({
onAdd(map) {
const container = L.DomUtil.create('div');
const input = L.DomUtil.create('input', container);
return container;
},
onRemove(map) {}
})
L.Control.dpts = function (opts) {
return new L.Control.Dpts(opts);
}
useEffect(() => {
const control = L.Control.dpts({ position: props.position })
map.addControl(control)
return () => {
map.removeControl(control)
}
}, [dpts])
return null;
}
React-Leaflet v3 provides the createControlComponent Hook in the Core API that takes in an instance of a Leaflet control and returns a Leaflet element.
Here is an example using Leaflet's Zoom control:
import L from 'leaflet';
import { createControlComponent } from '#react-leaflet/core';
const createControlLayer = (props) => {
// Set up an instance of the control:
const controlInstance = new L.Control.Zoom(props);
return controlInstance;
};
// Pass the control instance to the React-Leaflet createControlComponent hook:
const customControl = createControlComponent(createControlLayer);
export default customControl;
Then, add the new custom control layer to the Map:
<MapContainer
center={[37.0902, -95.7129]}
zoom={3}
zoomControl={false}
style={{ height: '100vh', width: '100%', padding: 0 }}
whenCreated={(map) => setMap(map)}
>
<CustomControl />
<LayersControl position="topright">
<LayersControl.BaseLayer checked name="Map">
<TileLayer
attribution='© OpenStreetMap contributors'
url={maps.base}
/>
</LayersControl.BaseLayer>
</LayersControl>
</MapContainer>
DEMO
https://react-leaflet.js.org/docs/core-api/#createcontrolcomponent
https://javascript.plainenglish.io/how-to-create-a-react-leaflet-control-component-with-leaflet-routing-machine-8eef98259f20
Related
I am trying to include arrows to the Polyline in react-leaft. For that I am using polylinedecorator plugin. There is a similar post on this platform. However, it uses withLeaflet module which is not supported in react-leaflet 4.0. How can I make it run without using 'withLeaflet'.
I have tried to implement it with the hooks. However, it does not work and need some assistance, how can I make it run.
export default function App(): JSX.Element {
const polylineRef = useRef<any>(null);
const arrow = [
{
offset: "100%",
repeat: 0,
symbol: L.Symbol.arrowHead({
pixelSize: 15,
polygon: false,
pathOptions: { stroke: true }
})
}];
useEffect(()=>{
L.polylineDecorator(polylineRef.current,{
patterns: arrow
})
}, [polylineRef]);
return (
<MapContainer center={center} zoom={13} scrollWheelZoom={true} style={{height: 'calc(100% - 30px)'}}>
<TileLayer
attribution='© OpenStreetMap contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
/>
{currentData?.movingActors.map(line =>(<Polyline key={line.id}
positions={[line.startLocation, line.endLocation] } ref={polylineRef}
color={modeColor(line.mode)}
/>
))}
</MapContainer>
</>);}
CHANGES MADE TO THE ACCEPTED ANSWER TO MAKE IT RUN
function PolylineDecorator({ patterns, polyline,color }) {
const map = useMap();
useEffect(() => {
if (!map) return;
L.polyline(polyline, {color}).addTo(map); // added color property
L.polylineDecorator(polyline, {
patterns,
}).addTo(map);
}, [map]);
return null;
}
{currentData?.movingActors.map(line =>(<PolylineDecorator key={line.id} patterns ={arrow} polyline={position} color = {modeColor(line.mode)} />) ) } //here I used color parameters to dynamically add colors
What you need is a custom react functional component that returns null and has a useEffect with the code to initialize the plugin:
function PolylineDecorator({ patterns, polyline }) {
const map = useMap();
useEffect(() => {
if (!map) return;
L.polyline(polyline).addTo(map);
L.polylineDecorator(polyline, {
patterns
}).addTo(map);
}, [map]);
return null;
}
and then use it like:
<MapContainer...>
<TileLayer url="http://{s}.tile.osm.org/{z}/{x}/{y}.png" />
<PolylineDecorator patterns={arrow} polyline={polyline} />
</MapContainer>
Demo
I am implementing the 'Leaflet.MagnifyingGlass' on my typescript react-leaflet v4 project. I am able to show the magnifying glass on the tile layer however, I am not sure, how to implement it on the already overlaid vector layer.
function Lense (){
const lenseLayer = useMap();
const magnifiedTiles = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png")
useEffect(() => {
L.magnifyingGlass({
layers: [magnifiedTiles]
//layers: [PolylineDecorator]
}).addTo(lenseLayer)
}, [lenseLayer]);
return <></>;
}
export default function App(): JSX.Element {
<MapContainer center={center} zoom={13} scrollWheelZoom={true} style={{height: 'calc(100% - 30px)'}}>
<TileLayer
attribution='© OpenStreetMap contributors'
url='https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' />
{currentData?.movingActors.map(line =>(<PolylineDecorator key={line.id}
polyline={positions} color={modeColor(line.mode)}/>))}
<Lense />
</MapContainer>
</>
);
}
As one can see, that at the moment, magnifying glass only is laid to tile object. I need to lay it on top of the polylindedecorator object. The function is defined as
function PolylineDecorator({ polyline,color }) {
const map = useMap();
useEffect(() => {
if (!map) return;
var x = L.polyline(polyline, {color}).arrowheads({ size: '5%' });
x.addTo(map);
}, [map]);
return null;
}
Any suggestions, how can I achieve that.
PS I have already tried react-leaflet-magnifying-glass, but it is not compatible with the latest react-leaflet version.
I'm a new leaflet learner with React typescript. Want to create a custom button on the map. On clicking the button a popup will appear. I saw many example but they are all based on older version and I also tried to create my own but no luck. The documentation also not providing much help. Even a functional custom control component is also very effective for my app. Any help on this will be much appreciated. Here is my code,
Custom button
import React, { Component } from "react";
import { useMap } from "react-leaflet";
import L, { LeafletMouseEvent, Map } from "leaflet";
class Description extends React.Component<{props: any}> {
createButtonControl() {
const MapHelp = L.Control.extend({
onAdd: (map : Map) => {
const helpDiv = L.DomUtil.create("button", ""); //how to pass here the button name and
//other property ?
//a bit clueless how to add a click event listener to this button and then
// open a popup div on the map
}
});
return new MapHelp({ position: "bottomright" });
}
componentDidMount() {
const { map } = this.props as any;
const control = this.createButtonControl();
control.addTo(map);
}
render() {
return null;
}
}
function withMap(Component : any) {
return function WrappedComponent(props : any) {
const map = useMap();
return <Component {...props} map={map} />;
};
}
export default withMap(Description);
The way I want to call it
<MapContainer
center={defaultPosition}
zoom={6}
zoomControl={false}
>
<Description />
<TileLayer
attribution="Map tiles by Carto, under CC BY 3.0. Data by OpenStreetMap, under ODbL."
url="https://cartodb-basemaps-{s}.global.ssl.fastly.net/light_all/{z}/{x}/{y}.png"
/>
<ZoomControl position={'topright'}/>
</MapContainer>
You're close. Sticking with the class component, you just need to continue creating your buttons instance. You can use a prop on Description to determine what your button will say and do:
<Description
title={"My Button Title"}
markerPosition={[20.27, -157]}
description="This is a custom description!"
/>
In your decsription's createButtonControl, you're almost there. You just need to fill it out a bit:
createButtonControl() {
const MapHelp = L.Control.extend({
onAdd: (map) => {
const helpDiv = L.DomUtil.create("button", "");
this.helpDiv = helpDiv;
// set the inner content from the props
helpDiv.innerHTML = this.props.title;
// add the event listener that will create a marker on the map
helpDiv.addEventListener("click", () => {
console.log(map.getCenter());
const marker = L.marker()
.setLatLng(this.props.markerPosition)
.bindPopup(this.props.description)
.addTo(map);
marker.openPopup();
});
// return the button div
return helpDiv;
}
});
return new MapHelp({ position: "bottomright" });
}
Working codesandbox
There's a million ways to vary this, but hopefully that will get you going.
I'm following this example that puts Mapbox labels on top of a layer. This seems to be written using the plain Mapbox package. I'm hoping to do the same for a map component in DeckGL.
The relevant code from the example:
const map = new mapboxgl.Map({
container: document.body,
style: 'mapbox://styles/mapbox/light-v9',
center: [-122.4, 37.79],
zoom: 15,
pitch: 60
});
map.on('load', () => {
const firstLabelLayerId = map.getStyle().layers.find(layer => layer.type === 'symbol').id;
My code using DeckGL is:
<DeckGL
initialViewState={INITIAL_VIEW_STATE}
layers={layers}
onClick={expandTooltip}
onViewStateChange={hideTooltip}
onWebGLInitialized={onInitialized}
views={MAP_VIEW}
controller={{
touchRotate: true,
inertia: 600,
}}
>
<StaticMap
reuseMaps
mapStyle={MAP_STYLE}
preventStyleDiffing={true}
mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
/>
</DeckGL>
How can I access getStyle().layers in the above components? I tried using useRef, as in this simplified component:
const mapRef = useRef();
<DeckGL
{...viewport}
maxZoom={20}
mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
ref={mapRef}
>
but found that it doesn't contain information about the component.
You will need to wait until mapLoads, something like:
1 - Define a new ref:
const mapRef = useRef();
2 - Wait for map loads:
<StaticMap
ref={mapRef}
onLoad={onMapLoad}
...otherProps
/>
3 - Use getMap method. Now we are sure that Mapbox instance exists:
const onMapLoad = useCallback(() => {
const map = mapRef.current.getMap();
const mapboxLayers = map.getStyle().layers;
console.log(mapboxLayers);
}, []);
You have to use deckgl layer after map gets loaded into the browser. For that purpose, you can use:
Map.on('load', callback function)
For more reference use this https://youtu.be/x6UcMcAWNMo
I'm trying to recreate an effect similar to the hover effect found on this site: http://tabotabo.com
Currently, what I'm doing is have the video playing on a layer with a second scaled up layer also playing the video with an additional Text object with a destination-in compositing operation. This is currently working well enough but I was curious if there would be a more efficient way to achieve this by either caching or cloning the first layer and sending that through to the second layer instead of having two separate video objects running in tandem.
Here is the relevant code, if it helps.
Main Render:
<Stage width={width} height={height} ref={ref => (this.stage = ref)}>
<Layer hitGraphEnabled={false}>
<CanvasVideo
src={this.state.background}
settings={{id: 'main', width: width, height: height }}
ref={(el) => this.main = el }
/>
</Layer>
<Layer hitGraphEnabled={false} scaleX={hoverScale} scaleY={hoverScale} x={scaleOffsetX} y={scaleOffsetY}>
<CanvasVideo
src={this.state.background}
settings={{id: 'main2', width: width, height: height }}
ref={(el) => this.main2 = el }
/>
<Text
id="hoverText"
text={this.state.menu.hoverText}
globalCompositeOperation='destination-in'
fontSize={200}
fill="white"
opacity={hoverOpacity}
height={height}
width={width}
align="center"
verticalAlign='middle'
/>
</Layer>
</Stage>
Video Container Class:
import React, { Component } from 'react';
import Konva from 'konva';
import { render } from 'react-dom';
import { Stage, Layer, Image } from 'react-konva';
class CanvasVideo extends Component {
constructor(props) {
super(props);
const video = document.createElement('video');
video.muted = true;
video.autoplay = false;
video.loop = true;
video.src = props.src;
this.state = {
video: video
};
video.addEventListener('canplay', () => {
video.play();
this.image.getLayer().batchDraw();
this.requestUpdate();
});
}
requestUpdate = () => {
this.image.getLayer().batchDraw();
requestAnimationFrame(this.requestUpdate);
}
render() {
let { settings } = this.props
return (
<Image
{...settings}
image={this.state.video}
ref={node => { this.image = node; }}
/>
);
}
}
CanvasVideo.defaultProps = {
settings: null,
};
export default CanvasVideo;
Any explanations or insights would be greatly appreciated.
Thank you!
Currently, there is no way to reuse Konva.Image or any other Konva.Node inside different parents. Konva.Node can have only one parent.
I see only one optimization here is to reuse <video> element you created in CanvasVideo component. It can be like this:
const cache = {};
function createVideo(url) {
if (cache[url]) {
return cache[url];
}
const video = document.createElement('video');
video.src = url;
cache[url] = video;
return video;
}
const video = createVideo(props.src);