Mapbox GL v.0.54: Change icon-image based on click - mapbox-gl-js

I'm trying to implement a single-layer in mapbox-GL that shows icons which change when clicked. So far I've attempted the following:
Use a property (after pre-loading the images as active and inactive):
'icon-image': ["case",["boolean", ["feature-state", "clicked"], false],
"inactive",
"active"
],
And various versions of map.updateImage() to dynamically change the image that is displayed for each point:
map.updateImage(`point1`, map.loadImage('/img/pin_active.png'))
map.updateImage(`point1`, 'active')) //with the image being loaded beforehand as 'active'
map.updateImage(`point1`, map.loadImage('/img/pin_active.png', function(error, image) {
if (error) throw error;
map.addImage(point1, image);
}))
The only solution that does work is using SDF (mapbox gl change icon color) - but this does not work for my icons, which are multi-color (and they get pretty ugly since the SDF format seems to scale badly).
Any ideas on how to approach this?

Ok, so after a bit of extra fiddling around I found a working solution. Just leaving this here for whoever finds it later.
If we load the images beforehand as strings active and inactive:
map.loadImage('/img/pin_blue.png', function(error, image) {
if (error) throw error;
map.addImage('inactive', image);
})
map.loadImage('/img/pin_red.png', function(error, image) {
if (error) throw error;
map.addImage('active', image);
})
we can do the following:
let data = {} // object that stores the geojson data
let points = map.queryRenderedFeatures(e.point, {
layers: ['projects-point']
})
if (points.length) {
data.map(function(item, index) {
if (item.id === points[0].id) {
data[index].properties.image = 'active'
}
else
data[index].properties.image = 'inactive'
})
map.getSource('projects').setData(this.dataObj.data)
}

Related

How to check Mapbox GL JS Draw state

How can you check the state of a new MapboxDraw object before sending it to the backend? For example, to show the user some warnings when he tries to submit some actions without creating an object (in my case a polygon) on the map.
this.draw = new MapboxDraw({
controls: {
trash: true,
polygon: true
},
defaultMode: 'draw_polygon',
displayControlsDefault: false,
})
# sudocode
if (user has not created a polygon on the map) {
alert('You must create a polygon before submitting the form!')
}
I actually tried to solve this with the following code, because the length value of the correct polygon must be more than 3 points.
if (draw.getAll().features[0].geometry.coordinates[0].length <= 3) {
alert('You must create a polygon before submitting the form!')
}
The above code only works in the first execution, but in the second execution it causes an error e.g if user tries to create a Polygon of two points or if user creates one polygon and then removes it
Uncaught TypeError: Cannot read property 'geometry' of undefined
You can attach many events from Mapbox Draw to your current map.
For example, map.on('draw.crete', function() {}) This will execute once 1 polygon was already created.
You can also use draw.getMode() for catching any type of polygons you draw.
See below example, Hope it helps :)
var draw = new mapboxDraw({
displayControlsDefault: false,
controls: {
point: true,
polygon: true,
line_string: true,
trash: true
}
});
map.on('draw.create', function(e) {
var drawMode = draw.getMode();
var drawnFeature = e.features[0];
switch (drawMode) {
case 'draw_point':
// Draw point here
break;
case 'draw_polygon':
// Draw polygon here
break;
case 'draw_line_string':
// Draw linestring here
break;
default: alert('no draw options'); break;
}
});
map.on('draw.update', function(e) {
// This will call once you edit drawn polygon
});
map.on('draw.delete', function(e) {
// This will call once you delete any polygon
});

Mapbox GL JS: Style is not done loading

