How to programmatically get PixiOverlay markers and get their attributes via drawn boundaries - leaflet

I am using React PixiOverlay wrapper (https://github.com/knapcio/react-leaflet-pixi-overlay) which has the nice ability to draw markers at scale on a leaflet map.
While this provides me with a really nice way to draw hundreds of thousands of markers efficiently, I do not know how to programmatically select these markers. I want to be able to draw a shape on the map and select the markers within that geometry. I have the ability to draw the shape, but I don't know how to search and select for the markers inside.
Any help would be very much appreciated.
EDIT: For those wondering how the initial rendering is done, there are an array of markers that are passed, which PixiOverlay reads and the renders. The color, marker type, coordinates, etc are all passed in this array.
import PixiOverlay from 'react-leaflet-pixi-overlay';
import { Map } from 'react-leaflet';
import { renderToString } from 'react-dom/server';
const App = () => {
const markers = [{
id: 'randomStringOrNumber',
iconColor: 'red',
position: [-37.814, 144.96332],
popup: renderToString(
<div>All good!</div>
),
onClick: () => alert('marker clicked'),
tooltip: 'Hey!',
},
{
id: '2',
iconColor: 'blue',
position: [-37.814, 144.96332],
popup: 'Quack!',
popupOpen: true, // if popup has to be open by default
onClick: () => alert('marker clicked'),
tooltip: 'Nice!',
}
];
return {
<Map
preferCanvas={true}
maxZoom={20}
minZoom={3}
center={[-37.814, 144.96332]}
// Other map props...
>
<TileLayer
url="https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"
attribution='© OpenStreetMap contributors'
/>
<PixiOverlay markers={markers} />
</Map>
};
}

Related

Multiple Doughnut chart as markers over Openstreetmap

I'm in trouble while implementing the doughnut chart over OpenStreetMap. I'm using react-chartjs2 for the doughnut chart and react-leaflet for Openstreetmap. Like we use the location icon on different coordinates over the map but here I want to use a Doughnut graph over the map instead of the location icon.
I want to achieve something like this
As per the react-leaflet documentation, the Marker icon property accepts two types of icons that is icon strings like image URL and divIcon which can be some HTML elements but while I'm rendering react component it does not accept and not showing it.
Here you can check in codesandbox I have added code to make it easy to try
https://codesandbox.io/s/doughnut-chart-over-osm-map-1indvl?file=/src/App.js
For what I know marker Icons can only be static, I use a function to create my only markers based on icons and plain html. Will be hard to do that with a component in your case.
My icon render function
import { divIcon } from "leaflet";
import { ReactElement } from "react";
import { renderToString } from "react-dom/server";
export const createLeafletIcon = (
icon: ReactElement,
size: number,
className?: string,
width: number = size,
height: number = size
) => {
return divIcon({
html: renderToString(icon),
iconSize: [width, height],
iconAnchor: [width / 2, height],
popupAnchor: [0, -height],
className: className ? className : "",
});
};
In your case I would try to cheese it and create blank markers and show the graph in popups instead and just force the popups to alway stay open.
EDIT: Added my custom Marker code below that have some nice options.
You can just use the defaultOpen option, and add the graph as a child component to the marker and it will show up in the popup. You can the change the styling of you liking to make it look like the graph is the marker.
import { LatLngLiteral } from "leaflet";
import React, { Children, ReactElement, useEffect, useRef } from "react";
import { Marker, Popup, useMap } from "react-leaflet";
import { MapPin } from "tabler-icons-react";
import { createLeafletIcon } from "./utils";
export interface LeafletMarkerProps {
position: LatLngLiteral;
flyToPosition?: boolean;
size?: number;
color?: string;
icon?: ReactElement;
defaultOpen?: boolean;
onOpen?: () => void;
children?: React.ReactNode;
markerType?: string;
zIndexOffset?: number;
}
const LeafletMarker: React.FC<LeafletMarkerProps> = ({
position,
flyToPosition = false,
children,
size = 30,
color,
defaultOpen = false,
onOpen,
icon = <MapPin size={size} color={color} />,
markerType,
zIndexOffset,
}) => {
const map = useMap();
const markerRef = useRef(null);
position && flyToPosition && map.flyTo(position);
const markerIcon = createLeafletIcon(icon, size, markerType); // Important to not get default styling
useEffect(() => {
if (defaultOpen) {
try {
// #ts-ignore
if (markerRef.current !== null && !markerRef.current.isPopupOpen()) {
// #ts-ignore
markerRef.current.openPopup();
}
} catch (error) {}
}
}, [defaultOpen, position.lat, position.lng]);
return (
<Marker
eventHandlers={{
popupopen: () => onOpen && onOpen(),
}}
ref={markerRef}
icon={markerIcon}
position={position}
zIndexOffset={zIndexOffset}
>
{/* autoPan important to not have jittering */}
<Popup autoPan={false}>{children}</Popup>
</Marker>
);
};
export default LeafletMarker;

How to Stop Markers Moving on Zoom in Mapbox GL JS

I have tried previous answers with no success.
On initial load custom markers are in the correct position, though I do have to manually shift them to the right by 220px to achieve this. There is no zoom level set due to the fitBounds we use, described in the next paragraph.
On zooming in or out, the markers lose their position - their position is only correct for a certain zoom level. I am using a Turf.js bbox to calculate the bounding box we can use in the fitBounds function (though this is slightly out with me having to shift the markers to the right 220px).
In my _app.js (Next.js) I import the css
import 'mapbox-gl/dist/mapbox-gl.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp
This is my Map component
import React, { useRef, useEffect } from 'react'
import mapboxgl from 'mapbox-gl'
import * as turf from '#turf/turf'
mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN
export default function App() {
const mapContainer = useRef(null)
const map = useRef(null)
var geojson = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: { price: 399, beds: 2 },
geometry: { type: 'Point', coordinates: [115.1848543, -8.721661] },
},
{
type: 'Feature',
properties: { price: 450, beds: 3 },
geometry: { type: 'Point', coordinates: [115.1676773, -8.72259] },
},
],
}
var bbox = turf.bbox(geojson)
useEffect(() => {
if (map.current) return // initialize map only once
map.current = new mapboxgl.Map({
container: mapContainer.current,
style: 'mapbox://styles/mapbox/streets-v11',
})
for (const marker of geojson.features) {
// Create a DOM element for each marker.
const el = document.createElement('div')
//? WE USE ml-[220px] AS THE MARKERS APPEAR 220PX TO THE LEFT OF WHERE THEY SHOULD BE
el.className =
'flex items-center justify-center h-[30px] px-3 tracking-wide font-extrabold rounded-full bg-zinc-100 border border-zinc-300 shadow-md text-[14px] text-black ml-[220px]'
el.innerText = '$' + marker.properties.price
// Add markers to the map.
new mapboxgl.Marker(el).setLngLat(marker.geometry.coordinates).addTo(map.current)
}
map.current.fitBounds(bbox, { padding: 100 })
})
return <div ref={mapContainer} className='w-full h-screen' />
An example can be found here
Any help would be very much appreciated.
On initial load custom markers are in the correct position, though I do have to manually shift them to the right by 220px to achieve this.
This doesn't make sense. If you have to move them 220px, they're clearly not in the right position. Presumably, the longitude of the point is wrong, and by offsetting it 220px it temporarily looks right - but as soon as the zoom changes, it will be wrong by a different amount.
You need to:
get the right lat/longs for your markers
make sure your marker images don't contain extra white space
set icon-anchor correctly for your image design

