CesiumGS / 3d-tiles

Specification for streaming massive heterogeneous 3D geospatial datasets :earth_americas:
2.05k stars 460 forks source link

Declarative styling #2

Open pjcozzi opened 9 years ago

pjcozzi commented 9 years ago

Short-Term

Spec

@ggetz

@pjcozzi

@ggetz

@pjcozzi


Later

Spec

{
    "show" : true,
    "color" : [255, 255, 255],
    "meta" : {
        "description" : "Hello, ${FeatureName}"
    }
}
var str = style.meta.description.evaluate(feature);
color : ramp(palette, distance(${thisFeature.location}, ${anotherFeature.location}))
show : (${thisFeature.height} > ${anotherBuilding.height})

Cesium Implementation

Built-in functions

pjcozzi commented 8 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: img_0350

Schema Ideas

Related-ish

pierotofy commented 8 years ago

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);
pjcozzi commented 8 years ago

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!

mramato commented 8 years ago

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 .

pjcozzi commented 8 years ago

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

pjcozzi commented 8 years ago

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 commented 8 years ago

Notes on parsers

We'll start with jsep.

pjcozzi commented 8 years ago

Notes on expressions

Now

"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
pierotofy commented 8 years ago

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
pjcozzi commented 8 years ago

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 commented 8 years ago

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

ggetz commented 8 years ago

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

pjcozzi commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

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

pjcozzi commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

@ggetz do you want to design the regex support?

ggetz commented 8 years ago

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 commented 8 years ago

do you want to design the regex support?

Sure

ggetz commented 8 years ago

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 commented 8 years ago

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.

pjcozzi commented 8 years ago

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

ggetz commented 8 years ago

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

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

ggetz commented 8 years ago

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

pjcozzi commented 8 years ago

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 commented 8 years ago

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

mramato commented 8 years ago

Why does HTML think “chucknorris” is a color?

ggetz commented 8 years ago

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

pjcozzi commented 8 years ago

OK, please make sure we do the same.

Also, what about a string that isn't hex or is too long?

ggetz commented 8 years ago

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

pjcozzi commented 8 years ago

OK, our implementation should work the same.

ggetz commented 8 years ago

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?

mramato commented 8 years ago

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

pjcozzi commented 8 years ago

@mramato is right in general, but is @ggetz saying that Chrome implements colors in CSS differently than the spec?

ggetz commented 8 years ago

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)

pjcozzi commented 8 years ago

So it sounds like everything is in-sync?

ggetz commented 8 years ago

Yes.

pjcozzi commented 8 years ago

:+1:

pjcozzi commented 8 years ago

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

pjcozzi commented 8 years ago

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

ggetz commented 8 years ago

${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.

pjcozzi commented 8 years ago

OK with me, see how the implementation feels.

pjcozzi commented 8 years ago

@ggetz also please update the spec when you confirm this.

ggetz commented 8 years ago

also please update the spec when you confirm this.

OK

pjcozzi commented 8 years ago

@ggetz the spec currently says

Explicit Boolean, Number, and String 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.

ggetz commented 8 years ago

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?

pjcozzi commented 8 years ago

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.

ggetz commented 8 years ago

OK, I agree these should be added. I will add this to the roadmap.