Mapbox GL JS Set Paint property on specific Feature in Layer - mapbox

I am using Mapbox Studio as basis for mapping and styling and then using HTML for additional map features.
One of the features is to change Icon opacity when hovering or mouse enter. I've checked other examples and all other refer to feature when you create it directly in HTML. I managed to change opacity but only for whole layer.
Can I use somehow e.features[0] command line to apply changes only to one feature rather than to whole layer?
I used this code which changer opacity for whole Layer 'Icon' (Layer contains 5 icons with text):
// Change the cursor to a default and change opacity when the it enters a feature in the 'Icons' layer.
map.on('mouseenter', 'Icons', function() {
map.getCanvas().style.cursor = 'default';
var feature = e.features[0];
map.setPaintProperty('Icons', 'icon-opacity', 0.5);
});
// Change it back to a pointer and reset opacity when it leaves.
map.on('mouseleave', 'Icons', function() {
map.getCanvas().style.cursor = '',
map.setPaintProperty('Icons', 'icon-opacity', 1);
});
Thank you!!!

There are a few ways which you could achieve this. One approach is to add each feature as separate layer, so that when you want to change the opacity of an icon added in a layer 'specific-icon-layer', you can pass 'specific-icon-layer' to the Map#on method. This is likely the most straightforward option if you have a relatively minimal number of markers.
Another approach is to add unique IDs to each icon feature, so that you can use a filter expression in conjunction with Map#setPaintProperty and Map#queryRenderedFeatures (or Map#querySourceFeatures). For example, suppose you add an 'id' property to each GeoJSON feature representing an icon in the source for the 'Icons' layer. Then, you could set up an event listener similar to this example, retrieve the 'id' of the returned feature, and use the 'id' (suppose here it is 'example-id') to update the paint property for the 'Icons' layer:
map.setPaintProperty(
'Icons',
'icon-opacity',
['match', ['get', 'id'], 'example-id', 0.5 , 1]
);
Here, we use match and get expressions to say "if the 'id' of a feature is 'example-id', paint its icon with opacity 0.5, otherwise use opacity 1."

Check the example at https://docs.mapbox.com/mapbox-gl-js/example/hover-styles/
This approach makes use of setFeatureState and feature-state expressions
The problem with using map.setPaintProperty(layer, property, filter, matchValue, styleValue, fallbackStyleValue) every time is that it restyles every feature on the layer instead of only the feature being interacted with. This can cause poor performance when the layer has a high number of features.

Related

Using the same Plug-in for multiple purposes in leaflet

I am trying to use the leaflet-draw tool for two different things:
as a "regular" tool to create new geometries
if I draw a line, I perform some calculations with turf.js, giving me points nearby.
I've set up two individual draw controls for each purpose. For the second, I have all but the draw:polyline disabled. The problem: I save my elements with the
map.on('draw:created', function(){...});
"command". But this way I (or the eventhandler, respectively :)) cant differentiate, if the line was drawn with the first or the second button. So basically i can use the draw tool either for one thing or the other. Is there a way where I can use the same tool for different applications on the same map?
Thanks for any hints or work arounds.
An alternative would be to use Leaflet-Geoman instead of Leaflet-Draw.
There you can create copies of Draw instances and add them a new shape name:
// copy a rectangle and customize its name, block, title and actions
map.pm.Toolbar.copyDrawControl('Polygon', {
name: 'PolygonCopy',
block: 'custom',
title: 'Display text on hover button',
actions: ['cancel', 'removeLastVertex', 'finish'],
});
And then you can check the shape name in the create event:
// listen to when a new layer is created
map.on('pm:create', function(e) {
console.log(e)
if(e.shape === 'Polygon'){
alert('Original Polygon')
}else if(e.shape === 'PolygonCopy'){
alert('Copy Polygon')
}
});
https://jsfiddle.net/falkedesign/r0sm9auo/

Update leaflet baselayer properties with GoogleMutant

I have a google layer (baselayer) GoogleMutant and want to update its options. I try to
map.remove(google.layer)
//update the POI visibility
google.layer.options.styles.forEach(i=>i.stylers[0].visibility = "off")
map.addLayer(google.layer)
this updates the layer options but the Points of interest are still on the map. Is there any way to update the options and apply them to the baselayer?
Thanks
Here is an example with google mutant https://jsfiddle.net/benderlio/2m4c01w6/10/
There is a button "remove POI", and I want to remove all poi from current layer with leaflet API
Leaflet doesn't work changing settings on the fly. You have to remove the whole object and create a substitute.
Like if you need to change a layer, you have to remove it like:
roadMutant.removeFrom(map); // or
map.removeLayer(roadMutant);
And then create and add the new one:
roadMutant.addTo(map);
I created a fiddle to help the change based on yours. It still have bugs and somewhere to grow, but it's a base...

dynamic icon/symbol coloring with mapbox

