Check if a GeoJSON source is present in mapbox viewport - mapbox

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

Related

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

How can I add Mapbox vector tile layer from Geoserver to Mapbox?

I have published a shapefile as a vector tile on GeoServer according to this. Now, how can I add this layer to Mapbox?
I would be very pleased if you can help me.
I have been trying to get that to work and for a vector layer you must specify 'source-layer' attribute which is the name of a layer on your GeoServer (ie. 'states' if using topp workspace) here is a code example that eventually worked for me:
<script>
mapboxgl.accessToken =
'<YOUR MAPBOX TOKEN HERE>';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
center: [-100, 40],
zoom: 12,
});
map.on('load', function () {
map.addSource('wms-test-source', {
type: 'vector',
// use the tiles option to specify a WMS tile source URL
// https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/
tiles: [
'http://<YOUR GEOSERVER ADDRESS>:8080/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS&VERSION=1.0.0&LAYER=topp:states&STYLE=&TILEMATRIX=EPSG:900913:{z}&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile&TILECOL={x}&TILEROW={y}',
],
'minZoom': 0,
'maxZoom': 14,
});
map.addLayer(
{
'id': 'wms-test-layer',
'type': 'fill',
'source': 'wms-test-source',
'source-layer': 'states',
'paint': { 'fill-color': '#00ffff' },
}
//'aeroway-line'
);
});
</script>
For more info go to GeoServer documentation at GeoServer - Mapbox Style Specification
The code provided at the link actually shows how to add the GeoServer layer to your Mapbox map. Assuming that you want to add the layer to your map with Mapbox GL JS (since your post is tagged with mapbox-gl-js, your code would look something like this:
mapboxgl.accessToken = /* YOUR MAPBOX ACCESS TOKEN HERE */;;
var map = new mapboxgl.Map({
container: 'map', // container id
style: 'mapbox://styles/mapbox/streets-v11', // stylesheet location
center: [-74.5, 40], // starting position [lng, lat]
zoom: 9 // starting zoom
});
// Add the GeoServer layers as a source to your map.
map.addSource(
"<source-name>": {
"type": "vector",
"tiles": [
"http://localhost:8080/geoserver/gwc/service/wmts?REQUEST=GetTile&SERVICE=WMTS
&VERSION=1.0.0&LAYER=<workspace>:<layer>&STYLE=&TILEMATRIX=EPSG:900913:{z}
&TILEMATRIXSET=EPSG:900913&FORMAT=application/vnd.mapbox-vector-tile
&TILECOL={x}&TILEROW={y}"
],
"minZoom": 0,
"maxZoom": 14
}
);
// Style the GeoServer source in order to display it visually on your map
map.addLayer({
'id': 'geoserver-layer',
'type': /* fill in based on options here: https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#type */,
'source': '<source-name>'
/* Add any additional properties, full details here: https://docs.mapbox.com/mapbox-gl-js/style-spec/layers/#type */
});
In short, the GeoServer documentation link provides the code for adding a source to your map, and then you should add a layer to style the source visually on your map.
I'd recommend taking a look at the many Mapbox GL JS examples to get a sense of how sources and layers can be added, styled, modified, etc.

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>

Mapbox GL JS: Display map labels above layer?

I'm using Mapbox GL JS, with a Mapbox Streets base layer. I have added a polygon layer with white borders and a transparent fill, but I'm finding it hard to read the basemap labels underneath the polygon layer. See how the labels are covered by the white borders:
Is there any way I can make sure the labels are on top of the polygon layer, or at least not obscured by it?
My code looks like this:
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v9',
center: [-2.839, 54.579],
zoom: 4
});
map.on('load', function () {
map.addSource('constituencies', {
'type': 'vector',
'url': 'mapbox://pcsu.xxx'
});
var constituencyLayer = map.addLayer({
'id': 'constituencies',
'source': 'constituencies',
'source-layer': 'constituencies',
'type': 'fill',
'paint': {
'fill-color': 'rgba(162,181,205,0.6)',
'fill-outline-color': 'white'
},
'layout': {
'visibility': 'visible'
}
});
Have a look at this Mapbox example and use the second argument of map.addLayer(layer, [before]). If this layer before exists, the new layer will be placed before/below it. I think the argument naming is a bit confusing here.
The name of the lowest label layer depends on the style. Most of the time you're looking for housenum-label. There's also a discussion in the Mapbox github issues how to ease this process, e.g by introducing placeholder layers.