How to Stop Markers Moving on Zoom in Mapbox GL JS - 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

Related

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/.

Change the center of React-leaflet

I am trying to dynamically change the center of a map-container with data provided externally. I get the data as a string, and then parse it to get it as numbers instead. But when I enter lat to the const center, I get a NaN when trying use it.
import React from 'react'
import { useCasparData } from 'caspar-graphics'
import { useTimeline } from '#nxtedition/graphics-kit'
import './style.css'
import { MapContainer, TileLayer, Marker, Popup, useMap } from 'react-leaflet'
import './leaflet.css'
export default function Lowerthird () {
const { text01, text02, text03, text04 } = useCasparData()
const lat = parseFloat(text01)
const zoom = 15
const center = [lat, 13.440222]
function onLoad(timeline) {
timeline
.add('start')
.from('.name', { x: -2000 }, 'start')
.from('.titel', { x: -1000 }, 'start')
}
function onStop(timeline) {
timeline
.reverse()
}
//useTimeline(onLoad, onStop)
return (
<MapContainer center={center} zoom={zoom} zoomControl={false}>
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
</MapContainer>
)
}
export const previewData = {
text01: '59.392133',
text02: '13.440222',
text03: '15',
text04: '[59.392133, 13.440222]'
}
I have looked through several threads here, but I have not found a answer that solves this for me... I do realize that the map-container is immutable - I just can't seem to figure out how to update it or set a new center...
(Oh... I am a total noob to react/leaflet, I am just trying to find a simple way to use Openstreetmap as a overlay in our broadcasts (tv))
You can use useMap() from a child component.
And with map, you can use functions such as flyTo to move around on the map.
Sadly useMap needs to be used from the child scope of MapContainer, however, you can just create an invisible component and add as a child and provide it with the new position when you want it to move.
So something like this (I have not tested this code but I have done similar stuff with markers):
interface ChangeCenterProps {
position: { lat: number; lng: number };
}
const ChangeCenter: React.FC<ChangeCenterProps> = ({ position }) => {
const map = useMap();
map.flyTo(position);
return <></>;
};

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

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

Check if a GeoJSON source is present in mapbox viewport

I have a map with several layers of GeoJSON each with their own unique layer name:
var map = new mapboxgl.Map({
container: 'map',
center: [-97.5651505, 37.89549,],
zoom: 4
});
var sources = {
'ord': 'chicago',
'pit': 'pittsburgh',
'atl': 'atlanta'
};
map.on('load', function () {
for (var s in sources) {
map.addSource(s, { type: 'geojson', data: `/geojson/${s}.json` });
map.addLayer({
'id': sources[s],
'type': 'fill',
'source': s,
'layout': {
'visibility': 'visible'
},
'paint': {
'fill-color': '#088',
'fill-opacity': 0.5
}
});
}
});
I would like to check if a user has zoomed in past zoom level 13 evaluate if any of these three layers is in the viewport. If it is I'll take action to add a button to the overlay. However, I'm having issues finding any documentation other than leaflet on how to check if a layer is inside the viewport. I've found some mention of markers that that doesn't seem to apply.
You can achieve this with queryRenderedFeatures which returns an array of features rendered within a given bounding box. However, if you omit the bounding box argument, queryRenderedFeatures will query within the entire viewport. You can also use the options.layers argument to limit your query to specific layers to avoid getting a bunch of features that are in the underlying style (for example, streets and lakes). You can do this query in a zoomend event listener to achieve your desired outcome. Putting it all together would look something like this:
map.on('zoomend', () => {
if (map.getZoom() > 13) {
const visibleFeatures = map.queryRenderedFeatures(null, {layers: ['ord', 'pit', 'atl']});
// if none of the layers are visible, visibleFeatures will be an empty array
if (visibleFeatures.length) {
// figure out which layers are showing and add your button
}
}
});

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>