Update style of individual feature from single geoJSON source on Mapbox map, when clicked

I'm working with Mapbox GL JS to plot geoJSON data on a map using their external geoJSON example as a starting point. The geoJSON file contains lots of features which are plotted as individual markers on the same layer. I would like to highlight the clicked marker by changing its colour from red to blue. I have adapted the example to show a pop-up with the point id when clicked (just as a proof of concept that the markers can fire events when clicked), however, I'm struggling to find a way to change the styling of the individual clicked marker.
The code is currently as follows:
mapboxgl.accessToken = 'pk.eyJ1IjoiZGFuYnJhbWFsbCIsImEiOiJjbDB3ODFveHYxOG5rM2pubWpwZ2R1Y2xuIn0.yatzJHqBTjQ6F3DHASlriw';
const map = new mapboxgl.Map({
container: 'map', // container ID
style: 'mapbox://styles/mapbox/satellite-v9', // style URL
zoom: 7, // starting zoom
center: [138.043, 35.201] // starting center
});
map.on('load', () => {
map.addSource('earthquakes', {
type: 'geojson',
data: 'https://docs.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson'
});
map.addLayer({
'id': 'earthquakes-layer',
'type': 'circle',
'source': 'earthquakes',
'paint': {
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-color': 'red',
'circle-stroke-color': 'white'
}
});
});
map.on('click', 'earthquakes-layer', (e) => {
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML('Id: ' + e.features[0].properties.id)
.addTo(map);
});
Here is a codepen: https://codepen.io/danb1/pen/BaYjOyx
Is it the case that it's actually not possible to use this approach, and instead each feature from the geoJSON file needs to be plotted as a separate layer? I'm struggling to find any examples of this and am not able to modify the geoJSON source — it has to come from one single file (rather than loading multiple geoJSON files separately on separate layers).
This is possible using feature-state. The first thing to do is to ensure the layer data contains ids for each feature (in the example the source data doesn't so we need to add generateId: true to the map.addSource method).
We then need to add mousemove and mouseleave events to the map to store the moused-over feature id (if there is one, i.e. if the mouse is hovering over a feature):
let hoveredEarthquakeId = null;
map.on('mousemove', 'earthquakes-layer', (e) => {
map.getCanvas().style.cursor = 'pointer';
if (e.features.length > 0) {
map.setFeatureState(
{ source: 'earthquakes', id: e.features[0].id },
{ hover: true }
);
hoveredEarthquakeId = e.features[0].id;
}
});
map.on('mouseleave', 'earthquakes-layer', () => {
map.getCanvas().style.cursor = '';
if (hoveredEarthquakeId !== null) {
map.setFeatureState(
{ source: 'earthquakes', id: hoveredEarthquakeId },
{ hover: false }
);
}
hoveredEarthquakeId = null;
});
Finally, in the layer properties, the colour setting of the circle needs to be updated to reflect the hover value stored against the feature:
'circle-color': [
'case',
['boolean', ['feature-state', 'hover'], false],
'#00f',
'#f00'
],
The final thing can be seen in the modified pen. There is also a MapBox tutorial covering this kind of thing in a slightly more complicated way, which I hadn't come across until now: https://docs.mapbox.com/help/tutorials/create-interactive-hover-effects-with-mapbox-gl-js/.

mapbox-gl-js create a circle around a lat/lng?

I need to create a circle around a point where a user clicks. How would I do this? Every tutorial shows extracting a circle from a geojson source and not creating one. Need to be able to edit the radius as well.
Did you try something yourself? Following the mapbox examples you should be able to get an idea of how to build something like that.
You would need to do 3 things:
Create a source that holds the data
Create a layer of type "circle" for displaying the data as circles
On every click of the user, extract the "latitude, longitude" and add a point to your data list. Then display all of those points as a circle on the map.
This is an example of how I would have coded that: https://jsfiddle.net/andi_lo/495t0dx2/
Hope that helps you out
mapboxgl.accessToken = '####';
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/light-v9', //stylesheet location
center: [-74.50, 40], // starting position
zoom: 9 // starting zoom
});
map.on('load', () => {
const points = turf.featureCollection([]);
// add data source to hold our data we want to display
map.addSource('circleData', {
type: 'geojson',
data: {
type: 'FeatureCollection',
features: [],
},
});
// add a layer that displays the data
map.addLayer({
id: 'data',
type: 'circle',
source: 'circleData',
paint: {
'circle-color': '#00b7bf',
'circle-radius': 8,
'circle-stroke-width': 1,
'circle-stroke-color': '#333',
},
});
// on user click, extract the latitude / longitude, update our data source and display it on our map
map.on('click', (clickEvent) => {
const lngLat = new Array(clickEvent.lngLat.lng, clickEvent.lngLat.lat);
points.features.push(turf.point(lngLat));
map.getSource('circleData').setData(points);
});
});
#map {
height: 500px;
}
<div id="map"></div>

