diff --git a/src/style/properties.js b/src/style/properties.js index 86569e3b52e..a1ce3bab347 100644 --- a/src/style/properties.js +++ b/src/style/properties.js @@ -21,23 +21,72 @@ import type { type TimePoint = number; -type TransitionParameters = { - now: TimePoint, - transition: TransitionSpecification -}; - export type EvaluationParameters = GlobalProperties & { now?: TimePoint, defaultFadeDuration?: number, zoomHistory?: ZoomHistory }; +/** + * Implements a number of classes that define state and behavior for paint and layout properties, most + * importantly their respective evaluation chains: + * + * Transitionable paint property value + * → Transitioning paint property value + * → Possibly evaluated paint property value + * → Fully evaluated paint property value + * + * Layout property value + * → Possibly evaluated layout property value + * → Fully evaluated layout property value + * + * @module + * @private + */ + +/** + * Implementations of the `Property` interface: + * + * * Hold metadata about a property that's independent of any specific value: stuff like the type of the value, + * the default value, etc. This comes from the style specification JSON. + * * Define behavior that needs to be polymorphic across different properties: "possibly evaluating" + * an input value (see below), and interpolating between two possibly-evaluted values. + * + * The type `T` is the fully-evaluated value type (e.g. `number`, `string`, `Color`). + * The type `R` is the intermediate "possibly evaluated" value type. See below. + * + * There are two main implementations of the interface -- one for properties that allow data-driven values, + * and one for properties that don't. There are a few "special case" implementations as well: one for properties + * which cross-fade between two values rather than interpolating, one for `heatmap-color`, and one for + * `light-position`. + * + * @private + */ export interface Property { specification: StylePropertySpecification; possiblyEvaluate(value: PropertyValue, parameters: EvaluationParameters): R; interpolate(a: R, b: R, t: number): R; } +/** + * `PropertyValue` represents the value part of a property key-value unit. It's used to represent both + * paint and layout property values, and regardless of whether or not their property supports data-driven + * expressions. + * + * `PropertyValue` stores the raw input value as seen in a style or a runtime styling API call, i.e. one of the + * following: + * + * * A constant value of the type appropriate for the property + * * A function which produces a value of that type (but functions are quasi-deprecated in favor of expressions) + * * An expression which produces a value of that type + * * "undefined"/"not present", in which case the property is assumed to take on its default value. + * + * In addition to storing the original input value, `PropertyValue` also stores a normalized representation, + * effectively treating functions as if they are expressions, and constant or default values as if they are + * (constant) expressions. + * + * @private + */ class PropertyValue { property: Property; value: PropertyValueSpecification | void; @@ -60,6 +109,23 @@ class PropertyValue { // ------- Transitionable ------- +type TransitionParameters = { + now: TimePoint, + transition: TransitionSpecification +}; + +/** + * Paint properties are _transitionable_: they can change in a fluid manner, interpolating or cross-fading between + * old and new value. The duration of the transition, and the delay before it begins, is configurable. + * + * `TransitionablePropertyValue` is a compositional class that stores both the property value and that transition + * configuration. + * + * A `TransitionablePropertyValue` can calculate the next step in the evaluation chain for paint property values: + * `TransitioningPropertyValue`. + * + * @private + */ class TransitionablePropertyValue { property: Property; value: PropertyValue; @@ -72,7 +138,8 @@ class TransitionablePropertyValue { transitioned(parameters: TransitionParameters, prior: TransitioningPropertyValue): TransitioningPropertyValue { - return new TransitioningPropertyValue(this.property, this.value, prior, extend({}, this.transition, parameters.transition), parameters.now); // eslint-disable-line no-use-before-define + return new TransitioningPropertyValue(this.property, this.value, prior, // eslint-disable-line no-use-before-define + extend({}, this.transition, parameters.transition), parameters.now); } untransitioned(): TransitioningPropertyValue { @@ -80,9 +147,22 @@ class TransitionablePropertyValue { } } +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys and values of type `TransitionablePropertyValue`. + * + * @private + */ type TransitionablePropertyValues = $Exact<$ObjMap(p: Property) => TransitionablePropertyValue>> +/** + * `Transitionable` stores a map of all (property name, `TransitionablePropertyValue`) pairs for paint properties of a + * given layer type. It can calculate the `TransitioningPropertyValue`s for all of them at once, producing a + * `Transitioning` instance for the same set of properties. + * + * @private + */ class Transitionable { _values: TransitionablePropertyValues; @@ -93,11 +173,11 @@ class Transitionable { } } - getValue(name: S) { + getValue(name: S): PropertyValueSpecification | void { return this._values[name].value.value; } - setValue(name: S, value: *) { + setValue(name: S, value: PropertyValueSpecification | void) { this._values[name].value = new PropertyValue(this._values[name].property, value === null ? undefined : value); } @@ -144,6 +224,15 @@ class Transitionable { // ------- Transitioning ------- +/** + * `TransitioningPropertyValue` implements the first of two intermediate steps in the evaluation chain of a paint + * property value. In this step, transitions between old and new values are handled: as long as the transition is in + * progress, `TransitioningPropertyValue` maintains a reference to the prior value, and interpolates between it and + * the new value based on the current time and the configured transition duration and delay. The product is the next + * step in the evaluation chain: the "possibly evaluated" result type `R`. See below for more on this concept. + * + * @private + */ class TransitioningPropertyValue { property: Property; value: PropertyValue; @@ -193,9 +282,22 @@ class TransitioningPropertyValue { } } +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys and values of type `TransitioningPropertyValue`. + * + * @private + */ type TransitioningPropertyValues = $Exact<$ObjMap(p: Property) => TransitioningPropertyValue>> +/** + * `Transitioning` stores a map of all (property name, `TransitioningPropertyValue`) pairs for paint properties of a + * given layer type. It can calculate the possibly-evaluated values for all of them at once, producing a + * `PossiblyEvaluated` instance for the same set of properties. + * + * @private + */ class Transitioning { _values: TransitioningPropertyValues; @@ -223,9 +325,26 @@ class Transitioning { // ------- Layout ------- +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys and values of type `PropertyValue`. + * + * @private + */ type LayoutPropertyValues = $Exact<$ObjMap(p: Property) => PropertyValue>> +/** + * Because layout properties are not transitionable, they have a simpler representation and evaluation chain than + * paint properties: `PropertyValue`s are possibly evaluated, producing possibly evaluated values, which are then + * fully evaluated. + * + * `Layout` stores a map of all (property name, `PropertyValue`) pairs for layout properties of a + * given layer type. It can calculate the possibly-evaluated values for all of them at once, producing a + * `PossiblyEvaluated` instance for the same set of properties. + * + * @private + */ class Layout { _values: LayoutPropertyValues; @@ -266,11 +385,40 @@ class Layout { // ------- PossiblyEvaluated ------- +/** + * "Possibly evaluated value" is an intermediate stage in the evaluation chain for both paint and layout property + * values. The purpose of this stage is to optimize away unnecessary recalculations for data-driven properties. Code + * which uses data-driven property values must assume that the value is dependent on feature data, and request that it + * be evaluated for each feature. But when that property value is in fact a constant or camera function, the calculation + * will not actually depend on the feature, and we can benefit from returning the prior result of having done the + * evaluation once, ahead of time, in an intermediate step whose inputs are just the value and "global" parameters + * such as current zoom level. + * + * `PossiblyEvaluatedValue` represents the three possible outcomes of this step: if the input value was a constant or + * camera expression, then the "possibly evaluated" result is a constant value. Otherwise, the input value was either + * a source expression or a camera expression, and we must defer final evaluation until supplied a feature. We separate + * the source and composite cases because they are handled differently when generating GL attributes, buffers, and + * uniforms. + * + * Note that `PossiblyEvaluatedValue` (and `PossiblyEvaluatedPropertyValue`, below) are _not_ used for properties that + * do not allow data-driven values. For such properties, we know that the "possibly evaluated" result is always a constant + * scalar value. See below. + * + * @private + */ export type PossiblyEvaluatedValue = | {kind: 'constant', value: T} | SourceExpression | CompositeExpression; +/** + * `PossiblyEvaluatedPropertyValue` is used for data-driven paint and layout property values. It holds a + * `PossiblyEvaluatedValue` and the `GlobalProperties` that were used to generate it. You're not allowed to supply + * a different set of `GlobalProperties` when performing the final evaluation because they would be ignored in the + * case where the input value was a constant or camera function. + * + * @private + */ class PossiblyEvaluatedPropertyValue { property: DataDrivenProperty; value: PossiblyEvaluatedValue; @@ -299,9 +447,30 @@ class PossiblyEvaluatedPropertyValue { } } +/** + * A helper type: given an object type `Properties` whose values are each of type `Property`, it calculates + * an object type with the same keys, and values of type `R`. + * + * For properties that don't allow data-driven values, `R` is a scalar type such as `number`, `string`, or `Color`. + * For data-driven properties, it is `PossiblyEvaluatedPropertyValue`. Critically, the type definitions are set up + * in a way that allows flow to know which of these two cases applies for any given property name, and if you attempt + * to use a `PossiblyEvaluatedPropertyValue` as if it was a scalar, or vice versa, you will get a type error. (However, + * there's at least one case in which flow fails to produce a type error that you should be aware of: in a context such + * as `layer.paint.get('foo-opacity') === 0`, if `foo-opacity` is data-driven, than the left-hand side is of type + * `PossiblyEvaluatedPropertyValue`, but flow will not complain about comparing this to a number using `===`. + * See https://github.com/facebook/flow/issues/2359.) + * + * There's also a third, special case possiblity for `R`: for cross-faded properties, it's `?CrossFaded`. + * + * @private + */ type PossiblyEvaluatedPropertyValues = $Exact<$ObjMap(p: Property) => R>> +/** + * `PossiblyEvaluated` stores a map of all (property name, `R`) pairs for paint or layout properties of a + * given layer type. + */ class PossiblyEvaluated { _values: PossiblyEvaluatedPropertyValues; @@ -314,6 +483,13 @@ class PossiblyEvaluated { } } +/** + * An implementation of `Property` for properties that do not permit data-driven (source or composite) expressions. + * This restriction allows us to declare statically that the result of possibly evaluating this kind of property + * is in fact always the scalar type `T`, and can be used without further evaluating the value on a per-feature basis. + * + * @private + */ class DataConstantProperty implements Property { specification: StylePropertySpecification; @@ -336,6 +512,13 @@ class DataConstantProperty implements Property { } } +/** + * An implementation of `Property` for properties that permit data-driven (source or composite) expressions. + * The result of possibly evaluating this kind of property is `PossiblyEvaluatedPropertyValue`; obtaining + * a scalar value `T` requires further evaluation on a per-feature basis. + * + * @private + */ class DataDrivenProperty implements Property> { specification: StylePropertySpecification; useIntegerZoom: boolean; @@ -359,6 +542,7 @@ class DataDrivenProperty implements Property, b: PossiblyEvaluatedPropertyValue, t: number): PossiblyEvaluatedPropertyValue { + // If either possibly-evaluated value is non-constant, give up: we aren't able to interpolate data-driven values. if (a.value.kind !== 'constant' || b.value.kind !== 'constant') { return a; } @@ -387,6 +571,12 @@ class DataDrivenProperty implements Property implements Property> { specification: StylePropertySpecification; @@ -425,6 +615,11 @@ class CrossFadedProperty implements Property> { } } +/** + * An implementation of `Property` for `heatmap-color`, which has unique evaluation requirements. + * + * @private + */ class HeatmapColorProperty implements Property { specification: StylePropertySpecification;