Declarative styling
Short-Term
Spec
@ggetz
- [ ] Better name than "3D Tiles Style"
- [ ] Hackathon to experiment with current version
@pjcozzi
- [x] TODOs in spec
- [x] Finish spec and schema
Cesium Implementation
@ggetz
- [x] Latest schema for color map and color ramp
- Remove
BooleanExpression,NumberExpression, and friends - Unit tests:
Cesium3DTileStyle(and implicitly forCesium3DTileStyleEngine)
- Remove
- [x] Load style from uri, not just JSON string.
- [x] Pass
styleEnginelast to constructors (and make it optional?) - [x] Example evaluating a style/expression on pick
- [x] Remove chroma - 18d80354be225a116d0a711bb79b190871a82534 and e7aebcddacdcdcc00bb5120215d7f248a5acb29a (and
ColorRampExpression.computeEvenlySpacedIntervals)
@pjcozzi
- [x] Reference doc
- [x] Update CHANGES.md
- [x] Run coverage and see if we are missing any tests for 3D Tiles styles
Later
Spec
- [ ] Add
JulianDatedatatype to support styling with date/time metadata - [ ] Introduce translucency property to assign to without having to set RGB?
- [ ] And/or built-in variables to get the feature's current color and show?
- [x] Operator overloads for regular expressions (we already overload for
Colorso why not here?), see https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/2#issuecomment-184332713 - [ ] Allow variable names inside member expressions, eg.
${foo[${memberName}]} - [x] Non-visual properties, e.g., for infobox display, e.g.
{
"show" : true,
"color" : [255, 255, 255],
"meta" : {
"description" : "Hello, ${FeatureName}"
}
}
var str = style.meta.description.evaluate(feature);
- [ ] Escape variables in strings.
- [ ] In general, is the ability to assign to feature properties in the style (as part of the JSON or maybe expression) useful?
- [ ] Improved "standard library"
- [x] Cesium built-in math functions, e.g.,
distance - [ ] Check out GLSL
- [ ] Built-ins like GLSL, e.g.,
czm_time,czm_inShadow, etc. - [ ] Include GLSL in a style?
- [ ] Cascading multiple styles on one tileset?
- [ ] Add explicit
intervalto optimizeconditionalwhen used for intervals - [ ] Referencing other features in a style
- [ ] Built-ins like GLSL, e.g.,
color : ramp(palette, distance(${thisFeature.location}, ${anotherFeature.location}))
show : (${thisFeature.height} > ${anotherBuilding.height})
- [ ] Multiple expressions in conditions, https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/2#issuecomment-191754803
- [ ] Generate reference doc from JSON schema
- [ ] Core styling schema + extensions for buildings, vector data, point clouds, etc.?
- [ ] Influence from CZML, i.e., time-dynamic styles
- [ ] Integration with Cesium
Propertysystem, e.g., evaluate expressions asPropertyor vice-versa.
- [ ] Integration with Cesium
- [ ] Other style languages, e.g., more like CSS selectors
- [x] Support vector and matrix properties since this is supported in the batch table binary.
- [ ] Bounding volume hierarchy information (https://github.com/AnalyticalGraphicsInc/cesium/issues/4654)
- [ ] Tile bounding volumes
- [ ] Level in tree
- [x] Support vector types like vec2, vec3, and vec4
- [x] Allow component-wise math operations similar to GLSL
Cesium Implementation
- [ ] Interfaces for
Cesium3DTileStyleand friends for users writing their own styles. - [ ] Fix jsep to allow for single backslash characters to be parsed correctly. Workaround is to replace before jsep for now.
- [ ] Column numbers for syntax errors
- [ ] Use regex and replace function for
replaceVariablesinExpression.js? - [x] GPU implementation for tile formats that would really benefit, e.g., point clouds
- [ ] Line width (can we make this as simple as a uniform/batch-value and not recreate geometry?)
- [ ] Expose AST?
- [ ] Expose Conditional properties?
- [ ] Apply to GeoJSON?
- [ ] Optimizations if needed: constant folding and dead code elimination
- [ ] Schema validation - how much do we do at runtime? Tool for offline validation?
- [ ] Style features in parallel
Built-in functions
- [x] radians(x) - convert value to radians
- [x] degrees(x) - convert value to degrees
- [x] cos(x)
- [x] sin(x)
- [x] tan(x)
- [x] asin(x)
- [x] acos(x)
- [x] atan(x)
- [x] atan2(y,x) - atan(y,x) in GLSL
- [x] pow(x,y)
- [x] sqrt(x)
- [x] abs(x)
- [x] sign(x)
- [x] floor(x)
- [x] ceil(x)
- [x] round(x)
- [x] min(x,y)
- [x] max(x,y)
- [x] clamp(x,y,z)
- [x] mix(x,y,a)
- [x] length(x)
- [x] distance(x,y)
- [x] dot(x,y)
- [x] cross(x,y)
- [x] normalize(x)
- [ ] random - 0.0 to 1.0 (some complexity on the GLSL side)
- [x] exp(x)
- [x] log(x)
- [x] exp2(x)
- [x] log2(x)
- [x] fract
- [x] time
- [x] Consider data-driven implementation to avoid lots of duplication where the only difference is the function name and number of arguments.
The immediate need for declarative 3D Tiles styling is to be able to write a simple expression that maps a feature's property (or properties) value to its appearance (initially, show/color), e.g.,
- "Show all buildings greater than 10 meters tall"
- "Use a color ramp to color buildings based on the number of floors"
There's early implementation work in Cesium's 3d-tiles-style branch.
The styling format is JSON, and the schema is not at all final. Here's a few examples:
// Show all buildings greater than 10 meters tall, e.g., `${Height} > 10`
{
"show" : {
"leftOperand" : "${Height}",
"operator" : ">",
"rightOperand" : 10
}
}
// Create a color ramp with the provided palette based on the number of floors in a building
{
"color" : {
"expression" : { // Will make this less verbose soon, just ${Floors}
"leftOperand" : "${Floors}",
"operator" : "+",
"rightOperand" : 0
},
"intervals" : [
1, // [1, 10)
10, // [10, 19)
20 // [20, infinity)
],
"colors" : [
[237, 248, 251],
[158, 188, 218],
[129, 15, 124]
]
}
}
// Map a feature's ID to a color
{
"color" : {
"propertyName" : "id",
"map" : {
"1" : [255, 0, 0],
"2" : [0, 255, 0]
},
"default" : [255, 255, 255]
}
}
In Cesium, a style can be applied to a Cesium3DTileset object, e.g.,
tileset.style = new Cesium.Cesium3DTileStyle(tileset, styleJson);
Creating the Cesium3DTileStyle object basically "complies" the style, and assigning it to tileset.style tells the tileset to apply the style (efficiently based on visibility and if the style or feature properties have changed).
After the style is compiled, it can be changed, e.g.,
tileset.style.show.operator = '===';
Feature properties that may impact how the style is evaluated can also be changed, e.g.,
feature.setProperty('id', 1);
In addition to declarative styling, the lower-level API can be used to override the appearance of individual features, e.g.,
var feature = scene.pick(movement.endPosition);
if (Cesium.defined(feature)) {
feature.color = Cesium.Color.YELLOW;
}
Outdated, but still useful:
Schema Ideas
- [x] Shorthand schema for literal boolean expression for
show - [x] Use CSS color formats throughout
- [x] Add RegEx pattern to color ramp
- [x] Resolve an expression (including reference to a property name) to a color (using optional regex)
- [x] Replace expression JSON with real expressions, e.g.,
${Height} * 2 > (${Width} + 1) / 3. The JSON becomes the AST, which is (initially if not always) not part of the spec, but rather a Cesium implementation detail.
Related-ish
- SQL
- Tangram Scene file, filters in particular
- MapBoxGL Styles
- CartoCSS, selectors in particular. Also see The end of CartoCSS.
I'd much rather use a concise syntax (e.g. "{Height} > 100") than individual properties, which would get really messy for complex rules.
A simple parser can transform the concise syntax into the equivalent structure in a precompilation step.
Something that I could see being used is also the ability to specify styling programmatically, so maybe define an abstract "Cesium3DTileStyleFormatter" interface in Cesium that can be implemented by the developer to specify the styling at runtime, e.g.:
var sf = new MyStyleFormatter();
// ...
tileset.style = new Cesium.Cesium3DTileStyle(tileset, sf);
Where MyStyleFormatter implements:
show: function(tile){
return tile.Height > 100;
}
It could be useful to do things like:
color: function(tile){
return new StaticColor(Math.random() * 255, Math.random() * 255, Math.random() * 255);
}
To color buildings at randoms (and other possibilities).
In fact a JSON style could be a special case of programmatic styling:
var sf = new JsonStyleFormatter(styleJson);
// ...
tileset.style = new Cesium.Cesium3DTileStyle(tileset, sf);
Thanks for the input, @pierotofy. We definitely want to support custom functions for expressions, and, I agree, will most likely go with the concise syntax. Thanks for the code snippets!
This is one area where I think Cesium's current Property system can be leveraged to create some powerful capabilities. I'm not saying the Property system exactly as it is today is a perfect fit, but ultimately I personally feel it's where things will go. I've wanted to add additional Property capabilities for a while and this may be a good reason to look into that.
On Fri, Feb 5, 2016 at 10:16 AM, Patrick Cozzi [email protected] wrote:
Thanks for the input, @pierotofy https://github.com/pierotofy. We definitely want to support custom functions for expressions, and, I agree, will most likely go with the concise syntax. Thanks for the code snippets!
— Reply to this email directly or view it on GitHub https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/2#issuecomment-180396071 .
Update https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/2#issuecomment-180355858 to account for the latest implementation work and offline discussion with @mramato.
The styling spec will be in the 3d-tiles/spec branch (just a placeholder for now). I'll bootstrap the prose/schema writing as things start to solidify.
Notes on parsers
- Expression parsers
- Small and easy to use: http://jsep.from.so/
- 3-part tutorial: http://ariya.ofilabs.com/2011/08/math-evaluator-in-javascript-part1.html
- Library: https://code.google.com/archive/p/tapdigit/
- More than we need now: http://mathjs.org/
- Full JavaScript parsers
- Lean: https://github.com/ternjs/acorn
- Popular: http://jointjs.com/demos/javascript-ast
We'll start with jsep.
Notes on expressions
Now
- [x] Types
- [x] Number
- [x] Boolean
- [x] String
- Might be a jsep bug with escaped quotes, e.g.,
"a \"b\" c"
- Might be a jsep bug with escaped quotes, e.g.,
- [x] Color (CSS color format)
- [x] Variable (feature property) - resolves to Number, Noolean, String, or Color (or null I suppose)
- [x] Short-circuit expressions
- [x] Member expressions
- [x] Spec
- [x] Implementation
- [x] Arrays
- [x]
Boolean,Number,Stringconstructors- [x] Spec
- [x] Implementation
- [x] RegEx
- [x] Implementation
- [x] Spec
- [x] toString
- [x] Evaluate colors take non-literal arguments in constructor
- [x] Constructor function changes
- [x] Fast path for well-known expressions, e.g.,
${Property} + literal - [x] Remove debugging console.log statements
- [x] Examples / test cases (good start, but not complete):
"show" : 'a // Syntax error
"show" : true // Boolean literal
"show" : !false // Unary NOT
"show" : true && true // Logical AND
"show" : false && true // Logical AND, short-circuit
"show" : false || true // Logical OR
"show" : true || false // Logical OR, short-circuit
"show" : (false && false) || (true && true)
"show" : true ? true : false // Conditional, short-circuit-ish
"show" : false ? false : true // Conditional, short-circuit-ish
"show" : 2 > 1 // And similiar for <, <=, >=, ===, !===
"show" : 0 > -1 // Unary NEGATE
"show" : (1 + 1) > 1 // *, /, %
// For now, do not support:
// * Unary: ~, +
// * Binary : |, ^, &, ==, !=, <<, >>, >>>
// * Expressions
// * Array, e.g., [1, 2]
// * Compound, e.g., 1; 2; 3
// * Member, e.g., feature.name // might need this soon
// * This, e.g., this
// See node types, operations, and literals in the annotated source: http://jsep.from.so/annotated_source/jsep.html
"show" : "${SomeFeatureProperty}" // Feature property is a boolean
"show" : "${ZipCode} === 19341" // Variable equals number
"show" : "${Temperature} > 100"
"show" : "(${Temperature} > 100) && ((${Weight} / ${Area}) > 2.0)"
"show" : "${County} === 'Chester'" // Property name equals string
"show" : "${County} === regExp('/^Chest/')" // String compare with RegEx. Open to other syntax
"show" : "${County} !== null" // I guess we should support null, what about undefined?
"show" : 1 // Convert number to boolean I suppose
// CSS colors
"color" : "#EAA56C"
"color" : "cyan"
"color" : "rgb(100, 255, 190)"
"color" : "hsl(250, 60%, 70%)"
"color" : "(${Temperature} === 'hot') ? 'red', '#ffffff'"
"color" : "rgb(255, 0, 0, (${Visible} ? 255 : 0))"
"color" : "rgb(${red}, ${green}, ${blue}, 255)" // Number properties
"color" : "rgb(${red} * 255, ${green} * 255, ${blue} * 255, 255)" // Convert 0..1 to 0..255
For RegEx syntax we could also use (borrowed from Perl/Ruby):
"show" : "${County} =~ /^Chest/" // Matches
"show" : "${County} !~ /^Chest/" // Does not match
null and undefined should probably be both supported (and enforce strong typing):
"show" : "${County} !== null && ${Country} !== undefined"
"show" : "${County} !== null" // Does not match undefined values
"show" : "${County} !== undefined" // Does not match null values
I would vote in favor of not allowing casts from numbers to bools, just to enforce better code practices, but it's not a big deal if they are allowed.
"show" : "1" // Error: "Expected bool, got number"
"show" : "${param} === 1" // OK
Thanks @pierotofy, @ggetz is starting the design/implementation now. We'll look at these cases; I'm not sure about introducing =~ yet though as we want to map to JavaScript as best as we can.
@ggetz check out the new Color section in the styling spec and let me know what you think about the TODOs (just open PRs for them) and please make sure our implementation and spec are in-sync and that we reasonably covered the edge cases.
I added my comments in https://github.com/AnalyticalGraphicsInc/3d-tiles/pull/70
@ggetz there is a new section on Number. Let me know your thoughts and please bring our implementation in sync. Also note that isNaN() is true and isFinite() is false due to implicit conversion to Number.
@ggetz there is a new section on conversions. Please make sure to test all possible Color conversions carefully, e.g., Color() !== undefined, Color() !== null, !Color() === false, etc. We want the semantics to be the same as if we were using the Cesium Color object in JavaScript.
Note that we have to add a toString() function to Color for implicit conversion to string (or explicit via Color().toString()). We'll have to add a few more functions to make it walk and talk like a real JavaScript object. More info to follow.
@ggetz for strings, make sure we parse both ' and ".
@ggetz there is a new section on operators. Please make sure our implementation is in-sync (in particular I added the unary + and I don't know if we implemented ternary yet) and carefully tested.
@ggetz are we in-sync with this:
Color supports the following binary operators by performing component-wise operations:
===,!==,+,-,*,/, and%. For exampleColor() === Color()is true since the red, green, blue, and alpha components are equal.
Note that we have to add a toString() function to Color for implicit conversion to string (or explicit via Color().toString()). We'll have to add a few more functions to make it walk and talk like a real JavaScript object. More info to follow.
I looked more at this. Just toString() is fine for our current purposes. See Section 19 here if interested.
@ggetz do you want to design the regex support?
Color supports the following binary operators by performing component-wise operations: ===, !==, +, -, *, /, and %. For example Color() === Color() is true since the red, green, blue, and alpha components are equal.
Yes this is implemented
do you want to design the regex support?
Sure
There is a new section on Number. Let me know your thoughts and please bring our implementation in sync. Also note that isNaN() is true and isFinite() is false due to implicit conversion to Number.
Looks good, I will put this in the implementation
there is a new section on conversions. Please make sure to test all possible Color conversions carefully, e.g., Color() !== undefined, Color() !== null, !Color() === false, etc. We want the semantics to be the same as if we were using the Cesium Color object in JavaScript.
OK
there is a new section on operators. Please make sure our implementation is in-sync (in particular I added the unary + and I don't know if we implemented ternary yet) and carefully tested.
Yes, I'll have to add in unary + and ternary.
@ggetz there's a new section on variables. Please make sure to test all the different datatypes.
for strings, make sure we parse both ' and ".
I believe jsep handles this, I will add a test to confirm.
there's a new section on variables. Please make sure to test all the different datatypes.
For variables, do we support multilevel properties? For example,
feature : {
property : {
subproperty : 1
}
}
can this be reached by ${property.subproperty} ?
For variables, do we support multilevel properties?
Probably, see #65, I just need to double check everything will work out throughout the pipeline.
Hold on this for the moment.
@ggetz what is the behavior of CSS colors for values out of range (e.g., red = 300). Please make sure we match it, e.g., throw error, clamp, etc.
when CSS colors are out of range with the rgb(), hsl(), etc.. functions, they just clamp the value to the max and min.