Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions build/generate-style-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function unionType(values) {
}
}

function propertyType(property) {
function propertyType(property, key?) {
if (typeof property.type === 'function') {
return property.type();
}
Expand Down Expand Up @@ -93,15 +93,15 @@ function propertyType(property) {
} else if (supportsZoomExpression(property)) {
return `PropertyValueSpecification<${baseType}>`;
} else if (property.expression) {
return 'ExpressionSpecification';
return key === 'visibility' ? 'VisibilitySpecification' : 'ExpressionSpecification';
} else {
return baseType;
}
}

function propertyDeclaration(key, property) {
const jsDoc = jsDocComment(property);
const declaration = `"${key}"${property.required ? '' : '?'}: ${propertyType(property)}`;
const declaration = `"${key}"${property.required ? '' : '?'}: ${propertyType(property, key)}`;
return jsDoc ? [jsDoc, declaration].join('\n') : declaration;
}

Expand Down Expand Up @@ -348,6 +348,8 @@ export type LegacyFilterSpecification =

export type FilterSpecification = ExpressionFilterSpecification | LegacyFilterSpecification

export type VisibilitySpecification = 'visible' | 'none' | ExpressionSpecification;

export type TransitionSpecification = {
duration?: number,
delay?: number
Expand Down
71 changes: 71 additions & 0 deletions src/expression/visibility.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import createVisibilityExpression from './visibility';
import {describe, test, expect, vi} from 'vitest';

describe('evaluate visibility expression', () => {
test('literal values', () => {
vi.spyOn(console, 'warn').mockImplementation(() => {});

expect(createVisibilityExpression('none', {}).evaluate()).toBe('none');
expect(console.warn).not.toHaveBeenCalled();

expect(createVisibilityExpression('visible', {}).evaluate()).toBe('visible');
expect(console.warn).not.toHaveBeenCalled();
});

test('global state property as visibility expression', () => {
const globalState = {};
const value = createVisibilityExpression(['global-state', 'x'], globalState);

vi.spyOn(console, 'warn').mockImplementation(() => {});

globalState.x = 'none';
expect(value.evaluate()).toBe('none');
expect(console.warn).not.toHaveBeenCalled();

globalState.x = 'visible';
expect(value.evaluate()).toBe('visible');
expect(console.warn).not.toHaveBeenCalled();
});

test('global state flag as visibility expression', () => {
const globalState = {};
const value = createVisibilityExpression(['case', ['global-state', 'x'], 'visible', 'none'], globalState);

vi.spyOn(console, 'warn').mockImplementation(() => {});

globalState.x = false;
expect(value.evaluate()).toBe('none');
expect(console.warn).not.toHaveBeenCalled();

globalState.x = true;
expect(value.evaluate()).toBe('visible');
expect(console.warn).not.toHaveBeenCalled();
});

test('warns and falls back to default for invalid expression', () => {
const value = createVisibilityExpression(['get', 'x'], {});

vi.spyOn(console, 'warn').mockImplementation(() => {});

expect(value.evaluate()).toBe('visible');
expect(console.warn).toHaveBeenCalledWith('Expected value to be of type string, but found null instead.');
});

test('warns and falls back to default for missing global property', () => {
const value = createVisibilityExpression(['global-state', 'x'], {});

vi.spyOn(console, 'warn').mockImplementation(() => {});

expect(value.evaluate()).toBe('visible');
expect(console.warn).toHaveBeenCalledWith('Expected value to be of type string, but found null instead.');
});

test('warns and falls back to default for invalid global property', () => {
const value = createVisibilityExpression(['global-state', 'x'], {x: 'invalid'});

vi.spyOn(console, 'warn').mockImplementation(() => {});

expect(value.evaluate()).toBe('visible');
expect(console.warn).toHaveBeenCalledWith('Expected value to be one of "visible", "none", but found "invalid" instead.');
});
});
43 changes: 43 additions & 0 deletions src/expression/visibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {createExpression, findGlobalStateRefs} from '.';
import {type GlobalProperties, type StylePropertySpecification} from '..';
import {type VisibilitySpecification} from '../types.g';

const visibilitySpec: StylePropertySpecification = {
type: 'enum',
'property-type': 'data-constant',
expression: {
interpolated: false,
parameters: ['global-state']
},
values: {visible: {}, none: {}},
transition: false,
default: 'visible'
};

export default function createVisibility(visibility: VisibilitySpecification, globalState: Record<string, any>) {
const expression = {
setValue,
evaluate: null
};
setValue(visibility);
return expression;

function setValue(visibility: VisibilitySpecification) {
if (visibility === null || visibility === undefined || visibility === 'visible' || visibility === 'none') {
expression.evaluate = visibility === 'none' ? () => 'none' : () => 'visible';
addGlobalStateRefs(expression);
return;
}

const compiled = createExpression(visibility, visibilitySpec, globalState);
if (compiled.result === 'error') {
throw new Error(compiled.value.map(err => `${err.key}: ${err.message}`).join(', '));
}
expression.evaluate = () => compiled.value.evaluate({} as GlobalProperties);
addGlobalStateRefs(expression, () => findGlobalStateRefs(compiled.value.expression));
}
}

function addGlobalStateRefs(visibility, getGlobalStateRefs: () => Set<string> = () => new Set<string>()) {
visibility.getGlobalStateRefs = getGlobalStateRefs;
}
2 changes: 1 addition & 1 deletion src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ function validSchema(k, v, obj, ref, version, kind) {
expect('boolean').toBe(typeof expression.interpolated);
expect(true).toBe(Array.isArray(expression.parameters));
if (obj['property-type'] !== 'color-ramp') expect(true).toBe(
expression.parameters.every(k => k === 'zoom' || k === 'feature' || k === 'feature-state')
expression.parameters.every(k => k === 'zoom' || k === 'feature' || k === 'feature-state' || k === 'global-state')
);
}

Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ import {validate} from './validate/validate';
import {migrate} from './migrate';
import {classifyRings} from './util/classify_rings';
import {ProjectionDefinition} from './expression/types/projection_definition';
import createVisibilityExpression from './expression/visibility';

type ExpressionType = 'data-driven' | 'cross-faded' | 'cross-faded-data-driven' | 'color-ramp' | 'data-constant' | 'constant';
type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'elevation' | 'line-progress'>;
type ExpressionParameters = Array<'zoom' | 'feature' | 'feature-state' | 'heatmap-density' | 'elevation' | 'line-progress' | 'global-state'>;

type ExpressionSpecificationDefinition = {
interpolated: boolean;
Expand Down Expand Up @@ -203,6 +204,7 @@ export {
createExpression,
isFunction, createFunction,
createPropertyExpression,
createVisibilityExpression,
convertFilter,
featureFilter,
typeOf,
Expand Down
80 changes: 70 additions & 10 deletions src/reference/v8.json
Original file line number Diff line number Diff line change
Expand Up @@ -949,7 +949,13 @@
"ios": "2.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_fill": {
Expand Down Expand Up @@ -996,7 +1002,13 @@
"ios": "2.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_circle": {
Expand Down Expand Up @@ -1043,7 +1055,13 @@
"ios": "2.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_heatmap": {
Expand All @@ -1066,7 +1084,13 @@
"ios": "4.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_fill-extrusion": {
Expand All @@ -1089,7 +1113,13 @@
"ios": "3.6.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_line": {
Expand Down Expand Up @@ -1250,7 +1280,13 @@
"ios": "2.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_symbol": {
Expand Down Expand Up @@ -2705,7 +2741,13 @@
"ios": "2.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_raster": {
Expand All @@ -2728,7 +2770,13 @@
"ios": "2.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_hillshade": {
Expand All @@ -2751,7 +2799,13 @@
"ios": "4.0.0"
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"layout_color-relief": {
Expand All @@ -2771,7 +2825,13 @@
"basic functionality": {
}
},
"property-type": "constant"
"expression": {
"interpolated": false,
"parameters": [
"global-state"
]
},
"property-type": "data-constant"
}
},
"filter": {
Expand Down
18 changes: 18 additions & 0 deletions test/integration/style-spec/tests/functions.input.json
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,24 @@
"paint": {
"color-relief-color": { "stops": [[0, "red"], [1, "blue"]] }
}
},
{
"id": "valid expression in visibility",
"type": "fill",
"source": "source",
"source-layer": "layer",
"layout": {
"visibility": ["global-state", "vis-prop"]
}
},
{
"id": "invalid expression - feature property not allowed in visibility",
"type": "fill",
"source": "source",
"source-layer": "layer",
"layout": {
"visibility": ["get", "vis-prop"]
}
}
]
}
6 changes: 5 additions & 1 deletion test/integration/style-spec/tests/functions.output.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@
"line": 916
},
{
"message": "layers[50].layout.visibility: expected one of [visible, none], [\"literal\",true] found",
"message": "layers[50].layout.visibility: Expected string but found boolean instead.",
"line": 925
},
{
Expand Down Expand Up @@ -214,5 +214,9 @@
{
"message": "layers[57].paint.color-relief-color: zoom functions not supported",
"line": 994
},
{
"message": "layers[59].layout.visibility: data expressions not supported",
"line": 1012
}
]
Loading