mobile-sdk
mobile-sdk copied to clipboard
Carto-Css Dynamic List of Colors
I am currently using SDK v4.4.0, and I saw that SDK v4.4.2 was recently released which includes some additional Carto-Css related features.
One question I had is:
- Added support for complex CartoCSS selectors ('when' selectors)
Question 1. What exactly are these "when selectors"? I couldn't find anything on them online. What are they useful for?
Question 2. Here is my main question, I have a requirement for the following:
- Receive from API a list of polygon IDs which should be highlighted on the map in a given set of colors.
- For example,
List<Color, List<PolygonIDs>>So, for example,
#565eba --> Ids: 1, 7
#c9aa2e--> Ids: 2, 10
#e00dd9--> Ids: 3, 6
... etc
- In general, the number of different colors and the Ids for each could be arbitrary
Here is my current "best" solution:
polygon-fill: ([ID] =~ [nuti::selected_id_regex]
? @selected_fill
: ([ID] =~ [nuti::custom_highlight_id_regex_1]
? [nuti::custom_highlight_color_1]
: ([ID] =~ [nuti::custom_highlight_id_regex_2]
? [nuti::custom_highlight_color_2]
: ([ID] =~ [nuti::custom_highlight_id_regex_3]
? [nuti::custom_highlight_color_3]
: ([ID] =~ [nuti::custom_highlight_id_regex_4]
? [nuti::custom_highlight_color_4]
: ([ID] =~ [nuti::custom_highlight_id_regex_5]
? [nuti::custom_highlight_color_5]
: ([name] = ""
? @vacant_fill
: @default_fill)
)
)
)
)
)
);
I have verified that this solution does work, although, it is obviously very ugly and only supports up to 5 different colors. Note: Here I have used different regex nuti params to indicate which "type" the polygon should be and also which dynamic color they should have.
Is there a more elegant way of solving this problem? Ideally one that can handle an arbitrary number of supplied "custom highlight colors"?
Question 3: I would need the above line to be copy/pasted and very lightly modified (e.g., changing the default_fill) for different polygon types/classes. Is there a way that I could write my own function in cartoCss? For instance, the linear function allows for a very simple one line of code while specifying various parameters.
For example, if I could create a function like so:
function custom_fill (@vacant_fill, @default_fill) {
// Use the above logic
}
Then, I could just have:
[class='type_1'] {
polygon-fill: custom_fill(@vacant_fill_type_1, @default_fill_type_1);
building-fill: custom_fill(@vacant_fill_type_1, @default_fill_type_1);
}
[class='type_2'] {
polygon-fill: custom_fill(@vacant_fill_type_2, @default_fill_type_2);
building-fill: custom_fill(@vacant_fill_type_2, @default_fill_type_2);
}
This would greatly simplify my carto-css code.
Thanks for your feedback and suggestions!
Hi.
Regarding the questions.
-
Correct,
whenselectors were added in SDK 4.4.2. They are briefly described in the wiki: https://github.com/CartoDB/mobile-sdk/wiki/CartoCSS-notes#complex-selectors -
Unfortunately I can not think of an elegant way for this.
whenselectors would perhaps allow to create more readable CartoCSS, but you would be still limited to a fixed number of cases. You could always create you CartoCSS dynamically based on the number of cases, but that is also somewhat ugly.
We currently do not have plans for adding custom functions or more advanced features to CartoCSS. But we may look into this in the intermediate future to have closer feature match with 'less.js' (CartoCSS is loosely based on 'less.js').
Thanks for the reply. Regarding the use of when selectors, I noticed the caution in terms of its usage. I assume that is specifically referring to complex predicates using a lot of && and || etc? But that simple when selectors should not be very heavy, correct?
Specifically, in this case:
Current implementation:
polygon-fill: ([ID] =~ [nuti::selected_id_regex]
? @selected_fill
: ([ID] =~ [nuti::custom_highlight_id_regex_1]
? [nuti::custom_highlight_color_1]
: ([ID] =~ [nuti::custom_highlight_id_regex_2]
? [nuti::custom_highlight_color_2]
: ([ID] =~ [nuti::custom_highlight_id_regex_3]
? [nuti::custom_highlight_color_3]
: ([ID] =~ [nuti::custom_highlight_id_regex_4]
? [nuti::custom_highlight_color_4]
: ([ID] =~ [nuti::custom_highlight_id_regex_5]
? [nuti::custom_highlight_color_5]
: ([name] = ""
? @vacant_fill
: @default_fill)
)
)
)
)
)
);
New Implementation:
/** This would be the base case which handles normal selection / vacant / normal */
polygon-fill: ([name] = "" ? @vacant_fill : ([ID] =~ [nuti::selected_id_regex] ? @selected_fill : @default_fill));
/** These would overwrite the above in the case of custom highlighting */
when ([ID] =~ [nuti::custom_highlight_id_regex_1]) {
polygon-fill: [nuti::custom_highlight_color_1];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_2]) {
polygon-fill: [nuti::custom_highlight_color_2];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_3]) {
polygon-fill: [nuti::custom_highlight_color_3];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_4]) {
polygon-fill: [nuti::custom_highlight_color_4];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_5]) {
polygon-fill: [nuti::custom_highlight_color_5];
}
Actually no, it does not matter how complex each condition in 'when' selector is, it really depends on the number of 'when' selectors and other selectors used. SDK currently does no optimisations in case on 'when' selectors and the final number of rules generated would likely be exponential on the number of cases. So the current implementation would likely perform better.
Does the size of the carto-css file matter much?
For example, using my current approach, for each different polygon type (which may be like ~30 different types), I have to have that massive nested a ? b : c logic, so the file becomes harder to read and also much larger.
By contrast, using those 5 "when" selectors I can simplify the other 30 different include statements a ton.
So, the "when" version would be more readable and also result in a smaller file.
I want to better understand the exponential complexity part. Because from my understanding, using my "original" scheme. For each polygon it must apply 7 checks (selected vs. highlighted x 5 vs. vacant).
Using the new "when" selector, for each one, I would have first 2 checks (vacant vs selected) and then it would perform the check for each of the "when" (x5) so it would seem the same 7 checks.
I understand that these "when" operators would be performed on all polygons, presumably that is the exponential complexity you're referring to. However, since I will want that nested logic for all polygons, wouldn't it be effectively the same thing?
I may have misunderstood. Thanks for clarifying.
Also, given that I am applying this logic for both polygon-fill and building-fill, using my original scheme is actually 2x as many checks, so 2 x 7 = 14.
However, using this "when" selector scheme, I think I can reduce the checks because I could do:
when(...) {
polygon-fill: ...
building-fill: ...
}
Unless again, I've missed something
@lasyakoechlin I understand this can be confusing, so I will give some general context about CartoCSS processing. There are basically three phases where processing occurs:
- During CartoCSS parsing (
MBVectorTileDecoderconstruction). This phase will create a linear list of rules that will match feature properties from vector tiles and symbolizers (line, point, polygon, building renderers basically) - During decoding vector tiles. This is done in background threads and with very complex rules it may take longer to process the tile. Usually this phase is not a real bottleneck.
- During rendering. Symbolizer expressions are evaluated and geometry with attributes is rendered using OpenGLES. This can be a bottleneck with heavy geometry (if >1000000 triangles are needed to render the geometry) or if there is a large number of draw calls required. The number of draw calls required is somewhat difficult to describe, there is some additional info about this in the project Wiki.
When I mentioned exponential number of rules generated, this refers only to the first step. The final number of rules generated for step 2 will be linear in the number of when clauses after the optimization. So with large number of when rules you may notice a small delay when calling MBVectorTileDecoder constructor. If you use less than 15 when rules per layer, I do not think there is a noticeable delay. I suggest doing a performance test yourself, perhaps I was too quick to warn about exponential complexity.
@mtehver i have been in some kind of need for this too and i have been thinking we could get it to work with some new string functions.
Let say you would store in a nuti param 123:#ff00ff,431:#00ff00
With =~ i could test that ID is in the nuti param. Now i need three methods to get the color value:
- get the start index of
=~:regexpStartIndex(regexp:string): number - count chars up to some index:
countChars(char:string, index: number): number - like an array get method using split:
stringSplitGet(str:string, splitChar:string, index): string
With that you could then get the color with stringSplitGet(stringSplitGet([nuti::param], ",", countChars(",", regexpStartIndex([ID])), ":", 1)
Not straight forward but some methods like this could allow to do what @lasyakoechlin with one rule i think
Thanks for the detailed response @mtehver. That makes sense. So, the exponential delay is only on the object creation. Which is less of an issue for me since it occurs in the background before the MapView is needed. I only used 5 when clauses, and I noticed no additional delays when rendering, etc.
So, just to confirm that the only difference between:
polygon-fill: ([ID] =~ [nuti::selected_id_regex]
? @selected_fill
: ([ID] =~ [nuti::custom_highlight_id_regex_1]
? [nuti::custom_highlight_color_1]
: ([ID] =~ [nuti::custom_highlight_id_regex_2]
? [nuti::custom_highlight_color_2]
: ([ID] =~ [nuti::custom_highlight_id_regex_3]
? [nuti::custom_highlight_color_3]
: ([ID] =~ [nuti::custom_highlight_id_regex_4]
? [nuti::custom_highlight_color_4]
: ([ID] =~ [nuti::custom_highlight_id_regex_5]
? [nuti::custom_highlight_color_5]
: ([name] = ""
? @vacant_fill
: @default_fill)
)
)
)
)
)
);
and:
/** This would be the base case which handles normal selection / vacant / normal */
polygon-fill: ([name] = "" ? @vacant_fill : ([ID] =~ [nuti::selected_id_regex] ? @selected_fill : @default_fill));
/** These would overwrite the above in the case of custom highlighting */
when ([ID] =~ [nuti::custom_highlight_id_regex_1]) {
polygon-fill: [nuti::custom_highlight_color_1];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_2]) {
polygon-fill: [nuti::custom_highlight_color_2];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_3]) {
polygon-fill: [nuti::custom_highlight_color_3];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_4]) {
polygon-fill: [nuti::custom_highlight_color_4];
}
when ([ID] =~ [nuti::custom_highlight_id_regex_5]) {
polygon-fill: [nuti::custom_highlight_color_5];
}
would likely be a slightly longer delay when creating the MBVectorTileDecoder object, but in run-time there should be no additional bottlenecks, correct?
For me, the convenience of a clean cartoCSS file far outweighs that initialization cost.
I like the suggestion from @farfromrefug in terms of the ability to split strings to represent array-like behavior. It still results in some messy logic, but it would be much more extendable. I really dislike having to hardcode a fixed number of regex.
I would also suggest offering the ability for custom functions to be written would be really helpful. I'm not sure how complicated that would be. But if there was someway to include a functions file or list of files or something, that would allow for a great simplification to the CartoCss process.
Most of my CartoCss code is somewhat repetitive with just different parameters (e.g., changing colors).
After implementing the when selector based highlighting, I am seeing what looks like a bit of clipping or something in the map rendering. I suspect based on the angles that it may have to do with map tile rendering? This is using 4.4.2 on both Android and iOS.
It seems to persist for different zoom levels, but occasionally I can find a zoom level where the polygons look normal, but if I zoom in or out more then the clipping reappears.
Are there any carto settings I can change to possibly improve this rendering? Thanks!



I am still having this issue. And for this figure there is no active highlighting at all. So, I think this rendering issue may be just a general issue with the 4.4.2. Even with the when selectors having no content.
@lasyakoechlin Could you make a small Android Studio demo project demonstrating the issue? Without reproducible testcase we have very little to act on.
It may take a bit of time to get one of those. But will try.
@lasyakoechlin SDK 4.4.4 fixed a clipping bug with GeoJSONVectorTileDataSource, so the issue is potentially fixed now.
Thanks! Will have a look. Sorry I never got a chance to put this together yet. Always on the "to do" list :)
@mtehver by the way, the clipping may have been reduced in 4.4.4 but not eliminated entirely. I know I still need to put together that sample project for you. I hope to do so in the near future.