Using my styled Mapbox GL JS map, with multistring lines from my geojson file, when a user clicks one of the lines I need it highlighted, this part I have working using:
map.on('click', 'route', function(e) {
var features = map.queryRenderedFeatures(e.point, { layers: ['route'] });
if (!features.length) {
return;
}
if (typeof map.getLayer('selectedRoad') !== "undefined" ){
map.removeLayer('selectedRoad')
map.removeSource('selectedRoad');
}
var feature = features[0];
console.log(feature.toJSON());
map.addSource('selectedRoad', {
"type":"geojson",
"data": feature.toJSON()
});
map.addLayer({
"id": "selectedRoad",
"type": "line",
"source": "selectedRoad",
"layout": {
"line-join": "round",
"line-cap": "round"
},
"paint": {
"line-color": "yellow",
"line-width": 8
}
});
Example of my geojson entries:
{"type": "FeatureCollection", "features": [{"type":"Feature","geometry":
{"type":"MultiLineString","coordinates":[[[-117.252069,32.748772],[-117.25169,32.749212],
[-117.25135,32.749608]]]},"properties":{"displaylabel":"1950-1999 GRAND
ST","sidedaytime1":"West Side Fri 7 am-10 am","sidedaytime2":"7am"}},
{"type":"Feature","geometry":{"type":"MultiLineString","coordinates":
[[[-117.25135,32.749608],[-117.250981,32.750037],[-117.250621,32.750452]]]},"properties":
{"displaylabel":"2000-2049 GRAND ST","sidedaytime1":"West Side Fri 7 am-10
am","sidedaytime2":"East Side Wed 7 am-10 am"}}
I need the user to be able to click and highlight 1 to 4 lines, and all 1 to 4 stay highlighted. Additionally, when a user 2nd clicks a highlighted line, I need it "unhighlighted. I cannot find any examples anywhere. Help is much appreciated!
The easiest way to accomplish this is to attach an event handler to the layer containing these lines, and then edit the feature state when a feature is clicked. When you load the layer to the map, you can use an expression to style each feature based on its feature state. Here's how I've handled this situation before:
/*loading the layer using an expression that defines a feature's color based on the
value located in the 'click' property*/
map.on('load', function() {
map.addLayer({
'id': 'route',
'type': 'line',
'source': {/*your source data*/},
'paint': {
'line-color': [
'case',
['boolean', ['feature-state', 'click'], false],
'#0f397d',
'#2e6fd9',
]}
});
});
/*this function would then be used to change the 'click' feature state
when the feature is clicked on*/
map.on('click', 'route', function (e) {
var features = map.queryRenderedFeatures(e.point, { layers: ['route'] });
for (var i = 0; i < features.length; i++) {
map.setFeatureState({source: 'route', id: features[i].id}, {click: true});
}
});
The above code will set any clicked feature to have a 'click' feature state of true, which will then style it using the second color listed in the expression. If you want to change it back on another click, you can set it back to false.
Related
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/.
Sorry of this is a simple question but I have a Mapbox map with 2 layers running on my website. Each layer can be toggled on or off using the example code on the MapBox support site.
However it uses the name of the layers directly for the text of the menu buttons. Is there a way to change this to a more aesthetically pleasing title?
Many thanks
Col
Code added below :
map.on('load', function() {
// add source and layer for 2020 Tour
map.addSource('pjuk-2020-tour', {
type: 'vector',
url: 'mapbox://styles/silentwitness/cka13nxh44g1v1iqd7xxr1nys'
});
map.addLayer({
'id': 'pjuk-2020-tour',
'type': 'circle',
'source': 'pjuk-2020-tour',
'layout': {
// make layer visible by default
'visibility': 'visible'
},
'source-layer': 'pjuk-2020-tour'
});
map.addSource('pjuk-previous-tours', {
type: 'vector',
url: 'mapbox://styles/silentwitness/cka13nxh44g1v1iqd7xxr1nys'
});
map.addLayer({
'id': 'pjuk-previous-tours',
'type': 'circle',
'source': 'pjuk-previous-tours',
'layout': {
// make layer visible by default
'visibility': 'visible'
},
'source-layer': 'pjuk-previous-tours'
});
});
// enumerate ids of the layers
var toggleableLayerIds = ['pjuk-2020-tour', 'pjuk-previous-tours'];
// set up the corresponding toggle button for each layer
for (var i = 0; i < toggleableLayerIds.length; i++) {
var id = toggleableLayerIds[i];
var link = document.createElement('a');
link.href = '#';
link.className = 'active';
link.textContent = id;
Using this example as a model, there are many ways which you could go about doing this. One is to create a variable storing a mapping from the desired title to the name of the layer, so that clicking on the relevant button will use the textContent as a key in this mapping to retrieve a value representing the name of the layer. For example:
var titleToLayerName = {
'PJUK Previous Tours': 'pjuk_previous_tours',
'PJUK 2020 Tour': 'pjuk_2020_tour'
}
...
link.onclick = function(e) {
var clickedLayer = titleToLayerName[this.textContent];
...
};
Several moments are happening correctly in this effort, the map is displaying as expected, the zip code boundaries are showing as expected, but I'm not able to figure how to get 5-digit zip codes to be the label within each zip code boundary. Any help (with example code if possible) would be greatly appreciated!
Here's some code:
<html>
<div id='mapdiv'></div>
...
mapboxgl.accessToken='<token>';
var mapobj = new mapboxgl.Map({
container: 'mapdiv',
style: 'mapbox://styles/mapbox/streets-v9',
minZoom: 3,
maxZoom: 20,
zoom: 10,
center: [-105.1613858,39.6791558]
});
<script src="https://api.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v0.39.1/mapbox-gl.css" rel="stylesheet" />
...
mapobj.on('load', function() {
// Add ZipCode source to map
mapobj.addSource('zipSource', {
type: 'vector',
url: 'mapbox://mapbox.enterprise-boundaries-p2-v1'
});
mapobj.showTileBoundaries = true;
// Add hot ZipCode layer to map
mapobj.addLayer({
id: 'hotZipLayer',
type: 'fill',
source: 'zipSource',
'source-layer': 'boundaries_postal_2',
paint: {
'fill-outline-color': 'rgba(0,0,0,1)',
'fill-color': 'rgba(0,0,0,0.01)'
}
});
// Add Zip numbers symbol layer to map
mapobj.addLayer({
id: 'zipCodeLabels',
type: 'symbol',
source: 'zipSource',
'source-layer': 'points_postal_2',
layout: {
'text-field': '{id}',
'text-justify': 'center',
'text-size' : 10
},
paint: {
'text-color': 'black',
'text-halo-color': 'white',
'text-halo-width': 25
}
});
});
And an example data entry:
[
{
"geometry":
{
"type":"Point","coordinates":[-105.0908374786377,39.707747919880916]
},
"type":"Feature",
"properties":
{
"id":"USP280226"
},
"id":2,
"layer":
{
"id":"zipCodeLabels",
"type":"symbol",
"source":"zipSource",
"source-layer":"points_postal_2",
"layout":
{
"text-field":"{id}",
"text-justify":"center",
"text-size":10
},
"paint":
{
"text-color":"black",
"text-halo-color":"white",
"text-halo-width":25
}
}
},...]
So in this case the value that would show up within the zip code boundary is "USP280226", what I would like to appear is "80226", so I would like to call substring(4) on that id value, but I don't see an easy way to do that for each displayed zip code on the map.
I would imagine MapBox has a way to do this properly, but I haven't been able to find it in the docs or examples.
Thanks in advance for any help.
The currently released version of Mapbox-GL-JS doesn't support any kind of functions on data. You will have to process the data offline so that it contains the labels you want to display.
(I think a forthcoming version may support this kind of function, but I'm not certain.)
EDIT The "expression" functionality is now released. Unfortunately I don't think it helps you. There's a concat function but no way to split strings that I can see.
TLDR; How do I add a popup to a mapbox "line" type layer?
I have a function loading a mapbox map and adding a layer of lines to it.
My goal is to add a popup on clicking on that line.
I followed the examples and added an on click event. But this gives me an error. Any pointers on what I am doing wrong?
function renderLineLayer(layerName,data) {
map.on('load', function() {
map.addLayer({
"id": layerName,
"type": "line",
"source": {
"type": "geojson",
"data": data
},
"layout": {
"line-join": "round",
"line-cap": "round",
"visibility":"visible"
},
"paint": {
"line-color": "blue",
"line-width": 8
}
});
console.log(map.getLayer(layerName));
map.on('click', layerName, function (e) {
console.log('click');
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML(e.features[0].properties.name)
.addTo(map);
});
});
}
The error I get is.
TypeError: listeners[i].call is not a function[Learn More] mapbox-gl-dev.js:29779:13
This is a bit of a guess, but the on('click') event API changed recently. Previously it didn't have the layer argument (IIRC). If you're using an older version of the Mapbox-GL-JS library, it would be trying to call your second argument (the layer name) as if it were a function and it would give that error message.
Solution: update to the latest Mapbox-GL-JS library version (0.36).
(I don't think the process for responding to mouse clicks on line features is any different than for points or polygons.)
I'm currently using Mapbox GL JS and I have custom icons like this example and I want to be able to drag the icons.
I'm doing it similar to draggable point example where I have mouseDown, onMove, and onUp functions. However the part I'm stuck is in onMove, I'm not sure how to set the custom icons, which are div to update its positions throughout the dragging process. I'm updating the new coordinates (lng & lat) of the icons but I'm not sure how to actually move them as right now the icons won't move/drag.
In the original draggable point example it has map.getSource('point').setData(geojson); which updates the geojson to allow the point moving on the map.
So basically I just want to be able to drag the custom icons in Mapbox GL JS.
Thanks.
I ran into a similar problem and after many hours, managed to combine the two examples and export the coordinates to form fields. Try this snippet (use your own accessToken, map style and marker image)
$(document).ready(function() {
// ===============================================
// mapbox
// ===============================================
// Holds mousedown state for events. if this
// flag is active, we move the point on `mousemove`.
var isDragging;
// Is the cursor over a point? if this
// flag is active, we listen for a mousedown event.
var isCursorOverPoint;
mapboxgl.accessToken = '############# Add your own accessToken here ##########';
var map = new mapboxgl.Map({
container: 'map-one',
style: 'mapbox://############# Add your own style here ##########',
center: [5.037913, 52.185175],
pitch: 0,
zoom: 12
});
var nav = new mapboxgl.Navigation({
position: 'top-left'
});
map.addControl(nav);
var canvas = map.getCanvasContainer();
var geojson = {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [5.067, 52.1923]
},
"properties": {
"title": "Afspreekpunt",
"marker-symbol": "dimmle-marker"
}
}]
};
function mouseDown() {
if (!isCursorOverPoint) return;
isDragging = true;
// Set a cursor indicator
canvas.style.cursor = 'grab';
// Mouse events
map.on('mousemove', onMove);
map.on('mouseup', onUp);
}
function onMove(e) {
if (!isDragging) return;
var coords = e.lngLat;
// Set a UI indicator for dragging.
canvas.style.cursor = 'grabbing';
// Update the Point feature in `geojson` coordinates
// and call setData to the source layer `point` on it.
geojson.features[0].geometry.coordinates = [coords.lng, coords.lat];
map.getSource('point').setData(geojson);
}
function onUp(e) {
if (!isDragging) return;
var coords = e.lngLat;
canvas.style.cursor = '';
isDragging = false;
// Update form fields with coordinates
$('#latitude').val(coords.lat);
$('#longitude').val(coords.lng);
}
// Mapbox map-accordion fix
$('#accordion').on('hidden.bs.collapse', function () {
map.resize();
})
$('#accordion').on('shown.bs.collapse', function () {
map.resize();
})
// After the map style has loaded on the page, add a source layer and default
// styling for a single point.
map.on('load', function() {
// Add a single point to the map
map.addSource('point', {
"type": "geojson",
"data": geojson
});
map.addLayer({
"id": "point",
"type": "symbol",
"source": "point",
"layout": {
// ##############################################
// NOTE: this is my marker, change it
// to the marker you uploaded in your map style
// - you will likely need different
// offset/translate values
// ##############################################
"icon-image": "my-marker",
"icon-size": 0.3,
"text-field": "",
"text-offset": [0, 0.6],
"text-anchor": "top",
"text-size": 14
},
"paint": {
"icon-translate": [-6, -34],
}
});
// If a feature is found on map movement,
// set a flag to permit a mousedown events.
map.on('mousemove', function(e) {
var features = map.queryRenderedFeatures(e.point, { layers: ['point'] });
// Change point and cursor style as a UI indicator
// and set a flag to enable other mouse events.
if (features.length) {
canvas.style.cursor = 'move';
isCursorOverPoint = true;
map.dragPan.disable();
} else {
//map.setPaintProperty('point', 'circle-color', '#3887be');
canvas.style.cursor = '';
isCursorOverPoint = false;
map.dragPan.enable();
}
});
// Set `true` to dispatch the event before other functions call it. This
// is necessary for disabling the default map dragging behaviour.
map.on('mousedown', mouseDown, true);
});
}); // end document ready
.map { border: 1px solid #ccc }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.25.1/mapbox-gl.js'></script>
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.25.1/mapbox-gl.css' rel='stylesheet' />
<div id='map-one' class='map' style='height: 360px;'></div>
<input id="latitude"> <input id="longitude">