Open pjcozzi opened 9 years ago
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.,
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:
show
${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.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 notifications@github.com 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
We'll start with jsep.
Now
"a \"b\" c"
Boolean
, Number
, String
constructors
${Property} + literal
"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.
OK, please make sure we do the same.
Also, what about a string that isn't hex or is too long?
That's covered in the the link @mramato posted.
make the string a length that is a multiple of 3 by adding 0s: chucknorris0 separate the string into 3 equal length strings: chuc knor ris0 truncate each string to 2 characters: ch kn ri keep the hex values, and add 0's where necessary: C0 00 00
OK, our implementation should work the same.
Trying the behavior myself in chrome, the color is ignored if it is not of the form #fff
or #ffffff
or an accepted color keyword, and ignores the color if there are any non-hex charters. Should we go with the legacy behavior described in the link or the chrome behavior?
Also I've been using Color.fromCssColorString()
to convert the css string. If we go with the former behavior, should I modify the fromCssColorString
function?
We should follow the specification and if Color.fromCssColorString
deviates from the spec, we should fix it. MDN has a summary and then links to the actual part of the spec that deals with color:
MDN: https://developer.mozilla.org/en-US/docs/Web/CSS/color CSS3 Colors: https://drafts.csswg.org/css-color-3/#color
@mramato is right in general, but is @ggetz saying that Chrome implements colors in CSS differently than the spec?
The css3 color spec says:
The format of an RGB value in hexadecimal notation is a ‘#’ immediately followed by either three or six hexadecimal characters. The three-digit RGB notation (#rgb) is converted into six-digit form (#rrggbb) by replicating digits, not by adding zeros. For example, #fb0 expands to #ffbb00. This ensures that white (#ffffff) can be specified with the short notation (#fff) and removes any dependencies on the color depth of the display.
Which matches the behavior of Chrome (and Color.fromCssColorString
)
So it sounds like everything is in-sync?
Yes.
:+1:
@ggetz can you bring the implementation in-sync with the latest on dot and bracket notation for member and array access? See
Make sure to test a complex nested use case, e.g., feature properties like:
{
"aaa" : {
"bbb" : "ccc",
"values" : [{
"ddd" : "eee",
"ggg" : [1, 2, 3]
}, {
"ddd" : "fff",
"ggg" : [4, 5, 6]
}]
}
}
(use better names)
@ggetz also let me know what you think about this case:
var foo = {
"a.b" : "string",
"a" : {
"b" : "object"
}
};
console.log(foo2.a.b); // object
console.log(foo2['a.b']); // string
With style expressions, what does ${a.b}
evaluate to? Perhaps object
, and then ${'a.b'}
evaluates to string
.
We could also require an explicit feature in this case (or always), e.g., ${feature.a.b}
and ${feature['a.b']}
. Then we need to handle the case when a feature has a feature property, e.g., perhaps ${feature} === ${feature.feature}
If this is really painful, we could explicitly disallow this for now, give precedence to object
in this case, and make it a TODO.
${a.b}
and ${'a.b'}
seem more intuitive and cleaner to me. Requiring an explicit feature without always requiring it is inconsistent. Also by not requiring the explicit feature, we don't need to worry about the feature
property case.
OK with me, see how the implementation feels.
@ggetz also please update the spec when you confirm this.
also please update the spec when you confirm this.
OK
@ggetz the spec currently says
Explicit
Boolean
,Number
, andString
constructor functions are not supported.
However, for example, if a tile stores YearBuilt
as a string, an expression like
${YearBuilt} >= 1970
"works" because of implicit conversion, but
${YearBuilt} === 1970
is never true. Perhaps we should add Boolean
, Number
, and String
for explicit conversion:
Number(${YearBuilt}) === 1970
If you agree, please update the spec and code.
The unary +
operator I recently implemented will convert a string to a number. So in this case, you can use +${YearBuilt} === 1970
to check if this is true. Is that sufficient?
For this specific case, yes. Given that we have omitted ==
and !=
, I still think we will need these for other cases, but we can wait until we have more experience with styling to see.
OK, I agree these should be added. I will add this to the roadmap.
Short-Term
Spec
@ggetz
@pjcozzi
Cesium Implementation
@ggetz
BooleanExpression
,NumberExpression
, and friendsCesium3DTileStyle
(and implicitly forCesium3DTileStyleEngine
)styleEngine
last to constructors (and make it optional?)ColorRampExpression.computeEvenlySpacedIntervals
)@pjcozzi
Later
Spec
JulianDate
datatype to support styling with date/time metadataColor
so why not here?), see https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/2#issuecomment-184332713${foo[${memberName}]}
distance
czm_time
,czm_inShadow
, etc.interval
to optimizeconditional
when used for intervalsProperty
system, e.g., evaluate expressions asProperty
or vice-versa.Cesium Implementation
Cesium3DTileStyle
and friends for users writing their own styles.replaceVariables
inExpression.js
?Built-in functions