How to get the results of a filtered Mapbox layer? - mapbox

I am trying to update an old Mapbox.js map to Mapbox GL. I am generating the map from geojson (and using coffescript).
map.addSource 'my_datasource',
'type': 'geojson'
'data': my_geojson
map.addLayer
'id': 'my_layer'
'type': 'symbol'
'source': 'my_datasource'
I am filtering the layer based on a user input that returns value
map.setFilter('my_layer', ["==", 'my_attribute', value ])
So far, so good. But now I want to zoom and reposition the map to fit the bounds of the filtered symbols.
I thought I would be able to do something like this
bounds = new (mapboxgl.LngLatBounds)
map.queryRenderedFeatures(layers: [ 'my_layer' ]).forEach (feature) ->
bounds.extend feature.geometry.coordinates
return
map.fitBounds bounds
But queryRenderedFeatures appears to be returning all (i.e., un-filtered) symbols.
After much reading around, my understanding is that queryRenderedFeatures should return the filtered symbols that are visible in the viewport (i.e., would be suitable for zooming in but not zooming out).
Is this correct? And if so, why is my function above returning unfiltered symbols? Appreciate any advice to help my transition to MapboxGL!

It's a bit unclear to me from the documentation whether filters in the layer should be applied, but in any case, there is an explicit filter parameter you can, pass, so:
map.queryRenderedFeatures(layers: [ 'my_layer' ], filter:["==", 'my_attribute', value ]).forEach (feature) ->
bounds.extend feature.geometry.coordinates
But I suspect you really want querySourceFeatures, because you don't want to be constrained by what's currently within the viewport:
map.querySourceFeatures(my_source, filter:["==", 'my_attribute', value ]).forEach (feature) ->
bounds.extend feature.geometry.coordinates
or, in native ES2015:
map.querySourceFeatures(my_source, { filter:['==', 'my_attribute', value ]} )
.forEach (feature => bounds.extend(feature.geometry.coordinates))

Try this post, I have added the code which will let you have the features using queryRenderedFeatures() or even using querySourceFeatures():
https://stackoverflow.com/a/66308173/9185662

Related

MapBox Randomizing Building Color

