How to use mapbox "case" expresion on nested properties? - mapbox

How to use nested value in order to use case == operator? Something like:
this.map.setPaintProperty("somelayer", "fill-color",
["case",
["==", ["properties:some_prop"], someval],
"#34c0dd",
"#499bbc"]
where properties is dict:
properties = {
some_prop: 1,
some_prop2: 2,
// and so on
}
I have tried ["properties.some_prop"] and ["properties"]["some_prop"] and that does not work as well.
And how to print that mapbox query like console.log or something?

If properties is just the regular properties field on a GeoJSON object, then you don't mention it explicitly - all those fields are just accessed directly:
this.map.setPaintProperty("somelayer", "fill-color",
["case",
["==", ["get", "some_prop"], someval], "#34c0dd",
"#499bbc"
]);
Assuming #499bbc is the default colour you want.

Related

Cluster markers in MapBox, how to "accumulate" just distinct properties?

I want to show markers on a map, where each marker is a UserLocation. A User can have multiple UserLocation.
When I cluster markers, I'd like to show the list of the Users of those clustered markers, without duplicates.
For instance, let's take these 3 near markers:
{ // Marker1
type: 'Feature',
properties: {user_id : "Daniele"},
geometry: { type : 'Point',
coordinates : [lng0, lat0]
}
},
{// Marker2
type: 'Feature',
properties: {user_id : "Daniele"},
geometry: { type : 'Point',
coordinates : [lng1, lat1]
}
},
{// Marker3
type: 'Feature',
properties: {user_id : "Roberto"},
geometry: { type : 'Point',
coordinates : [lng2, lat2]
}
}
When I cluster them, clicking the clustered circle, I want to see "Daniele, Roberto".
How can I do that?
Moreover, I'd like to set the size of circle, according to the distinct number of different users clustered (in the example above, should be 2).
**UPDATE 2
JSFIDDLE <--
An idea could be build an array of distinct names, and then use the length expression to size the circle.
Anyway, there should be a kind of syntax error...
clusterProperties: {
distinctNames :
['case',
/*cond */ ["!", ['in',['get', 'user_id'], ['accumulated']]],
/*result*/ ['concat', ['concat', ['get', 'user_id'], ',']],
/*default*/ ['accumulated']
]
}
According to the documentation you want to do something like this:
map.addSource(userData, {
id: 'user-locations',
type: 'geojson',
data: 'myuserdata.geojson',
cluster: true,
clusterProperties: {
names: ['concat', ['concat', ['get', 'user_id'], ',']]
}
}
Clustered points in your source will now have a property, names which will contain the comma-separated (and comma-terminated) string of names.
Moreover, I'd like to set the size of circle, according to the distinct number of different users clustered (in the example above, should be 2)
That sounds...challenging. One way I can think of doing that would be writing a custom accumulator function along these lines:
Make the function return an array of two values, [distinctNames, allNames] where the first is an integer, and the second is a string.
If allNames contains our current name, just return the array.
Otherwise, return an array which is [distinctNames + 1, allNames + thisName].
Manipulating arrays like this in Mapbox GL expressions is possible, but pretty fiddly. You need to use ['literal', ...] and ['at', ...]
The code would look something like this:
clusterProperties: {
names: ['concat', ['concat', ['get', 'user_id'], ',']],
distinctNames: [
['case', ['in', ['get', 'distinctNames'], ['at', ['accumulated'], 1]
['accumulated'],
['literal', ['+', ['at', ['accumulated'], 0], 1], ['concat', ['at', ['accumulated'], 1], ['get', 'distinctNames']]]
],
['concat', '%', ['get', 'user_id'], '%'] // wrap user ID in some unique character so we don't accidentally find user "rob" within another user "robin" for instance.
]
}
It's unclear from the documentation exactly how the accumulator function works, or how you access the current value. Their example implies that it would be ['get', <name of cluster property>] although that seems a bit weird.
The documentation is not really clear, but here is how I achieved this "distinct accumulate".
From the clusterProperties definition:
A custom reduce expression that references a special ["accumulated"] value, e.g.:
{"sum": [["+", ["accumulated"], ["get", "sum"]], ["get", "scalerank"]]}
Which results the same as: {"sum": ["+", ["get", "scalerank"]]}
In your case, you want to accumulate the user_id property from your markers without duplicates.
The logic is to add the user_id only if it has not already been added in the accumulated value.
clusterProperties: {
distinctUsers: [
// ['accumulated'] is the current value iterated during the reduce (the property is defined at [1])
// ['get', 'distinctCountries'] is the accumulated / concatenated string
[
// Concat accumulated value + current value if not present in accumulated
'concat',
['get', 'distinctUsers'],
[
'case',
['in', ['accumulated'], ['get', 'distinctUsers']], // If accumulated (user_id) has already been added
'', // Add EMPTY string
['concat', ', ', ['accumulated']], // Add the user_id (concatenated with a comma in your case)
],
],
['get', 'user_id'], // [1]: source marker property iterated in the custom reduce function
]
}
As Steve said in his answer, you could also wrap the user_id in some unique character so you don't accidentally find user "rob" within another user "robin" for instance.
The source property ['get', 'user_id'] defined at [1] would become :
['concat', '%', ['get', 'user_id'], '%']

How to query data by some conditions

I have a dataset, now I want to find the data(point) which have the value of "country" is "AU", and show them on the map,
how to express the variable in Mapbox JS GL code?
this is my code, but it is a filter method, I know it's wrong, how to express a condition not filter it?
var myFeatures = map.querySourceFeatures('my_layer'
filter: ["==", "country", "AU"]
});
You can filter source features like this:
const myFeatures = map.querySourceFeatures(
"points",
{
filter: ["==", ["get", "title"], "Mapbox SF"]
}
);
Here, "title" is the property name and "Mapbox SF" is the property value.
Here's a codepen to demonstrate this: https://codepen.io/manishraj/pen/jONQQgo

Mapbox js - tileset colors assignment

I am trying to create a colored uk areas map - I need the colors to be driven by local data (say for example sales by postcode), which will always change depending on user selections like report date ranges etc. I have a tileset source which I am assigning to a layer, as in the following (sensitive values blanked out):
this.map.addLayer({
"id": "uk-pc-data",
"type": "fill",
"source-layer": "***",
"source": {
type: 'vector',
url: '***'
}
});
I am then able to style the postcode areas within the tileset by addressing a name property on the tileset features, like so:
"paint": {
"fill-color": [
"match",
["get", "name", [{name: "PR"}, {name: "CH"}, {name: "LN"}]],
[
"PR",
"DD",
"AL",
"PO"
],
"hsla(344, 84%, 29%, 0.37)",
"hsla(131, 94%, 34%, 0)"
]
}
The above will assign one color to the matched areas, and a default to all the other (the non matched ones); What I would like to do, is computing the colour value locally (so based on data constantly changing) based on the feature name, like so
"paint": {
"fill-color": function (feature = {}) {
return localSalesByArea(feature.name)
}
}
This though does not seem to be possible: any help or pointers appreciated; I have been through examples from Mapbox
such as choroplet, heatmaps, expressions but these seem to rely on values delivered via the dataset itself (i.e Population), in my case the values which determine the color scale are separated (they come from an internal reporting api)
If the data on which the colours are determined does not exist in the tileset, you basically have to do a gigantic lookup by name.
You will have a function that generates the fill-color expression, something like:
function makeFillExpression(valuesByName) {
function valueToColor(value) {
return /* insert your choroplething special sauce here */;
}
return ret = [
"match",
["get", "name"],
Object.keys(valuesByName).map(name => valueToColor(valuesByName[name])),
'black'
];
}
Yes, it will be a very large and unwieldy expression.

Filter expanded OData request

I need to pass a parameter to the backend. I want to use $filter for this.
My code below is not working, there are no filters returned by io_tech_request_context->get_filter( )->get_filter_select_options( ) in the Figures entityset.
var aFilters = [ new sap.ui.model.Filter("TeamMembers/Figures/FILTERKEY", sap.ui.model.FilterOperator.EQ, sFilterValue) ];
this.getView().bindElement({
path: "/TeamDataSet('0')",
parameters: {
expand: 'TeamMembers/Figures'
},
filters: aFilters
});
Element binding / ContextBinding doesn't support the property filters since you're binding just a single entity and not a collection. If you need to filter Figures, you'll have to bind them to an aggregation (e.g. items on List), get the corresponding ListBinding object, and then call .filter from there.
Here is a similar example: https://embed.plnkr.co/AoIZI4/. Take a look at the handler onSearch.
In case someone actually wants to filter a list by expanded results, the navigation property can be applied in the Filter path which then gets resolved by the path of the bound binding object. For example:
<List items="{
path: 'myODataModel>/Products',
parameters: {
expand: 'ToSupplier'
}
}">
Then in Controller, filter Products by their supplier country:
new Filter({
path: "ToSupplier/Country",
operator: "EQ",
value1: "UK",
})
(From: https://embed.plnkr.co/wAlrHB/).
Be aware that this works only if expanding does not result in a collection but a single object (e.g. associated supplier). Filtering by looking up at an associated collection is supported as of OData V4 which can be achieved via FilterOperator.All / .Any.

Algolia filter equivalent to SQL IN for arrays?

I've got records in Algolia containing an array attribute with integer values, like :
{
...
choice_ids: [1, 99, 100, 200]
...
}
I want to filter all records containing any value of another array. For example I search for [1, 300, 400, 600, 700], I should get the record on top because it contains 1.
Should I construct the filter with OR arguments or is there a better way?
Should I construct the filter with OR arguments or is there a better way?
Yes that's the way to go.
index.search('....', { filters: '(choice_ids=1 OR choice_ids=99 OR choice_ids=100 OR choice_ids=200)' });
For me it wasn't working with '=' but with ':', meaning:
{ filters: 'choice_ids:1 OR choice_ids:99 OR choice_ids:100 OR choice_ids:200' })
For me, neither of #Léo Chaz Maltrait or #redox answers worked. I had to format mine like:
{ filters: '(choice_ids:"1") OR (choice_ids:"99" OR (choice_ids:"100") OR (choice_ids:"200"))' })
I am also using algoliasearch npm package.