Skip to content

Commit

Permalink
Split "curve" into "step" and "interpolate"
Browse files Browse the repository at this point in the history
  • Loading branch information
jfirebaugh committed Oct 27, 2017
1 parent e505df9 commit 2d3a54a
Show file tree
Hide file tree
Showing 39 changed files with 292 additions and 172 deletions.
2 changes: 1 addition & 1 deletion debug/circles.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"source": "circles",
"paint": {
"circle-radius": [
"curve",
"interpolate",
["exponential", 2.0],
["zoom"],
0, 5,
Expand Down
2 changes: 1 addition & 1 deletion debug/expressions.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"source": "circles",
"paint": {
"circle-radius": [
"curve",
"interpolate",
["exponential", 2.0],
["zoom"],
0, 5,
Expand Down
2 changes: 1 addition & 1 deletion debug/heatmap.html
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
},
"heatmap-intensity": 0.9,
"heatmap-color": [
"curve",
"interpolate",
["linear"],
["heatmap-density"],
0, "rgba(0, 0, 255, 0)",
Expand Down
8 changes: 4 additions & 4 deletions docs/components/expression-metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,20 +64,20 @@ const types = {
type: 'OutputType',
parameters: [{repeat: ['OutputType']}]
}],
curve: [{
step: [{
type: 'OutputType',
parameters: [
'input: number',
'["step"]',
'stop_output_0: OutputType',
'stop_input_1: number, stop_output_1: OutputType',
'stop_input_n: number, stop_output_n: OutputType, ...'
]
}, {
}],
interpolate: [{
type: 'OutputType (number, array<number>, or Color)',
parameters: [
'input: number',
'interpolation: ["linear"] | ["exponential", base] | ["cubic-bezier", x1, y1, x2, y2 ]',
'input: number',
'stop_input_1: number, stop_output_1: OutputType',
'stop_input_n: number, stop_output_n: OutputType, ...'
]
Expand Down
52 changes: 33 additions & 19 deletions docs/pages/style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -361,11 +361,7 @@ class Item extends React.Component {

{this.props.function === "interpolated" &&
<em className='quiet'>
Supports all <a href='#expressions-curve'><span className='icon smooth-ramp inline'/>curve</a> types. </em>}

{this.props.function === "piecewise-constant" &&
<em className='quiet'>
Supports <a href='#expressions-curve'><span className='icon step-ramp inline' /><code>step</code> curves</a> only. </em>}
Supports <a href='#expressions-interpolate'><span className='icon smooth-ramp inline'/><code>interpolate</code></a> expressions. </em>}

{this.props.transition &&
<em className='quiet'><span className='icon opacity inline quiet' />Transitionable. </em>}
Expand Down Expand Up @@ -1218,7 +1214,7 @@ export default class extends React.Component {
{highlightJSON(`
{
"circle-radius": [
"curve", ["linear"], ["zoom"],
"interpolate", ["linear"], ["zoom"],
// zoom is 5 (or less) -> circle radius will be 1px
5, 1,
// zoom is 10 (or greater) -> circle radius will be 2px
Expand All @@ -1227,17 +1223,34 @@ export default class extends React.Component {
}`)}
</div>

<p>This example uses a <a href="#expressions-curve"><code>'curve'</code></a> expression to
<p>This example uses an <a href="#expressions-interpolate"><code>interpolate</code></a> expression to
define a linear relationship between zoom level and circle size using a set of
input-output pairs. In this case, the expression indicates that the circle radius should
be 1 pixel when the zoom level is 5, and 2 pixels when the zoom is 10. (See <a
href="#expressions-curve"><code>the 'curve' documentation</code></a> for more
href="#expressions-interpolate">the <code>interpolate</code> documentation</a> for more
details.)</p>

<p>Note that any zoom expression used in a layout or paint property must be of the following form:</p>
<p>Note that any zoom expression used in a layout or paint property must be of the following forms:</p>

<div className='col12 space-bottom'>
{highlightJSON(`[ "curve", interpolation, ["zoom"], ... ]`)}
{highlightJSON(`[ "interpolate", interpolation, ["zoom"], ... ]`)}
</div>

<p>Or:</p>

<div className='col12 space-bottom'>
{highlightJSON(`[ "step", ["zoom"], ... ]`)}
</div>

<p>Or:</p>

<div className='col12 space-bottom'>
{highlightJSON(`
[
"let",
... variable bindings...,
[ "interpolate", interpolation, ["zoom"], ... ]
]`)}
</div>

<p>Or:</p>
Expand All @@ -1247,13 +1260,14 @@ export default class extends React.Component {
[
"let",
... variable bindings...,
[ "curve", interpolation, ["zoom"], ... ]
[ "step", ["zoom"], ... ]
]`)}
</div>

<p>That is, in layout or paint properties, <code>["zoom"]</code> may only appear as the
input to an outer <a href="#expressions-curve">curve</a>, and may not appear anywhere
else in the expression.</p>
<p>That is, in layout or paint properties, <code>["zoom"]</code> may appear only as the
input to an outer <a href="#expressions-interpolate"><code>interpolate</code></a> or
<a href="#expressions-step"><code>step</code></a> expression, or such an expression within a
<a href="#expressions-let"><code>let</code></a> expression.</p>

<h4>Example: a zoom-and-property expression</h4>
<p>Combining zoom and property expressions allows a layer's appearance to change with
Expand All @@ -1263,11 +1277,11 @@ export default class extends React.Component {
{highlightJSON(`
{
"circle-radius": [
"curve", ["linear"], ["zoom"],
// when zoom is 0, set each feature's circle radius to the value of its "rating" property
0, ["get", "rating"],
// when zoom is 0, set each feature's circle radius to four times the value of its "rating" property
10, ["*", 4, ["get", "rating"]]
"interpolate", ["linear"], ["zoom"],
// when zoom is 0, set each feature's circle radius to the value of its "rating" property
0, ["get", "rating"],
// when zoom is 0, set each feature's circle radius to four times the value of its "rating" property
10, ["*", 4, ["get", "rating"]]
]
}`)}
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/style-spec/expression/definitions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const Coercion = require('./coercion');
const At = require('./at');
const Match = require('./match');
const Case = require('./case');
const Curve = require('./curve');
const Step = require('./step');
const Interpolate = require('./interpolate');
const Coalesce = require('./coalesce');

import type { Expression } from '../expression';
Expand All @@ -46,7 +47,8 @@ const expressions: { [string]: Class<Expression> } = {
'case': Case,
'match': Match,
'coalesce': Coalesce,
'curve': Curve,
'step': Step,
'interpolate': Interpolate
};

function rgba(ctx, [r, g, b, a]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,20 @@ const UnitBezier = require('@mapbox/unitbezier');
const interpolate = require('../../util/interpolate');
const { toString, NumberType } = require('../types');
const { Color } = require('../values');
const { findStopLessThanOrEqualTo } = require("../stops");

import type { Stops } from '../stops';
import type { Expression } from '../expression';
import type ParsingContext from '../parsing_context';
import type EvaluationContext from '../evaluation_context';
import type { Type } from '../types';

export type InterpolationType =
{ name: 'step' } |
{ name: 'linear' } |
{ name: 'exponential', base: number } |
{ name: 'cubic-bezier', controlPoints: [number, number, number, number] };

type Stops = Array<[number, Expression]>;

class Curve implements Expression {
class Interpolate implements Expression {
key: string;
type: Type;

Expand Down Expand Up @@ -62,9 +61,7 @@ class Curve implements Expression {
return context.error(`Expected an interpolation type expression.`, 1);
}

if (interpolation[0] === 'step') {
interpolation = { name: 'step' };
} else if (interpolation[0] === 'linear') {
if (interpolation[0] === 'linear') {
interpolation = { name: 'linear' };
} else if (interpolation[0] === 'exponential') {
const base = interpolation[1];
Expand All @@ -91,15 +88,12 @@ class Curve implements Expression {
return context.error(`Unknown interpolation type ${String(interpolation[0])}`, 1, 0);
}

const isStep = interpolation.name === 'step';

const minArgs = isStep ? 5 : 4;
if (args.length - 1 < minArgs)
return context.error(`Expected at least ${minArgs} arguments, but found only ${args.length - 1}.`);
if (args.length - 1 < 4) {
return context.error(`Expected at least 4 arguments, but found only ${args.length - 1}.`);
}

const parity = minArgs % 2;
if ((args.length - 1) % 2 !== parity) {
return context.error(`Expected an ${parity === 0 ? 'even' : 'odd'} number of arguments.`);
if ((args.length - 1) % 2 !== 0) {
return context.error(`Expected an even number of arguments.`);
}

input = context.parse(input, 2, NumberType);
Expand All @@ -112,23 +106,19 @@ class Curve implements Expression {
outputType = context.expectedType;
}

if (isStep) {
rest.unshift(-Infinity);
}

for (let i = 0; i < rest.length; i += 2) {
const label = rest[i];
const value = rest[i + 1];

const labelKey = isStep ? i + 2 : i + 3;
const valueKey = isStep ? i + 3 : i + 4;
const labelKey = i + 3;
const valueKey = i + 4;

if (typeof label !== 'number') {
return context.error('Input/output pairs for "curve" expressions must be defined using literal numeric values (not computed expressions) for the input values.', labelKey);
return context.error('Input/output pairs for "interpolate" expressions must be defined using literal numeric values (not computed expressions) for the input values.', labelKey);
}

if (stops.length && stops[stops.length - 1][0] > label) {
return context.error('Input/output pairs for "curve" expressions must be arranged with input values in strictly ascending order.', labelKey);
return context.error('Input/output pairs for "interpolate" expressions must be arranged with input values in strictly ascending order.', labelKey);
}

const parsed = context.parse(value, valueKey, outputType);
Expand All @@ -137,19 +127,18 @@ class Curve implements Expression {
stops.push([label, parsed]);
}

if (interpolation.name !== 'step' &&
outputType.kind !== 'number' &&
if (outputType.kind !== 'number' &&
outputType.kind !== 'color' &&
!(
outputType.kind === 'array' &&
outputType.itemType.kind === 'number' &&
typeof outputType.N === 'number'
)
) {
return context.error(`Type ${toString(outputType)} is not interpolatable, and thus cannot be used as a ${interpolation.name} curve's output type.`);
return context.error(`Type ${toString(outputType)} is not interpolatable.`);
}

return new Curve(context.key, outputType, interpolation, input, stops);
return new Interpolate(context.key, outputType, interpolation, input, stops);
}

evaluate(ctx: EvaluationContext) {
Expand All @@ -171,13 +160,9 @@ class Curve implements Expression {
}

const index = findStopLessThanOrEqualTo(labels, value);
if (this.interpolation.name === 'step') {
return outputs[index].evaluate(ctx);
}

const lower = labels[index];
const upper = labels[index + 1];
const t = Curve.interpolationFactor(this.interpolation, value, lower, upper);
const t = Interpolate.interpolationFactor(this.interpolation, value, lower, upper);

const outputLower = outputs[index].evaluate(ctx);
const outputUpper = outputs[index + 1].evaluate(ctx);
Expand Down Expand Up @@ -246,31 +231,4 @@ function exponentialInterpolation(input, base, lowerValue, upperValue) {
}
}

module.exports = Curve;

/**
* Returns the index of the last stop <= input, or 0 if it doesn't exist.
* @private
*/
function findStopLessThanOrEqualTo(stops, input) {
const n = stops.length;
let lowerIndex = 0;
let upperIndex = n - 1;
let currentIndex = 0;
let currentValue, upperValue;

while (lowerIndex <= upperIndex) {
currentIndex = Math.floor((lowerIndex + upperIndex) / 2);
currentValue = stops[currentIndex];
upperValue = stops[currentIndex + 1];
if (input === currentValue || input > currentValue && input < upperValue) { // Search complete
return currentIndex;
} else if (currentValue < input) {
lowerIndex = currentIndex + 1;
} else if (currentValue > input) {
upperIndex = currentIndex - 1;
}
}

return Math.max(currentIndex - 1, 0);
}
module.exports = Interpolate;
Loading

0 comments on commit 2d3a54a

Please sign in to comment.