3d-tiles icon indicating copy to clipboard operation
3d-tiles copied to clipboard

Declarative styling

Open pjcozzi opened this issue 10 years ago • 94 comments

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 for Cesium3DTileStyleEngine)
  • [x] Load style from uri, not just JSON string.
  • [x] Pass styleEngine last 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 JulianDate datatype 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 Color so 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 interval to optimize conditional when used for intervals
    • [ ] Referencing other features in a style
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 Property system, e.g., evaluate expressions as Property or vice-versa.
  • [ ] 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 Cesium3DTileStyle and 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 replaceVariables in Expression.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.

pjcozzi avatar Jun 15 '15 13:06 pjcozzi

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: img_0350

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

pjcozzi avatar Feb 05 '16 13:02 pjcozzi

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);

pierotofy avatar Feb 05 '16 15:02 pierotofy

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!

pjcozzi avatar Feb 05 '16 15:02 pjcozzi

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 .

mramato avatar Feb 05 '16 15:02 mramato

Update https://github.com/AnalyticalGraphicsInc/3d-tiles/issues/2#issuecomment-180355858 to account for the latest implementation work and offline discussion with @mramato.

pjcozzi avatar Feb 09 '16 20:02 pjcozzi

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.

pjcozzi avatar Feb 10 '16 14:02 pjcozzi

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.

pjcozzi avatar Feb 13 '16 01:02 pjcozzi

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"
    • [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, String constructors
    • [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

pjcozzi avatar Feb 14 '16 16:02 pjcozzi

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

pierotofy avatar Feb 15 '16 18:02 pierotofy

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.

pjcozzi avatar Feb 15 '16 18:02 pjcozzi

@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.

pjcozzi avatar Feb 18 '16 19:02 pjcozzi

I added my comments in https://github.com/AnalyticalGraphicsInc/3d-tiles/pull/70

ggetz avatar Feb 18 '16 19:02 ggetz

@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.

pjcozzi avatar Feb 19 '16 12:02 pjcozzi

@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.

pjcozzi avatar Feb 19 '16 13:02 pjcozzi

@ggetz for strings, make sure we parse both ' and ".

pjcozzi avatar Feb 19 '16 13:02 pjcozzi

@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.

pjcozzi avatar Feb 19 '16 13:02 pjcozzi

@ggetz are we in-sync with this:

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.

pjcozzi avatar Feb 19 '16 14:02 pjcozzi

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.

pjcozzi avatar Feb 19 '16 14:02 pjcozzi

@ggetz do you want to design the regex support?

pjcozzi avatar Feb 19 '16 14:02 pjcozzi

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

ggetz avatar Feb 19 '16 14:02 ggetz

do you want to design the regex support?

Sure

ggetz avatar Feb 19 '16 14:02 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.

Looks good, I will put this in the implementation

ggetz avatar Feb 19 '16 14:02 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.

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 avatar Feb 19 '16 14:02 ggetz

@ggetz there's a new section on variables. Please make sure to test all the different datatypes.

pjcozzi avatar Feb 19 '16 14:02 pjcozzi

for strings, make sure we parse both ' and ".

I believe jsep handles this, I will add a test to confirm.

ggetz avatar Feb 19 '16 14:02 ggetz

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} ?

ggetz avatar Feb 19 '16 14:02 ggetz

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.

pjcozzi avatar Feb 19 '16 15:02 pjcozzi

@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.

pjcozzi avatar Feb 19 '16 21:02 pjcozzi

when CSS colors are out of range with the rgb(), hsl(), etc.. functions, they just clamp the value to the max and min.

ggetz avatar Feb 19 '16 22:02 ggetz