I have a map wher we can classically switch from one style to another, streets to satellite for example.
I want to be informed that the style is loaded to then add a layer.
According to the doc, I tried to wait that the style being loaded to add a layer based on a GEOJson dataset.
That works perfectly when the page is loaded which fires map.on('load') but I get an error when I just change the style, so when adding layer from map.on('styledataloading'), and I even get memory problems in Firefox.
My code is:
mapboxgl.accessToken = 'pk.token';
var map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v10',
center: [5,45.5],
zoom: 7
});
map.on('load', function () {
loadRegionMask();
});
map.on('styledataloading', function (styledata) {
if (map.isStyleLoaded()) {
loadRegionMask();
}
});
$('#typeMap').on('click', function switchLayer(layer) {
var layerId = layer.target.control.id;
switch (layerId) {
case 'streets':
map.setStyle('mapbox://styles/mapbox/' + layerId + '-v10');
break;
case 'satellite':
map.setStyle('mapbox://styles/mapbox/satellite-streets-v9');
break;
}
});
function loadJSON(callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', 'regions.json', true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
};
xobj.send(null);
}
function loadRegionMask() {
loadJSON(function(response) {
var geoPoints_JSON = JSON.parse(response);
map.addSource("region-boundaries", {
'type': 'geojson',
'data': geoPoints_JSON,
});
map.addLayer({
'id': 'region-fill',
'type': 'fill',
'source': "region-boundaries",
'layout': {},
'paint': {
'fill-color': '#C4633F',
'fill-opacity': 0.5
},
"filter": ["==", "$type", "Polygon"]
});
});
}
And the error is:
Uncaught Error: Style is not done loading
at t._checkLoaded (mapbox-gl.js:308)
at t.addSource (mapbox-gl.js:308)
at e.addSource (mapbox-gl.js:390)
at map.js:92 (map.addSource("region-boundaries",...)
at XMLHttpRequest.xobj.onreadystatechange (map.js:63)
Why do I get this error whereas I call loadRegionMask() after testing that the style is loaded?
1. Listen styledata event to solve your problem
You may need to listen styledata event in your project, since this is the only standard event mentioned in mapbox-gl-js documents, see https://docs.mapbox.com/mapbox-gl-js/api/#map.event:styledata.
You can use it in this way:
map.on('styledata', function() {
addLayer();
});
2. Reasons why you shouldn't use other methods mentioned above
setTimeout may work but is not a recommend way to solve the problem, and you would got unexpected result if your render work is heavy;
style.load is a private event in mapbox, as discussed in issue https://github.com/mapbox/mapbox-gl-js/issues/7579, so we shouldn't listen to it apparently;
.isStyleLoaded() works but can't be called all the time until style is full loaded, you need a listener rather than a judgement method;
Ok, this mapbox issue sucks, but I have a solution
myMap.on('styledata', () => {
const waiting = () => {
if (!myMap.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
loadMyLayers();
}
};
waiting();
});
I mix both solutions.
I was facing a similar issue and ended up with this solution:
I created a small function that would check if the style was done loading:
// Check if the Mapbox-GL style is loaded.
function checkIfMapboxStyleIsLoaded() {
if (map.isStyleLoaded()) {
return true; // When it is safe to manipulate layers
} else {
return false; // When it is not safe to manipulate layers
}
}
Then whenever I swap or otherwise modify layers in the app I use the function like this:
function swapLayer() {
var check = checkIfMapboxStyleIsLoaded();
if (!check) {
// It's not safe to manipulate layers yet, so wait 200ms and then check again
setTimeout(function() {
swapLayer();
}, 200);
return;
}
// Whew, now it's safe to manipulate layers!
the rest of the swapLayer logic goes here...
}
Use the style.load event. It will trigger once each time a new style loads.
map.on('style.load', function() {
addLayer();
});
My working example:
when I change style
map.setStyle()
I get error Uncaught Error: Style is not done loading
This solved my problem
Do not use map.on("load", loadTiles);
instead use
map.on('styledata', function() {
addLayer();
});
when you change style, map.setStyle(), you must wait for setStyle() finished, then to add other layers.
so far map.setStyle('xxx', callback) Does not allowed. To wait until callback, work around is use map.on("styledata"
map.on("load" not work, if you change map.setStyle(). you will get error: Uncaught Error: Style is not done loading
The current style event structure is broken (at least as of Mapbox GL v1.3.0). If you check map.isStyleLoaded() in the styledata event handler, it always resolves to false:
map.on('styledata', function (e) {
if (map.isStyleLoaded()){
// This never happens...
}
}
My solution is to create a new event called "style_finally_loaded" that gets fired only once, and only when the style has actually loaded:
var checking_style_status = false;
map.on('styledata', function (e) {
if (checking_style_status){
// If already checking style status, bail out
// (important because styledata event may fire multiple times)
return;
} else {
checking_style_status = true;
check_style_status();
}
});
function check_style_status() {
if (map.isStyleLoaded()) {
checking_style_status = false;
map._container.trigger('map_style_finally_loaded');
} else {
// If not yet loaded, repeat check after delay:
setTimeout(function() {check_style_status();}, 200);
return;
}
}
I had the same problem, when adding real estate markers to the map. For the first time addding the markers I wait till the map turns idle. After it was added once I save this in realEstateWasInitialLoaded and just add it afterwards without any waiting. But make sure to reset realEstateWasInitialLoaded to false when changing the base map or something similar.
checkIfRealEstateLayerCanBeAddedAndAdd() {
/* The map must exist and real estates must be ready */
if (this.map && this.realEstates) {
this.map.once('idle', () => {
if (!this.realEstateWasInitialLoaded) {
this.addRealEstatesLayer();
this.realEstateWasInitialLoaded = true
}
})
if(this.realEstateWasInitialLoaded) {
this.addRealEstatesLayer();
}
}
},
I ended up with :
map.once("idle", ()=>{ ... some function here});
In case you have a bunch of stuff you want to do , i would do something like this =>
add them to an array which looks like [{func: function, param: params}], then you have another function which does this:
executeActions(actions) {
actions.forEach((action) => {
action.func(action.params);
});
And at the end you have
this.map.once("idle", () => {
this.executeActions(actionsArray);
});
I have created simple solution. Give 1 second for mapbox to load the style after you set the style and you can draw the layer
map.setStyle(styleUrl);
setTimeout(function(){
reDrawMapSourceAndLayer(); /// your function layer
}, 1000);
when you use map.on('styledataloading') it will trigger couple of time when you changes the style
map.on('styledataloading', () => {
const waiting = () => {
if (!myMap.isStyleLoaded()) {
setTimeout(waiting, 200);
} else {
loadMyLayers();
}
};
waiting();
});

Styling MapBox GL Markers

I am trying to move from using Mapbox.js with Leaflet to Mapbox GL.
I have the following geojson that should be rendered as per the image attached. Unfortunately, after reading many articles and through the documentation related to data driven visualisation I've hit a wall. Wondered if anyone could help with an example?!
Here's the geojson:
{ "type":"Feature",geometry: { "type":"Point","coordinates":[1.1147,51.7829] },properties: { "title":"Paul","marker-color":"#0094F7","marker-size":"large","marker-symbol":"star" } },
{ "type":"Feature",geometry: { "type":"Point","coordinates":[-2.34851,52.6123] },properties: { "title":"Will","marker-color":"#F9883E","marker-size":"large","marker-symbol":"1" } },
{ "type":"Feature",geometry: { "type":"Point","coordinates":[-2.76389,53.0232] },properties: { "title":"Mark","marker-color":"#F1574E","marker-size":"large","marker-symbol":"13" } },
{ "type":"Feature",geometry: { "type":"Point","coordinates":[-.127211,51.6014] },properties: { "title":"David","marker-color":"#83C736","marker-size":"large","marker-symbol":"20" } },
{ "type":"Feature",geometry: { "type":"Point","coordinates":[-2.06682,53.4986] },properties: { "title":"Adam","marker-color":"#FC5C53","marker-size":"large","marker-symbol":"rocket" } }
The colour values are set by the end user and could be anything so I can't just create a lot of images to suit, and the numbers refer to dates so will be 1-31.
Here's what it currently looks like in Mapbox.js:
Thanks all!
Using Markers, you have full control to display any HTML element you like - so you could use CSS to construct a circle or teardrop of the right color, or an SVG icon.
See this example: https://www.mapbox.com/mapbox-gl-js/example/custom-marker-icons/
Essentially, instead of adding the GeoJSON directly as a source into the map, you will want to iterate over each point, and add markers onto the map.

Mapbox No Source With This ID

I want to remove sources and layers on Mapbox map. I managed to remove every sources and layers except for the first source and layer that I have added to Mapbox map.
Note that I am not good in using jQuery $.post.
Here is how I add all the sources and layers.
$.post('ajax/marker.php', function(data)
{
var firstSplit = data.split(",");
for(i=0;i<firstSplit.length-1;i++)
{
var secondSplit = firstSplit[i].split("|");
var id = secondSplit[0];
var lat = secondSplit[1];
var lng = secondSplit[2];
var point = {
"type": "Point",
"coordinates": [lat, lng]
};
map.addSource(id, { type: 'geojson', data: point });
map.addLayer({
"id": id,
"type": "symbol",
"source": id,
"layout": {
"icon-image": "airport-15"
}
});
}
});
Remember that, I managed to view all the sources and layers on Mapbox map.
Its just that I am not able to remove only the first source and layers that I have added to the map. I hope someone out there has some ideas regarding this problem. Thanks.
I used the two statements below in a loop to remove sources and layers.
map.removeSource(id);
map.removeLayer(id);
I did a test to remove the first source and layers manually as below but it did not work.
map.removeSource('1612280004A');
map.removeLayer('1612280004A');
However, it works on the next sources and layers.
My best guess from what you've posted is that you can't remove the layer while there are still sources attached to it. Try reversing the order of your two statements:
map.removeLayer('1612280004A');
map.removeSource('1612280004A');
My GeoJson shared the same ID. Removing both layer and source fixed this issue
function RemoveMapLayer() {
var mpLayer = map.getLayer("points");
if (typeof mpLayer === 'undefined') {
// No Layer
} else {
map.removeLayer("points");
}
var mpSource = map.getSource("points");
if (typeof mpSource === 'undefined') {
alert("no source");
} else {
map.removeSource("points");
}
}
Before deleting, check if the source and layer are present, for example:
if (map.getLayer('points')){
map.removeLayer('points');
}
if (map.getSource('point')){
map.removeSource('point');
}

Adding CartoDb layer to Leaflet Layer Control

I'm trying to toggle the display of a CartoDb layer on a Leaflet map. I've been able to load the layer using this code:
var layerUrl = 'http://ronh-aagis.cartodb.com/api/v1/viz/rotaryclubs_geo2/viz.json';
var clubPts = cartodb.createLayer(map, layerUrl, {
// The ST_AsGeoJSON(ST_Simplify(the_geom,.01)) as geometry will store a simplified GeoJSON representation of each polygon as an attribute we can pick up on hover
query: 'select *, ST_AsGeoJSON(the_geom) as geometry from {{table_name}}',
interactivity: 'cartodb_id, geometry'
})
.on('done', function(layer) {
map.addLayer(layer);
layer.on('featureOver', function(e, pos, latlng, data) {
$('.leaflet-container').css('cursor','pointer');
if (data.cartodb_id != point.cartodb_id) {
drawHoverPoint(data);
}
cartodb.log.log(pos, data);
});
layer.on('featureOut', function(e, pos, latlng, data) {
$('.leaflet-container').css('cursor','default')
removePoint();
});
layer.on('error', function(err) {
cartodb.log.log('error: ' + err);
});
}).on('error', function() {
cartodb.log.log("some error occurred");
});
Yet when I try to add this layer to a Layer Control:
var clubs = new L.LayerGroup();
clubs.addLayer(clubPts);
I get an "Uncaught TypeError: Object # has no method 'onAdd'" error.
Any thoughts? Thanks!
A great way to reduce complexity and get up to speed quickly here would be using an already-built Leaflet plugin, like Vector Layers, that has built-in CartoDB support already. Take a look at the demo here. http://jasonsanford.github.io/leaflet-vector-layers/demos/cartodb/