How to apply css on polylines : leaflet

I am working with the application which uses leaflet api.
Introduction
I needed to draw different types of fences, using decorators i can somewhat apply good visuals to the polylines but not much.
Problem
I was willing to show twisted wires instead of dashes, dots or plain lines and I know the twisted wire line will be an image but can't find help about applying custom css to polylines.
Script Example
var fence2Icon = L.icon({
iconUrl: 'xxxx.png',
iconSize: [5, 20]
iconAnchor: [5, 18]
});
// Add coordinate to the polyline
var polylineFence2 = new L.Polyline([], { color: 'red' });
function fencePlace2(e) {
// New marker on coordinate, add it to the map
new L.Marker(e.latlng, { icon: fence2Icon, draggable: false }).addTo(curr);
// Add coordinate to the polyline
polylineFence2.addLatLng(e.latlng).addTo(curr);
var decorator = L.polylineDecorator(polylineFence2, {
patterns:[{offset:5,repeat:'20px',symbol:new L.Symbol.Dash({pixelSize:5})
}]
}).addTo(curr);
}
L.easyButton('fa-flash', function () {
$('.leaflet-container').css('cursor', 'crosshair');
map.on('click', fencePlace2);
polylineFence2 = new L.Polyline([], { color: 'red' });
}).addTo(map);
If someone know anything about polyline or another way please do help.
Thanks for your time:-)
You can add a class in the options of your polyline ...
var polyline = L.polyline(latlngs, { className: 'my_polyline' }).addTo(map);
and add your own settings in the CSS ...
.my_polyline {
stroke: green;
fill: none;
stroke-dasharray: 10,10;
stroke-width: 5;
}
Here is an example: http://jsfiddle.net/FranceImage/9dggfhnc/
You can also access some options directly ...
var polyline = L.polyline(latlngs, { dashArray: '10,10' }).addTo(map);
See Path Options
If you create a polyline you're in fact adding an element to the SVG element which Leaflet uses to draw it's overlays. Styling SVG path elements is different from styling regular HTML elements. There's no such thing as border and background-color etc. It has different properties, if you're interested here's a nice read on the matter:
https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Fills_and_Strokes
You can style Leaflet's path elements when you instanciate them via options or via CSS using the properties (related to styling) described in the documentation here:
http://leafletjs.com/reference.html#path
Via options:
new L.Polyline([...], {
weight: 3,
color: 'red',
opacity: 0.5
}).addTo(map);
Via CSS:
new L.Polyline([...], {
className: 'polyline'
}).addTo(map);
.polyline {
weight: 3,
color: red,
opacity: 0.5
}
However, what you want, using an image simply isn't possible. You can use images as fill for SVG paths, but it would be impossible for your usecase. You'de need to add a pattern definition to the SVG Leaflet is using and then you could use that id as the fill property as outlined in this answer:
https://stackoverflow.com/a/3798797/2019281 but will always fill/tile the image horizontally which won't work if your polyline is vertical.