Combining react-leaflet and leaflet-pixi-overlay - reactjs

I am trying to display a complex map with many moving markers, with different icons etc... I have a pure react / react-leaflet solution, but it start to struggle with ~1k markers moving every second.
It looks like leaflet-pixi-overlay could be a way of really speeding things up. But I am just starting with the whole chain (react/javascript/maps/leaflet/etc..) and have problems when connecting all this.
Right now I am just trying to display a polygon in my overlay, and nothing is rendered. It turns out that the translation of lat/long to x/y is failing for that polygon's points. Pixi's latLngToLayerPoint function returns 'infinity' for x and y.
This seems to come from the fact that the zoom is not defined for the layer in question (it is 'infinity' also).
Even if I call latLngToLayerPoint with the zoom setting from the map, things fail too (x/y values are not infinite any more, but they are way out there).
This is my code:
import React, { useEffect, useRef } from 'react';
import { Map, TileLayer } from 'react-leaflet'
import "leaflet/dist/leaflet.css"
import * as PIXI from 'pixi.js'
import 'leaflet-pixi-overlay' // Must be called before the 'leaflet' import
import L from 'leaflet';
let polyLatLngs = [[50.630, 13.047], [50.645, 13.070], [50.625, 13.090], [50.608, 13.070], [50.630, 13.047]]
let pixiContainer = new PIXI.Container()
let prevZoom
let firstDraw = true;
let projectedPolygon;
var shape = new PIXI.Graphics();
pixiContainer.addChild(shape);
let myOverlay = L.pixiOverlay(function (utils) {
let map = utils.getMap()
let zoom = map.getZoom()
console.log('map zoom=' + zoom + ', center=' + map.getCenter())
console.log(' bounds=' + JSON.stringify(map.getBounds()))
console.log(' size=' + map.getSize() + ', panes=' + JSON.stringify(map.getPanes()))
if (map) {
var container = utils.getContainer();
var renderer = utils.getRenderer();
var project = utils.latLngToLayerPoint;
var scale = utils.getScale();
if (firstDraw) {
projectedPolygon = polyLatLngs.map((coords, index) => {
console.log('i=' + index + ', coords=' + coords + ', proj=' + project(coords))
return project(coords)
// return project(coords, zoom) // : this fails too
})
}
if (firstDraw || prevZoom !== zoom) {
shape.clear();
shape.lineStyle(3 / scale, 0x3388ff, 1);
shape.beginFill(0x3388ff, 0.2);
projectedPolygon.forEach(function (coords, index) {
if (index === 0) shape.moveTo(coords.x, coords.y);
else shape.lineTo(coords.x, coords.y);
});
shape.endFill();
}
firstDraw = false;
prevZoom = zoom;
renderer.render(container);
}
}, pixiContainer)
function PxMap(props) {
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current !== null) {
let map = mapRef.current.leafletElement;
console.log('useEffect: add overlay ')
console.log(JSON.stringify(map.getPane('overlayPane').childElementCount))
myOverlay.addTo(map);
console.log(JSON.stringify(map.getPane('overlayPane').childElementCount))
}
}, [mapRef]);
return (
<div style={{ flexgrow: 1, height: '100%' }}>
<Map
preferCanvas={true}
ref={mapRef}
style={{ height: '100%' }}
center={[50.63, 13.047]}
zoom={12}
>
<TileLayer
attribution='&copy OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png?"
/>
</Map>
</div>
)
}
export default PxMap
I think that things are connected correctly between React and leaflet, the map displays ok, I can see the overlay being added etc...
BUT there is a connection missing somewhere, to give more context / information to PIXI.
Any ideas? Thanks!

