-
Notifications
You must be signed in to change notification settings - Fork 2.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[for discussion] Draft proposal - arbitrary expressions for style functions #4715
Changes from 4 commits
d002cce
09f8a2b
9576516
f32b98d
5d14e1d
fc7948f
04167e1
7caa399
f52532f
c72ed74
439b908
f8ff159
1464a8b
a7fc39c
2b95529
7bec2c8
d9d722e
291582f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>Mapbox GL JS debug page</title> | ||
<meta charset='utf-8'> | ||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | ||
<link rel='stylesheet' href='/dist/mapbox-gl.css' /> | ||
<style> | ||
body { margin: 0; padding: 0; } | ||
html, body, #map { height: 100%; } | ||
</style> | ||
</head> | ||
|
||
<body> | ||
<div id='map'></div> | ||
|
||
<script src='/dist/mapbox-gl-dev.js'></script> | ||
<script src='/debug/access_token_generated.js'></script> | ||
<script> | ||
|
||
var style = { | ||
sprite: "mapbox://sprites/mapbox/streets-v10", | ||
glyphs: "mapbox://fonts/mapbox/{fontstack}/{range}.pbf", | ||
sources: { | ||
"geojson": { | ||
"type": "geojson", | ||
"data": { | ||
"type": "FeatureCollection", | ||
"features": [{"type":"Feature","properties":{"x":-10,"y":-10},"geometry":{"type":"Point","coordinates":[-10,-10]}},{"type":"Feature","properties":{"x":-10,"y":-6},"geometry":{"type":"Point","coordinates":[-10,-6]}},{"type":"Feature","properties":{"x":-10,"y":-2},"geometry":{"type":"Point","coordinates":[-10,-2]}},{"type":"Feature","properties":{"x":-10,"y":2},"geometry":{"type":"Point","coordinates":[-10,2]}},{"type":"Feature","properties":{"x":-10,"y":6},"geometry":{"type":"Point","coordinates":[-10,6]}},{"type":"Feature","properties":{"x":-10,"y":10},"geometry":{"type":"Point","coordinates":[-10,10]}},{"type":"Feature","properties":{"x":-6,"y":-10},"geometry":{"type":"Point","coordinates":[-6,-10]}},{"type":"Feature","properties":{"x":-6,"y":-6},"geometry":{"type":"Point","coordinates":[-6,-6]}},{"type":"Feature","properties":{"x":-6,"y":-2},"geometry":{"type":"Point","coordinates":[-6,-2]}},{"type":"Feature","properties":{"x":-6,"y":2},"geometry":{"type":"Point","coordinates":[-6,2]}},{"type":"Feature","properties":{"x":-6,"y":6},"geometry":{"type":"Point","coordinates":[-6,6]}},{"type":"Feature","properties":{"x":-6,"y":10},"geometry":{"type":"Point","coordinates":[-6,10]}},{"type":"Feature","properties":{"x":-2,"y":-10},"geometry":{"type":"Point","coordinates":[-2,-10]}},{"type":"Feature","properties":{"x":-2,"y":-6},"geometry":{"type":"Point","coordinates":[-2,-6]}},{"type":"Feature","properties":{"x":-2,"y":-2},"geometry":{"type":"Point","coordinates":[-2,-2]}},{"type":"Feature","properties":{"x":-2,"y":2},"geometry":{"type":"Point","coordinates":[-2,2]}},{"type":"Feature","properties":{"x":-2,"y":6},"geometry":{"type":"Point","coordinates":[-2,6]}},{"type":"Feature","properties":{"x":-2,"y":10},"geometry":{"type":"Point","coordinates":[-2,10]}},{"type":"Feature","properties":{"x":2,"y":-10},"geometry":{"type":"Point","coordinates":[2,-10]}},{"type":"Feature","properties":{"x":2,"y":-6},"geometry":{"type":"Point","coordinates":[2,-6]}},{"type":"Feature","properties":{"x":2,"y":-2},"geometry":{"type":"Point","coordinates":[2,-2]}},{"type":"Feature","properties":{"x":2,"y":2},"geometry":{"type":"Point","coordinates":[2,2]}},{"type":"Feature","properties":{"x":2,"y":6},"geometry":{"type":"Point","coordinates":[2,6]}},{"type":"Feature","properties":{"x":2,"y":10},"geometry":{"type":"Point","coordinates":[2,10]}},{"type":"Feature","properties":{"x":6,"y":-10},"geometry":{"type":"Point","coordinates":[6,-10]}},{"type":"Feature","properties":{"x":6,"y":-6},"geometry":{"type":"Point","coordinates":[6,-6]}},{"type":"Feature","properties":{"x":6,"y":-2},"geometry":{"type":"Point","coordinates":[6,-2]}},{"type":"Feature","properties":{"x":6,"y":2},"geometry":{"type":"Point","coordinates":[6,2]}},{"type":"Feature","properties":{"x":6,"y":6},"geometry":{"type":"Point","coordinates":[6,6]}},{"type":"Feature","properties":{"x":6,"y":10},"geometry":{"type":"Point","coordinates":[6,10]}},{"type":"Feature","properties":{"x":10,"y":-10},"geometry":{"type":"Point","coordinates":[10,-10]}},{"type":"Feature","properties":{"x":10,"y":-6},"geometry":{"type":"Point","coordinates":[10,-6]}},{"type":"Feature","properties":{"x":10,"y":-2},"geometry":{"type":"Point","coordinates":[10,-2]}},{"type":"Feature","properties":{"x":10,"y":2},"geometry":{"type":"Point","coordinates":[10,2]}},{"type":"Feature","properties":{"x":10,"y":6},"geometry":{"type":"Point","coordinates":[10,6]}},{"type":"Feature","properties":{"x":10,"y":10},"geometry":{"type":"Point","coordinates":[10,10]}}] | ||
} | ||
} | ||
}, | ||
layers: [ | ||
{ | ||
id: 'circle', | ||
type: 'circle', | ||
source: 'geojson', | ||
paint: { | ||
'circle-radius': { | ||
type: 'expression', | ||
expression: [ | ||
'*', | ||
[ '^', 2, {ref: 'map', key: 'zoom'} ], | ||
0.125, | ||
[ | ||
'+', | ||
20, | ||
{ref: 'feature', key: 'x'}, | ||
{ref: 'feature', key: 'y'} | ||
] | ||
] | ||
}, | ||
'circle-opacity': { | ||
type: 'expression', | ||
expression: [ '/', [ '+', 10, {ref: 'feature', key: 'x'} ], 30 ] | ||
}, | ||
'circle-color': { | ||
type: 'expression', | ||
expression: [ | ||
'concat', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Concatenating strings to form a color would be problematic for all the native SDKs, where colors are formed using objects rather than CSS color strings. For example, on iOS, a color-typed property must be set to something that evaluates to a UIColor object, not a string. Setting a constant color might look like this: circleLayer.circleColor = NSExpression(constantValue: UIColor.orange) If the JSON file format would instead treat circleLayer.circleColor = NSExpression(format: "mgl_colorWithRedGreenBlueAlpha({ 128 + 10 * feature.x, 128 + 10 * feature.y, 128, 1 })") perhaps with a more memorable convenience method, let redExpression = NSExpression(format: "128 + 10 * feature.x")
let greenExpression = NSExpression(format: "128 + 10 * feature.y")
let blueExpression = NSExpression(constantValue: 128)
circleLayer.circleColor = NSExpression(mglRed: redExpression, green: greenExpression, blue: blueExpression) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 yep, agreed that having color functions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The CSS 4 color-modifying function draft might serve as inspiration here: https://drafts.csswg.org/css-color/#modifying-colors There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Woah. That's pretty awesome, although including "color adjuster" functions would probably require taking on a dependency like https://github.com/gka/chroma.js/. For the scope of this PR, I'm inclined to stick to color constructors, and to ticket/discuss separately the possibility of adding more sophisticated color operations. |
||
'rgb(', | ||
[ '+', 128, [ '*', 10, {ref: 'feature', key: 'x'} ] ], | ||
',', | ||
[ '+', 128, [ '*', 10, {ref: 'feature', key: 'y'} ] ], | ||
',', | ||
128, | ||
')' | ||
] | ||
} | ||
} | ||
} | ||
], | ||
version: 8 | ||
} | ||
|
||
var map = window.map = new mapboxgl.Map({ | ||
container: 'map', | ||
zoom: 0, | ||
center: [0, 0], | ||
style: style, | ||
hash: true | ||
}); | ||
|
||
</script> | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# Style Function Expressions | ||
|
||
**Constants:** | ||
- `[ "ln2" ]` | ||
- `[ "pi" ]` | ||
- `[ "e" ]` | ||
|
||
**Literal:** | ||
- JSON string / number / boolean literal | ||
|
||
**Property lookup:** | ||
- Feature property: `[ "data", key_expr ]` | ||
- Map property: | ||
- `[ "zoom" ]` (Note: expressions that refer to the map zoom level are only evaluated at integer zoomslevels. When the map is at non-integer zoom levels, the expression's value will be approximated using linear or exponential interpolation.) | ||
- `[ "pitch" ]` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why would feature property lookups require a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
^ yeah, that's my thinking. there's no need for it to be |
||
- etc. | ||
|
||
**Decision:** | ||
- `["if", boolean_expr, expr_if_true, expr_if_false]` | ||
|
||
**Boolean:** | ||
- `[ "has", key_expr ]` | ||
- `[ "==", expr1, expr2]` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How would the style author specify that a comparison between two strings should be performed case- or diacritic-insensitively (#4136)? Or would we require There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This approach sounds good to me... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I’m not so sure that would be robust enough for internationalization purposes: #4136 (comment). Relatedly, should |
||
- `[ ">", lhs_expr, rhs_expr ]` (similar for <, >=, <=) | ||
- `[ "between", value_expr, lower_bound_expr, upper_bound_expr ]` | ||
- `[ "in", value_expr, item1_expr, item2_expr, ... ]` | ||
- `[ "all", boolean_expr1, boolean_expr2, ... ]` (similar for any, none) | ||
|
||
**String:** | ||
- `["concat", expr1, expr2, …]` | ||
- `["upcase", string_expr]`, `["downcase", string_expr]` | ||
|
||
**Numeric:** | ||
- +, -, \*, /, %, ^ (e.g. `["+", expr1, expr2, expr3, …]`, `["-", expr1, expr2 ]`, etc.) | ||
- log10, ln, log2 | ||
- sin, cos, tan, asin, acos, atan | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These trigonometric functions aren’t listed as being built into NSExpression, but we could define our own functions. |
||
- ceil, floor, round, abs | ||
- min, max | ||
|
||
**Color:** | ||
- rgb, hsl, hcl, lab, hex, (others?) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
'use strict'; | ||
|
||
const assert = require('assert'); | ||
const createFilter = require('../feature_filter'); | ||
|
||
module.exports = compileExpression; | ||
|
||
function compileExpression(expr) { | ||
const compiled = compile(expr); | ||
const fun = new Function('mapProperties', 'feature', ` | ||
var props = (feature && feature.properties || {}); | ||
return (${compiled.compiledExpression}) | ||
`); | ||
fun.isFeatureConstant = compiled.isFeatureConstant; | ||
fun.isZoomConsant = compiled.isZoomConstant; | ||
return fun; | ||
} | ||
|
||
function compile(expr) { | ||
if ( | ||
!expr || | ||
typeof expr === 'string' || | ||
typeof expr === 'number' || | ||
typeof expr === 'boolean' | ||
) return { | ||
compiledExpression: JSON.stringify(expr), | ||
isFeatureConstant: true, | ||
isZoomConstant: true | ||
}; | ||
|
||
if (expr.ref === 'feature') { | ||
return { | ||
compiledExpression: ` | ||
${JSON.stringify(expr.key)} in props ? | ||
props[${JSON.stringify(expr.key)}] : | ||
(${JSON.stringify(expr.defaultValue)} || 0) | ||
`, | ||
isFeatureConstant: false, | ||
isZoomConstant: true | ||
}; | ||
} | ||
|
||
if (expr.ref === 'map') { | ||
return { | ||
compiledExpression: `mapProperties[${JSON.stringify(expr.key)}]`, | ||
isFeatureConstant: true, | ||
isZoomConstant: expr.key !== 'zoom' | ||
}; | ||
} | ||
|
||
assert(Array.isArray(expr)); | ||
|
||
const op = expr[0]; | ||
|
||
// feature filter | ||
if ( | ||
op === '==' || | ||
op === '!=' || | ||
op === '<' || | ||
op === '>' || | ||
op === '<=' || | ||
op === '>=' || | ||
op === 'any' || | ||
op === 'all' || | ||
op === 'none' || | ||
op === 'in' || | ||
op === '!in' || | ||
op === 'has' || | ||
op === '!has' | ||
) { | ||
const featureFilter = createFilter(expr); | ||
return { | ||
compiledExpression: `(${featureFilter.toString()})(feature)`, | ||
isFeatureConstant: true, | ||
isZoomConstant: true | ||
}; | ||
} | ||
|
||
const subExpressions = expr.slice(1); | ||
const args = subExpressions.map(compile).map(s => `(${s.compiledExpression})`); | ||
|
||
let compiled; | ||
if (op === 'concat') { | ||
compiled = `[${args.join(',')}].join('')`; | ||
} else if (op === '+' || op === '*') { | ||
compiled = args.join(op); | ||
} else if (op === 'if') { | ||
compiled = `${args[0]} ? ${args[1]} : ${args[2]}`; | ||
} else if (op === '-' || op === '/' || op === '%') { | ||
compiled = `${args[0]} ${op} ${args[1]}`; | ||
} else if (op === '^') { | ||
compiled = `Math.pow(${args[0]}, ${args[1]})`; | ||
} | ||
|
||
return { | ||
compiledExpression: compiled, | ||
isFeatureConstant: subExpressions.reduce((memo, e) => memo && e.isFeatureConstant, true), | ||
isZoomConstnat: subExpressions.reduce((memo, e) => memo && e.isZoomConstant, true) | ||
}; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To illustrate the advantages of using NSExpression in the iOS/macOS SDKs, this paint property would be set using the following line of code (Objective-C then Swift):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's so nice! What do
map.zoom
,feature.x
, etc. end up looking like in the resulting NSExpression object?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The initialized NSExpression consists of the following object tree:
multiply:by:
, the operand_NSPredicateUtilities
, and the following NSExpressions as arguments:multiply:by:
, the operand_NSPredicateUtilities
, and the following NSExpressions as arguments:raise:toPower:
, the operand_NSPredicateUtilities
, and the following NSExpressions as arguments:map.zoom
(as a string)NSExpression comes with a built-in way to evaluate itself given a context dictionary. However, imperative filters aren’t on the roadmap – mapbox/mapbox-gl-native#7860 (comment) – so the iOS/macOS SDK instead translates the AST provided by NSExpression into the AST expected by mbgl.