Show vector layers through Leaflet.MagnifyingGlass on react-leaflet 4 - reactjs

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.

Related

how to ensure data isn't undefined before initial render?

I have a map component which I have been using to plot polyline data on a react leaflet map.
I want the initial center of the leaflet map to be the first position in my array of latitude and longitude data. I created a function to set values based on two different conditions these work correctly the values get set as can be seen through the logs to the console.
But when my component is initially rendered it causes an error because it says that initialPositionLat and initialPositionLong are undefined.
I tried using useEffect to set the values but I am unsure how to ensure initialPositionLat and initialPositionLong are set from the function before the component is rendered. Does anyone know where I am going wrong?
The map component:
function Mapp(props) {
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
setInitPosition(props);
setIsLoading(false);
},[]);
let initialPositionLat;
let initialPositionLong;
function setInitPosition(props) {
if(!Array.isArray(props.activityData)) {
console.log("first if positions = ", props.activityData.positions[0][1])
initialPositionLat = props.activityData.positions[0][0];
initialPositionLong = props.activityData.positions[0][1];
}
else {
console.log("2nd if positons = ", props.activityData.polylines.positions)
initialPositionLat = props.activityData.poylines.positions[0][0];
initialPositionLong = props.activityData.poylines.positions[0][1]
}
//console.log("initial position = ", [initialPositionLat, initialPositionLong]);
}
return !isLoading ? (
<MapContainer center={[initialPositionLat, initialPositionLong]} zoom={15} scrollWheelZoom={false}>
<TileLayer attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{!Array.isArray(props.activityData) && <Polyline positions={props.activityData.positions} >
</Polyline>
}
{Array.isArray(props.activityData.polylines) && props.activityData.polylines.length > 1 &&
props.activityData.polylines.map((activity, idx) => (
<Polyline key={idx} positions={activity.positions}
</Polyline>
))}
</MapContainer>
) : (
<div>
<p>Loading...</p>
</div>
)
}
export default Mapp;
You can do like this,
<MapContainer center={getInitPosition()} zoom={15} scrollWheelZoom={false}>
and change the function,
function getInitPosition() {
if(!Array.isArray(props.activityData)) {
console.log("first if positions = ", props.activityData.positions[0][1])
return [props.activityData.positions[0][0],props.activityData.positions[0][1]];
}
else {
console.log("2nd if positons = ", props.activityData.polylines.positions)
return [props.activityData.poylines.positions[0][0],props.activityData.poylines.positions[0][1]];
}
//console.log("initial position = ", [initialPositionLat, initialPositionLong]);
}

polylinedacorator with react leaflet 4

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

Rendering a MapContainer on several subpages results in error

I'm having some issues with MapContainer from react-leaflet throwing errors at me in my app.
I have a tabel of events, that links to a page with event details, this details page implements a map with the following component:
export function MapWithPoints(props: Props) {
const { points } = props
return (
<MapContainer scrollWheelZoom={false} style={{ height: 300, width: '100vw' }}>
<InnerMap points={points} />
</MapContainer>
)
}
InnerMap is defined as:
function InnerMap(props: Props) {
const { points } = props
const leafletMap = useMap()
React.useEffect(() => {
if (leafletMap && points.length) {
const pointArray: L.LatLngTuple[] = points.map((item) => [
item.lat,
item.lng,
])
const bounds = L.latLngBounds(pointArray)
leafletMap.fitBounds(bounds)
}
}, [points, leafletMap])
return (
<React.Fragment>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{points
.filter((poi) => poi.lat && poi.lng)
.map((poi) => (
<Marker
position={[poi.lat, poi.lng]}
key={`${poi.id}_${poi.lat}_${poi.lng}`}
icon={poi.icon ? poi.icon : DefaultIcon}
>
{poi.description ? <Popup>{poi.description}</Popup> : null}
</Marker>
))}
</React.Fragment>
)
}
The map works when I go to the first (random) entry in my list of events, the event page loads like it should, but if I go back and then choose another event page, MapContainer throws an error:
Uncaught Error: Map container is already initialized.
at NewClass._initContainer (Map.js:1092:1)
at NewClass.initialize (Map.js:136:1)
at new NewClass (Class.js:24:1)
at MapContainer.js:31:1
at commitAttachRef (react-dom.development.js:23645:1)
at safelyAttachRef (react-dom.development.js:22891:1)
at reappearLayoutEffectsOnFiber (react-dom.development.js:23545:1)
at reappearLayoutEffects_complete (react-dom.development.js:24838:1)
at reappearLayoutEffects_begin (react-dom.development.js:24826:1)
at commitLayoutEffects_begin (react-dom.development.js:24649:1)
I'm using react 18, and react-leaflet 4.0.0
Is there something I have missed in the setup?
There is a bug in 4.0.0, but even when using the latest version, I still get the error now and then. A workaround is to have a unique key on Map Container, for example timestamp. But this result in a memory leak, as each time a map is loaded it consumes more memory

React Leaflet v3 : Good way to create a control

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

Cloning nodes and appending to separate Layers in React-Konva

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);

Resources