Finally found the problem, drilling down into the leaflet-pixi-overlay lib.
The solution is to define the minZoom and maxZoom in the Map element:
<Map
preferCanvas={true}
ref={mapRef}
style={{ height: '100%' }}
center={[50.63, 13.047]}
zoom={12}
minZoom={ 9} // Add these options...
maxZoom={ 16} //
Internally, L.PixiOverlay.js relies on these two values to define:
// return the layer projection zoom level
projectionZoom: function (map) {return (map.getMaxZoom() + map.getMinZoom()) / 2;},
Which in turn is used to define the default zoom setting?
this._initialZoom = this.options.projectionZoom(map);
....
zoom = (zoom === undefined) ? _layer._initialZoom : zoom;
var projectedPoint = map.project(L.latLng(latLng), zoom);
Not sure why this is done this way, but setting these options solves the problem.

Related

Need dynamic icon for MarkerClusterer in #react-google-maps/api

TL;DR: In #react-google-maps/api, I want to be able to make dynamic cluster icons/symbol in the style of pie charts, based on the markers in the cluster, but it seems I can only make icons from a static array, and cannot pass the the markers as parameters.
Full Description:
I am using typescript react with the package #react-google-maps/api, and I'm trying to find a way with the ClustererComponent/MarkerClusterer to take a callback or similar in order to be able to be able to create an svg for each cluster based on the markers in the given cluster.
The current issue is that the way I understand it, I am limited to a static array of urls to icons, and thought I can make an svg in those, I have no way to pass parameters into those svgs, as the only way the package allows me to chose a style is thought index in the style array.
I have read thought the following material, but have not been able to get find a way to make an icon dynamically based on the markers:
Documentation for #react-google-maps/api: https://react-google-maps-api-docs.netlify.app/#markerclustere
Documentation for google maps markerclusterer: https://developers.google.com/maps/documentation/javascript/marker-clustering
I have found libraries like this: https://github.com/hassanlatif/google-map-chart-marker-clusterer, that should be able to be used as a solution, but they don't seem to work with the #react-google-maps/api, only with earlier versions of google map. If this is not the case, and these can be used directly, then I would be more then happy with an answer describing how to use libraries like the one above with #react-google-maps/api, as that should allow be to make clusters in the same way as the picture below.
EDIT: as I got reminded in the comments, here is the code I have so far:
What I've tried: I have tried to find any way to set in an svg element instead of a url, but have since just decided to make a url with the svg data, as shown below. I have tried to edit the url of the clusters under the MarkerClusterer thought the callback for onClusteringBegin, onClusteringEnd and onLoad, but so far, no luck.
How I make the svg into url-data, so it can be used for img src
/*
* Pie Chart SVG Icon in URL form
*
* Inspiration taken from: https://medium.com/hackernoon/a-simple-pie-chart-in-svg-dbdd653b6936
*
* Note: As of right now, I am identifying the difference in marker types by setting the type-number I use in the title of the marker
*/
const serializeXmlNode = (xmlNode: any) => {
if (typeof window.XMLSerializer != "undefined") {
return (new window.XMLSerializer()).serializeToString(xmlNode);
} else if (typeof xmlNode.xml != "undefined") {
return xmlNode.xml;
}
return "";
}
function getCoordinatesForPercent(percent: number) {
const x = Math.cos(2 * Math.PI * percent);
const y = Math.sin(2 * Math.PI * percent);
return [x, y];
}
const makePieChartIcon = (slices: any[]) => {
const svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg')
svg.setAttribute('viewBox', '-1.1 -1.1 2.2 2.2')
svg.setAttribute('style', 'transform: rotate(-90deg)')
svg.setAttribute('height', '60')
var circle = document.createElementNS(svgNS, 'circle')
circle.setAttribute('r', '1.1')
circle.setAttribute('fill', 'white')
svg.appendChild(circle);
let cumulativePercent = 0;
slices.map((slice: any) => {
const [startX, startY] = getCoordinatesForPercent(cumulativePercent);
cumulativePercent += slice.percent;
const [endX, endY] = getCoordinatesForPercent(cumulativePercent);
const largeArcFlag = slice.percent > .5 ? 1 : 0;
const pathData = [
`M ${startX} ${startY}`, // Move
`A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
`L 0 0`, // Line
].join(' ');
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', pathData);
path.setAttribute('fill', slice.color);
svg.appendChild(path);
})
var svgUrl = 'data:image/svg+xml;charset=UTF-8,' + serializeXmlNode(svg)
return svgUrl
}
const makeDynamicClusterIcon = (markers: any[]) => {
var numMarkers = markers.length;
var slices = markers.reduce((acc: any, marker: any) => {
acc[parseInt(marker.title)].percent += 1 / numMarkers;
return acc;
}, [
{ percent: 0, color: 'Green' },
{ percent: 0, color: 'Blue' },
{ percent: 0, color: 'Red' },
])
var newIconURL = makePieChartIcon(slices)
return newIconURL;
}
How I use the MarkerClusterer Component
<MarkerClusterer
options={{
averageCenter: true,
styles: clusterStyles,
}}
>
{(clusterer) =>
markerData.map((marker: any) => (
<Marker
key={marker.key}
title={String(marker.type)}
position={{ lat: marker.lat, lng: marker.lng }}
clusterer={clusterer}
/>
))
}
</MarkerClusterer>
Right now, I can only use some static styles, but I have them as the following for testing:
const clusterStyles = [
{
height: 50, textColor: '#ffffff', width: 50,
url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="green" /%3E%3C/svg%3E',
},
{
height: 50, textColor: '#ffffff', width: 50,
url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="red" /%3E%3C/svg%3E',
}
];
I found a solution, by finding out that the style array for each cluster (ClusterStyles) can be changed, and then I have change it with the data from the specific markers in the given cluster. I ended up doing this in the callback onClusteringEnd, as here:
{/* Added to the MarkerClusterer */}
onClusteringEnd={(clusterer) => {
clusterer.clusters.map((cluster) => {
cluster.clusterIcon.styles = makeDynamicClusterIcon(cluster.markers)
})
}}
And I changed the last line with return of the makeDynamicClusterIcon function I showed above to instead say:
return [{ url: newIconURL, height: 60, width: 60, textColor: '#FFFFFF', textSize: 22 }];

Load SVG div to React component

I'm using an NPM package that draws a fretboard using D3 (https://github.com/txels/fretboard) .It outputs a div with SVG. I have it working with a normal HTML page but if I try and load it to a React component with code below it gets appended to end of page rather than in div where I have the expression.
Any ideas how I get this generated div in the component div?
import React from "react";
import { Fretboard, Tunings } from "fretboards";
const GenerateFret = () => {
const config = {
frets: 12, // Number of frets to display
startFret: 0, // Initial fret
strings: 6, // Strings
tuning: Tunings.guitar6.standard, // Tuning: default = Standard Guitar
fretWidth: 50, // Display width of frets in pixels
fretHeight: 20, // Display heigh of frets in pixels
leftHanded: false, // Show mirror image for left handed players
showTitle: true, // Set the note name as the title, so it will display on hover
where="#fret"
};
const notes =
"6:e2 6:f2 6:f#2 6:g2 6:g#2 6:a2 6:a#2 6:b2 6:c3 6:c#3 6:d3 6:d#3 6:e3 " +
"5:a2 5:a#2 5:b2 5:c3 5:c#3 5:d3 5:d#3 5:e3 5:f3 5:f#3 5:g3 5:g#3 5:a3 " +
"4:d3 4:d#3 4:e3 4:f3 4:f#3 4:g3 4:g#3 4:a3 4:a#3 4:b3 4:c4 4:c#4 4:d4 " +
"3:g3 3:g#3 3:a3 3:a#3 3:b3 3:c4 3:c#4 3:d4 3:d#4 3:e4 3:f4 3:f#4 3:g4 " +
"2:b3 2:c4 2:c#4 2:d4 2:d#4 2:e4 2:f4 2:f#4 2:g4 2:g#4 2:a4 2:a#4 2:b4 " +
"1:e4 1:f4 1:f#4 1:g4 1:g#4 1:a4 1:a#4 1:b4 1:c5 1:c#5 1:d5 1:d#5 1:e5";
let board = Fretboard(config);
board.draw(notes);
};
const GameArea = () => {
return (
<div id="fret" className="col-8 border border-primary fb-container">
{GenerateFret()}
</div>
);
};
export default GameArea;
There is a config for [where] which changes the D3 selection from body.
I tried adding an ID to the div and then passing in where="#fret" to the config const but this stops any visual from being output.
Manually changing the DOM in React components is an example of side effect and therefore should be put inside useEffect
Try:
const GameArea = () => {
useEffect(()=>{
GenerateFret(); // call function inside useEffect
}, []) // you need to run it only once, so pass empty array
return (
<div id="fret" className="col-8 border border-primary fb-container"/>
);
};
This is the whole code:
import React from "react";
import { Fretboard, Tunings } from "fretboards";
const GenerateFret = () => {
const config = {
frets: 12, // Number of frets to display
startFret: 0, // Initial fret
strings: 6, // Strings
tuning: Tunings.guitar6.standard, // Tuning: default = Standard Guitar
fretWidth: 50, // Display width of frets in pixels
fretHeight: 20, // Display heigh of frets in pixels
leftHanded: false, // Show mirror image for left handed players
showTitle: true, // Set the note name as the title, so it will display on hover
where="#fret"
};
const notes =
"6:e2 6:f2 6:f#2 6:g2 6:g#2 6:a2 6:a#2 6:b2 6:c3 6:c#3 6:d3 6:d#3 6:e3 " +
"5:a2 5:a#2 5:b2 5:c3 5:c#3 5:d3 5:d#3 5:e3 5:f3 5:f#3 5:g3 5:g#3 5:a3 " +
"4:d3 4:d#3 4:e3 4:f3 4:f#3 4:g3 4:g#3 4:a3 4:a#3 4:b3 4:c4 4:c#4 4:d4 " +
"3:g3 3:g#3 3:a3 3:a#3 3:b3 3:c4 3:c#4 3:d4 3:d#4 3:e4 3:f4 3:f#4 3:g4 " +
"2:b3 2:c4 2:c#4 2:d4 2:d#4 2:e4 2:f4 2:f#4 2:g4 2:g#4 2:a4 2:a#4 2:b4 " +
"1:e4 1:f4 1:f#4 1:g4 1:g#4 1:a4 1:a#4 1:b4 1:c5 1:c#5 1:d5 1:d#5 1:e5";
let board = Fretboard(config);
board.draw(notes);
};
const GameArea = () => {
useEffect(()=>{
GenerateFret(); // call function inside useEffect
}, []) // you need to run it only once, so pass empty array
return (
<div id="fret" className="col-8 border border-primary fb-container"/>
);
};

How to create and set a setMapTypeId using react-google-maps

I was looking for a way to create my own mars map in a website, using google maps.
I found this example in google map api
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
center: {lat: 0, lng: 0},
zoom: 1,
streetViewControl: false,
mapTypeControlOptions: {
mapTypeIds: ['moon']
}
});
var moonMapType = new google.maps.ImageMapType({
getTileUrl: function(coord, zoom) {
var normalizedCoord = getNormalizedCoord(coord, zoom);
if (!normalizedCoord) {
return null;
}
var bound = Math.pow(2, zoom);
return '//mw1.google.com/mw-planetary/lunar/lunarmaps_v1/clem_bw' +
'/' + zoom + '/' + normalizedCoord.x + '/' +
(bound - normalizedCoord.y - 1) + '.jpg';
},
tileSize: new google.maps.Size(256, 256),
maxZoom: 9,
minZoom: 0,
radius: 1738000,
name: 'Moon'
});
map.mapTypes.set('moon', moonMapType);
map.setMapTypeId('moon');
}
// Normalizes the coords that tiles repeat across the x axis (horizontally)
// like the standard Google map tiles.
function getNormalizedCoord(coord, zoom) {
var y = coord.y;
var x = coord.x;
// tile range in one direction range is dependent on zoom level
// 0 = 1 tile, 1 = 2 tiles, 2 = 4 tiles, 3 = 8 tiles, etc
var tileRange = 1 << zoom;
// don't repeat across y-axis (vertically)
if (y < 0 || y >= tileRange) {
return null;
}
// repeat across x-axis
if (x < 0 || x >= tileRange) {
x = (x % tileRange + tileRange) % tileRange;
}
return {x: x, y: y};
}
/* Always set the map height explicitly to define the size of the div
* element that contains the map. */
#map {
height: 100%;
}
/* Optional: Makes the sample page fill the window. */
html, body {
height: 100%;
margin: 0;
padding: 0;
}
<div id="map"></div>
<!-- Replace the value of the key parameter with your own API key. -->
<script
async
defer
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyCkUOdZ5y7hMm0yrcCQoCvLwzdM6M8s5qk&callback=initMap">
</script>
https://jsfiddle.net/dobleuber/319kgLh4/
It works perfect, but I would like to create the same thing with react using react-google-maps.
I looked out in the react-google-maps code but I only see getters no setters for the map props:
getMapTypeId, getStreetView, ect.
Is there any way to achieve this without modify the react-google-maps code?
Thanks in advance
use props mapTypeId="moon" in react-google-maps
I've found a better way to solve this that preserve the changes on re-render, leaving it here to anyone who comes here.
there is an onLoad function that exposes a map instance, we can use this to set mapTypeId instead of passing it as an option. In this way, if the user changes the map type later, it will preserve the changes on re-render.
<GoogleMap
onLoad={(map) => {
map.setMapTypeId('moon');
}}
/>

Creating an array of styles in React

For certain reasons I need to create an array of different styles to eventually use at certain times. Regardless I have this bit of code...
export const carouselData = {
cdata: [{
bgimage: require('Assets/img/Banners/mybanner1.jpg')
},{
bgimage: require('Assets/img/Banners/mybanner2.jpg'),
}]
}
...
var mySectionStyle
this.props.cdata.cdata.map((carouselData, key) => (
mySectionStyle[key] = {
backgroundImage: "url(" + carouselData.bgimage + ")"
}
))
return (
{ this.props.cdata.cdata.map((carouselData, key) => (
<div className="bg_image" style={ sectionStyle[key] }>
//Some stuff here
</div>
))}
)
Now to anyone that is half decent at coding probably sees huge issues with this code but as a newbie I need help finishing it (or rewriting).
Can anyone help me create an array so I can access my styles one by one with mySectionStyle[0], mySectionStyle[1], mySectionStyle[2] etc
Edit. I have an array that has many images in it and I want those in an array so I can set the carousel up with different background images.
Why can't you just do:
var mySectionStyle = {
style1: {
margin: 0,
},
style2: {
margin: 10,
},
}
const style1 = mySectionStyle['style1'];
const style2 = mySectionStyle['style2'];
If you later need it in an array, you can use the Object methods to convert it.
const availableStyles = Object.keys(mySectionStyle); // ['style1', 'style2']
availableStyles.forEach(style => mySectionStyle[style].backgroundImage = `url(${carouselData.bgimage})`;);
See also Object.values and Object.entries for other conversion to array options.

Troubles layering map tiles on top of each other with React, D3v5, openstreetmaps?

I'm trying to have multiple map layers of maps on top of each other using react, d3, and openstreetmaps. I originally got started with React and the react-simple-maps library a few months ago, and so far I've been able to replicate this setup of getting a topojson file into d3 to generate the map, which is much more customize-able. The shapefiles you can download give you lines around the countries, or the states or counties or even US zip codes...pretty cool!
An open source (hint: free!) way to get a more granular, street-level view, along with other features like rivers, lakes etc would be a nice add-on to the maps I am building for my data visualizations. So far, I have the d3 map in place, but really confused on the openstreetmap, which for some reason insists on being positioned at the bottom right corner of the page, and not direclty overtop of the d3 map? I know there are lot of other libraries, including the not-so-free resources offered by google and mapbox and the other big players in the map game, but I'm close to getting this working...any ideas where I'm going wrong? I think it has something to do with grouping the svg maps properly but I'm not sure? Here's some code...any ideas are appreciated! A quick note is that these topojson files are large and can be slow to load, so I use axios to fetch them from a container component higher up in the tree to pass them down as props. Here's an example of that, where you have a nicely positioned map and a stubborn openstreetmap that lives below it. Some of it is removed for brevity, my apologies if it doesn't run?
EDIT: sorry for my WALL of code, after two weeks of no replies, here's a codepen that shows just the openstreetmap as it's own component:
https://stackblitz.com/edit/react-ktjlxk?embed=1&file=index.js
import React, { Component } from "react";
import "./worldMapOSM.css";
import * as d3 from "d3";
import * as d3Tile from "d3-tile";
import { geoMercator, geoPath } from "d3-geo";
//styles for map projection
const width = 960;
const height = 500;
class WorldMapOSM extends Component {
projection() {
return geoMercator() //geoConicConformal
.scale(120)
.center([6.8682168, 52.3401469])
.translate([width / 2, height / 2])
.clipExtent([[0, 0], [width, height]]) //????
.precision(0.5);
}
componentDidMount() {
const width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight),
prefix = prefixMatch(["webkit", "ms", "Moz", "O"]);
const svg = d3.select(this.refs.anchor);
const map = d3
.select("body")
.append("div")
.attr("class", "map")
.style("width", width + "px")
.style("height", height + "px")
const layer = map.append("div").attr("class", "layer");
const tile = d3Tile
.tile()
.size([width, height])
.scale(this.projection().scale() * 2 * Math.PI)
.translate(this.projection([0, 0]))
.zoomDelta((window.devicePixelRatio || 1) - 0.5);
const tiles = d3Tile.tile();
const image = layer
.style(prefix + "transform", matrix3d(tiles.scale, tiles.translate))
.selectAll(".tile")
.data(tiles, function(d) {
return d;
})
image.exit().remove();
image
.enter()
.append("img")
.attr("class", "tile")
.attr("src", function(d) {
return (
"http://" +
"abc"[d.y % 3] +
".tile.openstreetmap.org/" +
d.z +
"/" +
d.x +
"/" +
d.y +
".png"
);
})
.style("left", function(d) {
return (d[0] << 8) + "px";
})
.style("top", function(d) {
return (d[1] << 8) + "px";
});
svg
.append("use")
.attr("xlink:href", "#land")
.attr("class", "stroke");
formatLocation(this.projection.invert(d3.mouse(this)), zoom.scale())
function matrix3d(scale, translate) {
var k = scale / 256,
r = scale % 1 ? Number : Math.round;
return (
"matrix3d(" +
[
k,0,0,0,0,k,0,0,0,0,k,0,
r(translate[0] * scale),
r(translate[1] * scale),
0,
1
] +
")"
);
}
function prefixMatch(p) {
var i = -1,
n = p.length,
s = document.body.style;
while (++i < n)
if (p[i] + "Transform" in s) return "-" + p[i].toLowerCase() + "-";
return "";
}
function formatLocation(p, k) {
var format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
return (
(p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + "°N") +
" " +
(p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E")
);
}
}
}
render() {
return (
<div className="worldMap dropShadow">
<svg width={960} height={600} viewBox="0 0 960 600">
<g id="map">
{/* D3 map */}
<g className="eurCountries">
{this.props.Countries.map((d, i) => (
<path
key={`path-${i}`}
d={geoPath().projection(this.projection())(d)}
className="state"
fill={`rgba(187,218,247,${(1 / 1000) * i})`}
stroke="#afafaf"
strokeWidth={0.5}
/>
))}
</g>
<g ref="anchor" />{/* openstreetmap */}
</g>
</svg>
</div>
);
}
}
export default WorldMapOSM;

Resources