Is it possible to loop through all of the buildings on the map an assign them different colours, or a different property value that drives a thematic grouping?
I have code to loop through the buildings:
features = map.queryRenderedFeatures({ layers: ["building"], filter: ['==', 'extrude', 'true']});
features.forEach(function(feature){
// how to change feature colour or property in here?
}
There's a couple of ways achieving something like what you want.
Feature state
If your buildings layer has feature IDs, you can use setFeatureState to set a state on each building.
features = map.querySourceFeatures('building', { sourceLayer: 'buildings' }));
features.forEach(function(feature){
if (!map.getFeatureState({ id: feature.id, source: 'building', sourceLayer: 'buildings' }).color) {
map.setFeatureState({ id: feature.id, source: 'building', sourceLayer: 'buildings' }, { color: makeRandomColor() }
}
});
You can then use ['feature-state', 'color'] in an expression.
Quasi random
If there's no feature ID, you might be able to use some other attribute in a way that appears to be random. For instance, if there's some other kind of ID, you can use a mod function to map it to a color, in a way that might look kind of random.

How can I zoomed to the cql_filter feature?

I am using geoserver. I write the following code;
const mywms = L.tileLayer.wms("http://localhost:8080/geoserver/taj/wms", {
layers: 'taj:country',
format: 'image/png',
CQL_FILTER: 'name=pana'
transparent: true,
opacity: 0.4,
version: '1.1.0',
attribution: "country layer"
});
All is good. The layer get filtered. But I need the selected feature to zoom full extend.
I tried to center the mywms layer using this code; map.fitBounds(mywms.GetBounds());. But it shows the error; mywms.getBOunds is not a function. Any help?
As is pointed out in the comments WMS requests contain the bounding box of the map in the request and so will always cover the whole of the map area.
To get the extent of a single feature you need to make a WFS request with the filter included and then zoom to the extent of that feature when it is returned.
Thank you very much for all of the helping hands. And special thanks to Mr. Ina Turton who helps me to find out the actual problem and get the solution. my url for the wfs server looks like this;
http://localhost:8080/geoserver/tajikistan/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=taj%3Acountry&maxFeatures=50&outputFormat=application%2Fjson
I write the following code to for zooming into my cql_filter area:
//Geoserver Web Feature Service
$.ajax('http://localhost:8080/geoserver/wfs',{
type: 'GET',
data: {
service: 'WFS',
version: '1.1.0',
request: 'GetFeature',
typename: 'tajikistan:country',
CQL_FILTER: "name_rg='Centre'",
srsname: 'EPSG:4326',
outputFormat: 'text/javascript',
},
dataType: 'jsonp',
jsonpCallback:'callback:handleJson',
jsonp:'format_options'
});
function handleJson(data) {
var requiredArea = L.geoJson(data).addTo(map);
map.fitBounds(requiredArea.getBounds())
}
In this code the CQL_FILTER is used for to filter out the required area. In my case the data having column name name_rg which have the property as Centre. I query this region using this code. I fetch the data using ajax method. I write the code little tricky because the regular ajax call doesn't callback the handleJson function and return an parseJson error. And finally I added the variable named as requiredArea. Get bounds of this region and set bounds of the required region and added into the map.

Get coordinate of selected tileset tile (by expression)

I am able to find and change the properties of a vector tileset using expression, for example:
map.setPaintProperty('my-shp', 'fill-opacity', [
'match',
['get', 'NO'],
13708, 1,
/* else */ 0
]);
I was wondering if there is a way to get the exact coordinate of the tileset item using expression?
Sorry if I am wrong with the terminology (of tileset/tile item), feel free to correct me. Thanks
To clarify, using the above expression, I can change the opacity of the tileset item where NO is 13708. My question is, is there a way to get the lat/long coordinate of the tileset item where NO is 13708?
You could just iterate over all rendered features and test the "NO" property then return the coordinates of this feature. To get all rendered features you need to use queryRenderedFeatures. Features follow the GeoJSON structure.
For example:
let features = map.queryRenderedFeatures({ layers: ['my-shp'] });
let filtered = features.filter(feature => {
return feature.properties.NO === 13708;
});
console.log(filtered);

Mapbox: Filtering out markers in a Leaflet Omnivore KML layer

I am exporting Google Directions routes as KML and displaying them on a Mapbox map by reading them with Omnivore and adding them to the map,
The Google KML stores each route as two Places (the start and end points) and one LineString (the route). In Mapbox I would like to show only the routes, that is to filter out the markers somehow. I'm displaying markers out of my own database and the Google markers clutter it up.
Here is my code. I change the styling of the LineStrings just to show that I can, but do not know what magic call(s) to make to not display the Points.
Thanks.
runLayer = omnivore.kml('data/xxxx.kml')
.on('ready', function() {
var llBnds = runLayer.getBounds();
map.fitBounds(llBnds);
this.eachLayer(function (layer) {
if (layer.feature.geometry.type == 'LineString') {
layer.setStyle({
color: '#4E3508',
weight: 4
});
}
if (layer.feature.geometry.type == 'Point') {
//
// Do something useful here to not display these items!!
//
}
});
})
.addTo(map);
Welcome to SO!
Many possible solutions:
Most straight forward from the code you provided, just use the removeLayer method on your runLayer Layer Group when you get a 'Point' feature.
Cleaner solution would be to filter out those features before they are even converted into Leaflet layers, through a custom GeoJSON Layer Group passed as 3rd argument of omnivore.kml, with a specified filter option:
var customLayer = L.geoJSON(null, {
filter: function(geoJsonFeature) {
// my custom filter function: do not display Point type features.
return geoJsonFeature.geometry.type !== 'Point';
}
}).addTo(map);
var runLayer = omnivore.kml('data/xxxx.kml', null, customLayer);
You can also use the style and/or onEachFeature options on customLayer to directly apply your desired style on your LineString.

Updating layers in Leaflet / Mapbox

I'm trying to make a mapping visualization in realtime, where I keep getting new points via websockets. The initial plotting these markers on the map seems simple, but I'm not sure what's the right way of updating a layer on Mapbox.
As of now, whenever I get a new point, I remove the old layer, create a new one and then add it on the map. The problem with this approach is that it is slow and for high number of points (>5000) it starts lagging.
// remove layer
if (this.pointsLayer != null) {
map.removeLayer(this.pointsLayer);
}
// build geoJSON
var geoJSON = { "type": "FeatureCollection", "features": [] };
geoJSON["features"] = tweets.map(function(tweet) {
return this.getGeoPoint(tweet);
}.bind(this));
// add geoJSON to layer
this.pointsLayer = L.mapbox.featureLayer(geoJSON, {
pointToLayer: function(feature, latlon) {
return L.circleMarker(latlon, {
fillColor: '#AA5042',
fillOpacity: 0.7,
radius: 3,
stroke: false
});
}
}).addTo(map);
Is there a better way?
You can create an empty GeoJSON layer by passing it a false instead of real data:
//create empty layer
this.pointsLayer = L.mapbox.featureLayer(false, {
pointToLayer: function(feature, latlon) {
return L.circleMarker(latlon, {
fillColor: '#AA5042',
fillOpacity: 0.7,
radius: 3,
stroke: false
});
}
}).addTo(map);
then use .addData to update it as new tweets come in. Something like:
// build geoJSON
var geoJSON = { "type": "FeatureCollection", "features": [] };
geoJSON["features"] = /**whatever function you use to build a single tweet's geoJSON**/
// add geoJSON to layer
this.pointsLayer.addData(geoJSON);
For a single tweet, I guess you could just create a Feature instead of a FeatureCollection, though I don't know whether that extra layer of abstraction would make any difference in terms of performance.
EDIT: Here is an example fiddle showing the .addData method at work:
http://jsfiddle.net/nathansnider/4mwrwo0t/
It does slow down noticeably if you add 10,000 points, and for 15,000 points, it's really sluggish, but I suspect that has less to do with how the points are added that the demands of rendering so many circleMarkers.
If you aren't already, you may want to try using the new Leaflet 1.0 beta, which redraws vector layers faster and is generally much more responsive with large datasets. Compare this 15,000-point example using Leaflet 0.7.5 to the same code using Leaflet 1.0.0b2. Not everything is fixed (popups take a long time to open in both), but the difference in lag time when trying to drag the map is pretty dramatic.
There's no reason to go through the intermediate step of construction a GeoJSON object just so you can add it to the map. Depending on your exact needs, you can do something like this:
tweets.forEach(function(t) {
L.marker(this.getGeoPoint(t)).addTo(map);
}, this);
You should manage the tweets object so it only contains points that are not already visible on the map, though. Deleting all the old markers, just so you can add them again, is of course going to be very slow.
I would take a look at Leaflet Realtime:
Put realtime data on a Leaflet map: live tracking GPS units, sensor data or just about anything.
https://github.com/perliedman/leaflet-realtime