Skip to content

Commit e48ac7a

Browse files
authored
Hillshade new types (#1088)
* add hillshade-method * add illumination altitude and basic method * start of adding color-array and number-array types * add NumberArray and ColorArray types * lint * code coverage * fix docs * Switch number-array and color-array to camelCase to match other types. Provide an example of a field requiring backwards compatibility. * throw error if attempting to interpolate between arrays with mismatched lengths * lint * update sdk-support * update Changelog * unit test stylistic changes * improve variable names * add "additional methods" line to hillshade sdk-support table. * add numberArray and colorArray function unit tests * add unit tests * add unit tests * remove unit tests that fail typecheck * add values tests * add identity funciton unit tests * add parsing tests for numberArray and ColorArray * add padding parsing unit test * normalizePropertyExpression unit tests * Add normalizePropertyExpression tests for constants. Fix isFunction() * add unit tests for normalizePropertyExpression * change if/else to switch/case * improve test naming and organization * update hillshade documentation wording * fix docs wording * refactoring * improve(?) readability of coercion logic. * Provide more details for hillshade methods * add image showing examples of each hillshade method
1 parent 988f4dd commit e48ac7a

31 files changed

+1282
-58
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
### ✨ Features and improvements
44
- `glyphs` is now optional even if a symbol layer specifies `text-field`; if it is unset, `text-font` is interpreted as a fallback font list ([#1068](https://github.com/maplibre/maplibre-style-spec/pull/1068))
5+
- `hillshade` layer now supports multiple methods, and the `multidirectional` method supports array values for illumination properties ([#1088](https://github.com/maplibre/maplibre-style-spec/pull/1088))
56

67
### 🐞 Bug fixes
78
- Fix RuntimeError class, make it inherited from Error ([#983](https://github.com/maplibre/maplibre-style-spec/issues/983))

build/generate-docs.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@ function typeToMarkdownLink(type: string): string {
175175
case 'formatted':
176176
case 'resolvedimage':
177177
case 'padding':
178+
case 'numberarray':
179+
case 'colorarray':
178180
return ` [${type}](types.md#${type.toLocaleLowerCase()})`;
179181
case 'filter':
180182
return ` [${type}](expressions.md)`;
@@ -214,7 +216,11 @@ function convertPropertyToMarkdown(key: string, value: JsonObject, keyPrefix = '
214216
}
215217

216218
if (value.minimum !== undefined || value.maximum !== undefined) {
217-
markdown += ` in range ${formatRange(value.minimum, value.maximum)}`;
219+
if (value.type === 'numberArray') {
220+
markdown += ` with value(s) in range ${formatRange(value.minimum, value.maximum)}`;
221+
} else {
222+
markdown += ` in range ${formatRange(value.minimum, value.maximum)}`;
223+
}
218224
}
219225

220226
markdown += '. ';

build/generate-style-spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ function propertyType(property) {
3939
return '{[_: string]: SourceSpecification}';
4040
case 'projection:':
4141
return 'ProjectionSpecification';
42+
case 'numberArray':
43+
return 'NumberArraySpecification';
44+
case 'colorArray':
45+
return 'ColorArraySpecification';
4246
case '*':
4347
return 'unknown';
4448
default:
@@ -127,6 +131,8 @@ export type ProjectionDefinitionT = [string, string, number];
127131
export type ProjectionDefinitionSpecification = string | ProjectionDefinitionT | PropertyValueSpecification<ProjectionDefinitionT>
128132
129133
export type PaddingSpecification = number | number[];
134+
export type NumberArraySpecification = number | number[];
135+
export type ColorArraySpecification = string | string[];
130136
131137
export type VariableAnchorOffsetCollectionSpecification = Array<string | [number, number]>;
132138

docs/assets/hillshade_methods.png

563 KB
Loading

docs/types.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,26 @@ There are also additional presets that yield commonly used expressions:
206206
| Preset | Full value | Description |
207207
|--------|------------|-------------|
208208
| `globe` | `["interpolate", ["linear"], ["zoom"],`<br>`10, "vertical-perspective", 12, "mercator"]` | Adaptive globe: interpolates from vertical-perspective to mercator projection between zoom levels 10 and 12. |
209+
210+
211+
## `numberArray`
212+
213+
A single number value, or an array of number values.
214+
215+
```json
216+
{
217+
"hillshade-illumination-direction": 24,
218+
"hillshade-illumination-direction": [45, 57.3]
219+
}
220+
```
221+
222+
## `colorArray`
223+
224+
A single color value, or an array of color values.
225+
226+
```json
227+
{
228+
"hillshade-highlight-color": "#ffff00",
229+
"hillshade-highlight-color": ["#ffff00", "rgb(255, 255, 0)", "yellow"]
230+
}
231+
```

src/expression/definitions/coercion.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {Formatted} from '../types/formatted';
55
import {ResolvedImage} from '../types/resolved_image';
66
import {Color} from '../types/color';
77
import {Padding} from '../types/padding';
8+
import {NumberArray} from '../types/number_array';
9+
import {ColorArray} from '../types/color_array';
810
import {VariableAnchorOffsetCollection} from '../types/variable_anchor_offset_collection';
911

1012
import type {Expression} from '../expression';
@@ -97,6 +99,30 @@ export class Coercion implements Expression {
9799
}
98100
throw new RuntimeError(`Could not parse padding from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
99101
}
102+
case 'numberArray': {
103+
let input;
104+
for (const arg of this.args) {
105+
input = arg.evaluate(ctx);
106+
107+
const val = NumberArray.parse(input);
108+
if (val) {
109+
return val;
110+
}
111+
}
112+
throw new RuntimeError(`Could not parse numberArray from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
113+
}
114+
case 'colorArray': {
115+
let input;
116+
for (const arg of this.args) {
117+
input = arg.evaluate(ctx);
118+
119+
const val = ColorArray.parse(input);
120+
if (val) {
121+
return val;
122+
}
123+
}
124+
throw new RuntimeError(`Could not parse colorArray from value '${typeof input === 'string' ? input : JSON.stringify(input)}'`);
125+
}
100126
case 'variableAnchorOffsetCollection': {
101127
let input;
102128
for (const arg of this.args) {

src/expression/definitions/interpolate.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import UnitBezier from '@mapbox/unitbezier';
22

3-
import {array, ArrayType, ColorType, ColorTypeT, NumberType, NumberTypeT, PaddingType, PaddingTypeT, VariableAnchorOffsetCollectionType, VariableAnchorOffsetCollectionTypeT, typeToString, verifyType, ProjectionDefinitionType} from '../types';
3+
import {array, ArrayType, ColorType, ColorTypeT, NumberType, NumberTypeT, PaddingType, PaddingTypeT, NumberArrayTypeT, ColorArrayTypeT, VariableAnchorOffsetCollectionType, VariableAnchorOffsetCollectionTypeT, typeToString, verifyType, ProjectionDefinitionType, ColorArrayType, NumberArrayType} from '../types';
44
import {findStopLessThanOrEqualTo} from '../stops';
55
import {Color} from '../types/color';
66
import {interpolateArray, interpolateNumber} from '../../util/interpolate-primitives';
77
import {Padding} from '../types/padding';
8+
import {ColorArray} from '../types/color_array';
9+
import {NumberArray} from '../types/number_array';
810
import {VariableAnchorOffsetCollection} from '../types/variable_anchor_offset_collection';
911
import {ProjectionDefinition} from '../types/projection_definition';
1012

@@ -23,7 +25,7 @@ export type InterpolationType = {
2325
name: 'cubic-bezier';
2426
controlPoints: [number, number, number, number];
2527
};
26-
type InterpolatedValueType = NumberTypeT | ColorTypeT | ProjectionDefinitionTypeT | PaddingTypeT | VariableAnchorOffsetCollectionTypeT | ArrayType<NumberTypeT>;
28+
type InterpolatedValueType = NumberTypeT | ColorTypeT | ProjectionDefinitionTypeT | PaddingTypeT | NumberArrayTypeT | ColorArrayTypeT | VariableAnchorOffsetCollectionTypeT | ArrayType<NumberTypeT>;
2729
export class Interpolate implements Expression {
2830
type: InterpolatedValueType;
2931

@@ -109,7 +111,7 @@ export class Interpolate implements Expression {
109111
const stops: Stops = [];
110112

111113
let outputType: Type = null;
112-
if (operator === 'interpolate-hcl' || operator === 'interpolate-lab') {
114+
if ((operator === 'interpolate-hcl' || operator === 'interpolate-lab') && context.expectedType != ColorArrayType) {
113115
outputType = ColorType;
114116
} else if (context.expectedType && context.expectedType.kind !== 'value') {
115117
outputType = context.expectedType;
@@ -139,6 +141,8 @@ export class Interpolate implements Expression {
139141
!verifyType(outputType, ProjectionDefinitionType) &&
140142
!verifyType(outputType, ColorType) &&
141143
!verifyType(outputType, PaddingType) &&
144+
!verifyType(outputType, NumberArrayType) &&
145+
!verifyType(outputType, ColorArrayType) &&
142146
!verifyType(outputType, VariableAnchorOffsetCollectionType) &&
143147
!verifyType(outputType, array(NumberType))
144148
) {
@@ -183,6 +187,10 @@ export class Interpolate implements Expression {
183187
return Color.interpolate(outputLower, outputUpper, t);
184188
case 'padding':
185189
return Padding.interpolate(outputLower, outputUpper, t);
190+
case 'colorArray':
191+
return ColorArray.interpolate(outputLower, outputUpper, t);
192+
case 'numberArray':
193+
return NumberArray.interpolate(outputLower, outputUpper, t);
186194
case 'variableAnchorOffsetCollection':
187195
return VariableAnchorOffsetCollection.interpolate(outputLower, outputUpper, t);
188196
case 'array':
@@ -191,9 +199,19 @@ export class Interpolate implements Expression {
191199
return ProjectionDefinition.interpolate(outputLower, outputUpper, t);
192200
}
193201
case 'interpolate-hcl':
194-
return Color.interpolate(outputLower, outputUpper, t, 'hcl');
202+
switch (this.type.kind) {
203+
case 'color':
204+
return Color.interpolate(outputLower, outputUpper, t, 'hcl');
205+
case 'colorArray':
206+
return ColorArray.interpolate(outputLower, outputUpper, t, 'hcl');
207+
}
195208
case 'interpolate-lab':
196-
return Color.interpolate(outputLower, outputUpper, t, 'lab');
209+
switch (this.type.kind) {
210+
case 'color':
211+
return Color.interpolate(outputLower, outputUpper, t, 'lab');
212+
case 'colorArray':
213+
return ColorArray.interpolate(outputLower, outputUpper, t, 'lab');
214+
}
197215
}
198216
}
199217

@@ -261,6 +279,8 @@ export const interpolateFactory = {
261279
color: Color.interpolate,
262280
number: interpolateNumber,
263281
padding: Padding.interpolate,
282+
numberArray: NumberArray.interpolate,
283+
colorArray: ColorArray.interpolate,
264284
variableAnchorOffsetCollection: VariableAnchorOffsetCollection.interpolate,
265285
array: interpolateArray
266286
}

src/expression/expression.test.ts

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {createPropertyExpression, Feature, GlobalProperties, StylePropertyExpression} from '../expression';
22
import {expressions} from './definitions';
33
import v8 from '../reference/v8.json' with {type: 'json'};
4-
import {createExpression, ICanonicalTileID, StyleExpression, StylePropertySpecification} from '..';
4+
import {Color, createExpression, ICanonicalTileID, StyleExpression, StylePropertySpecification} from '..';
55
import {ExpressionParsingError} from './parsing_error';
66
import {getGeometry} from '../../test/lib/geometry';
77
import {VariableAnchorOffsetCollection} from './types/variable_anchor_offset_collection';
8+
import {expectToMatchColor} from '../../test/lib/util';
89

910
// filter out internal "error" and "filter-*" expressions from definition list
1011
const filterExpressionRegex = /filter-/;
@@ -668,4 +669,90 @@ describe('projection expression', () => {
668669
}
669670
})
670671

672+
test('interpolate numberArray', () => {
673+
const response = createExpression(['interpolate', ['linear'], ['zoom'], 8, ['literal', [2,3]], 10, ['literal', [4,5]]], {
674+
type: 'numberArray',
675+
'property-type': 'data-constant',
676+
expression: {
677+
interpolated: true,
678+
parameters: ['zoom']
679+
},
680+
transition: false
681+
});
682+
683+
if (response.result === 'success') {
684+
expect(response.value.evaluate({zoom: 8}).values).toEqual([2,3]);
685+
expect(response.value.evaluate({zoom: 9}).values).toEqual([3,4]);
686+
expect(response.value.evaluate({zoom: 10}).values).toEqual([4,5]);
687+
} else {
688+
throw new Error('Failed to parse Interpolate expression');
689+
}
690+
})
691+
692+
test('interpolate ColorArray', () => {
693+
const response = createExpression(['interpolate', ['linear'], ['zoom'], 8, ['literal', ['white','black']], 10, ['literal', ['black','white']]], {
694+
type: 'colorArray',
695+
'property-type': 'data-constant',
696+
expression: {
697+
interpolated: true,
698+
parameters: ['zoom']
699+
},
700+
transition: false
701+
});
702+
703+
if (response.result === 'success') {
704+
expect(response.value.evaluate({zoom: 8}).values).toEqual([Color.parse('white'), Color.parse('black')]);
705+
expect(response.value.evaluate({zoom: 9}).values).toEqual([new Color(0.5, 0.5, 0.5), new Color(0.5, 0.5, 0.5)]);
706+
expect(response.value.evaluate({zoom: 10}).values).toEqual([Color.parse('black'), Color.parse('white')]);
707+
} else {
708+
throw new Error('Failed to parse Interpolate expression');
709+
}
710+
})
711+
712+
test('interpolate-hcl ColorArray', () => {
713+
const response = createExpression(['interpolate-hcl', ['linear'], ['zoom'], 8, ['literal', ['white','black']], 10, ['literal', ['black','white']]], {
714+
type: 'colorArray',
715+
'property-type': 'data-constant',
716+
expression: {
717+
interpolated: true,
718+
parameters: ['zoom']
719+
},
720+
transition: false
721+
});
722+
723+
if (response.result === 'success') {
724+
expect(response.value.evaluate({zoom: 8}).values).toEqual([Color.parse('white'), Color.parse('black')]);
725+
expect(response.value.evaluate({zoom: 9}).values).toHaveLength(2);
726+
for (let i = 0; i < 2; i++) {
727+
expectToMatchColor(response.value.evaluate({zoom: 9}).values[i], 'rgb(46.63266% 46.63266% 46.63266% / 1)');
728+
}
729+
expect(response.value.evaluate({zoom: 10}).values).toEqual([Color.parse('black'), Color.parse('white')]);
730+
} else {
731+
throw new Error('Failed to parse Interpolate expression');
732+
}
733+
})
734+
735+
test('interpolate-lab ColorArray', () => {
736+
const response = createExpression(['interpolate-lab', ['linear'], ['zoom'], 8, ['literal', ['white','black']], 10, ['literal', ['black','white']]], {
737+
type: 'colorArray',
738+
'property-type': 'data-constant',
739+
expression: {
740+
interpolated: true,
741+
parameters: ['zoom']
742+
},
743+
transition: false
744+
});
745+
746+
if (response.result === 'success') {
747+
expect(response.value.evaluate({zoom: 8}).values).toEqual([Color.parse('white'), Color.parse('black')]);
748+
expect(response.value.evaluate({zoom: 9}).values).toHaveLength(2);
749+
for (let i = 0; i < 2; i++) {
750+
expectToMatchColor(response.value.evaluate({zoom: 9}).values[i], 'rgb(46.63266% 46.63266% 46.63266% / 1)');
751+
}
752+
expect(response.value.evaluate({zoom: 10}).values).toEqual([Color.parse('black'), Color.parse('white')]);
753+
} else {
754+
throw new Error('Failed to parse Interpolate expression');
755+
}
756+
})
757+
671758
});

0 commit comments

Comments
 (0)