I am working with mapbox gl js for the first time. I need to be able to define a 'template' icon/symbol (lets say a triangle that is approx 16X16) and then use that icon/symbol on a layer but be able to use the icon-color property in the 'paint' section to be able to set the color for the icon/symbol in the style.
I have created a triangle svg file. I then used a cli utility to convert that svg to an sdf (I named it triangle16_sdf.png) and uploaded it to my aws public bucket. The sdf is monochromatic. I am trying to use that in a style as defined int the below style snippet:
{"type":"symbol","icons":{"triangle-16":"https://my_aws_url/icons/triangle16_sdf.png"},"paint":{"icon-color":"#00ff00"},"layout":{"symbol-placement":"point","icon-image":"triangle-16","icon-ignore-placement":true,"icon-allow-overlap":true}}
This shows the icon but only the template. It does not color the icon with the appropriate color.
Is there something I am doing incorrectly here or is there another approach where I can have a single icon/symbol that I can color differently with a style?
I'm not sure you can use the style to tell mapbox-gl that an icon should be considered an "sdf" icon. But with the addImage method you are able to do this:
const image = new Image();
image.onload = () => {
map.addImage('icon-name', image, {sdf: true});
};
image.src = 'your-image-url';

No setBounds function for Leaflet imageOverlay

I'm reading an imageOverlay URL from an ArcGIS webserver that uses the leaflet getBound() coordinates as part of the URL (we have large maps that are filtered for the current window 'extent'). Apologies for not including the actual path (I'm working with sensitive client data). Eg:
http://myarcgiswebserver.com/MapServer/export/dpi=96&format=png32&bbox=27.119750976562504%2C-31.194007509998823%2C32.39044189453126%2C-29.692824739380754&size=1719%2C434
[bbox] = current imageBounds
When dragging my map the imageOverlay url is updated correctly but my leaflet window is no longer aligned to the imageBound values that were set when first adding the imageOverlay which results in a skewed output (this is my assumption):
The only workaround is to remove the existing imageOverlay and add a new one (which ruins the user experience as the map disappears then reappears each time the window is dragged or zoomed).
Am i approaching this problem incorrectly or would the introduction of a function to update the current imageBounds resolve this? Perhaps not a new function but the expansion of setUrl with additional parameters...?
Many thanks for any feedback...
As #ghybs pointed out, your use case might be better served by using the WMS
interface of your ArcGIS server.
Anyway, you say
The only workaround is to remove the existing imageOverlay and add a new one (which ruins the user experience as the map disappears then reappears each time the window is dragged or zoomed).
Well, that glitch is due to you probably doing something like:
Remove old overlay
Add new overlay
Wait until the image is received from the network
Wait one frame so the new overlay is shown
and instead you should be doing something like:
Add new overlay
Wait until the image is received from the network
Remove old overlay
Wait one frame so the new overlay is shown
The problem is just the async wait and the possible race conditions there, but should be easy to hack together, e.g.:
var activeOverlay = null;
var overlayInRequest = null;
map.on('moveend zoomend', {
// If we are already requesting a new overlay, ignore it.
// This might need some additional debouncing logic to prevent
// lots of concurrent requests
if (overlayInRequest) {
overlayInRequest.off('load', showOverlay);
}
overlayInRequest = L.imageOverlay( computeUrl( map.getBounds() ), myOverlayOptions );
overlayInRequest.on('load', showOverlay);
});
function showOverlay(ev) {
activeOverlay.remove();
activeOverlay = overlayInRequest;
activeOverlay.addTo(map);
overlayInRequest = undefined;
}
If you use an ImageOverlay but change its url dynamically, with a new image that reflects a new bounding box, then indeed that is the reason for the behaviour you describe: you display an image that has been generated using a new bbox, but positioned in the initial bbox, since the image overlay remains at the same geographical position on the map.
Instead, it sounds to me that you should use a TileLayer.WMS.
It would automatically manage the bounding box update for you. You may need to find the correct options to fit your service provider required URL syntax, though.
Example: http://playground-leaflet.rhcloud.com/yel/1/edit?html,output

Click-through markers and polylines in Leaflet

In Leaflet, is it possible to define a marker or polyline with {clickable:false}, so that a click is passed through to whatever lies beneath - be it the map or a clickable geometry object?
At the moment I solve this problem by making the marker/polyline clickable and passing the event onwards myself. But this leads to the mouse cursor always showing as the hand symbol. Ideally, the mouse cursor should look like the normal pointer or the hand, depending on whether what is beneath the marker/polyline is clickable.
This may not be the answer you are looking for, but you can use featureGroups to have all of your clickable polylines come to the front so that the actions are surfaced.
var lg_noclick = new L.FeatureGroup().addTo(map);
var lg_click = new L.FeatureGroup().addTo(map);
// Add lines
lg_click.bringToFront();
updated fiddle
Also if you can afford to know your lines before hand, correct ordering of when you add the lines it will work as well.
I know this is not ideal but it suited my situation just fine, so it might be good for you as well.
This hides the icon and brings it back after a second using mouseenter and mouseleave events:
$('.leaflet-marker-icon').mouseenter(function() {
$(this).hide();
});
$('.leaflet-marker-icon').mouseleave(function() {
$(this).delay(1000).show(0);
});