diff --git a/CHANGELOG.md b/CHANGELOG.md index 4365ecc49..932a79df5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * Document `raster-fade-duration` property's effect on rendering a video. [#297](https://github.com/maplibre/maplibre-style-spec/pull/297) * Add and expose `isZoomExpression`. [#267](https://github.com/maplibre/maplibre-style-spec/issues/267) +* Add raster dem source `redFactor`, `greenFactor`, `blueFactor`, `baseShift` properties [#326](https://github.com/maplibre/maplibre-style-spec/issues/326) ## 19.3.0 diff --git a/src/reference/v8.json b/src/reference/v8.json index e74ff21ba..66b7c8c4f 100644 --- a/src/reference/v8.json +++ b/src/reference/v8.json @@ -352,11 +352,34 @@ }, "mapbox": { "doc": "Mapbox Terrain RGB tiles. See https://www.mapbox.com/help/access-elevation-data/#mapbox-terrain-rgb for more info." + }, + "custom": { + "doc": "Decodes tiles using the redFactor, blueFactor, greenFactor, baseShift parameters." } }, "default": "mapbox", - "doc": "The encoding used by this source. Mapbox Terrain RGB is used by default" + "doc": "The encoding used by this source. Mapbox Terrain RGB is used by default." + }, + "redFactor": { + "type": "number", + "default": 1.0, + "doc": "Value that will be multiplied by the red channel value when decoding. Only used on custom encodings." + }, + "blueFactor": { + "type": "number", + "default": 1.0, + "doc": "Value that will be multiplied by the blue channel value when decoding. Only used on custom encodings." }, + "greenFactor": { + "type": "number", + "default": 1.0, + "doc": "Value that will be multiplied by the green channel value when decoding. Only used on custom encodings." + }, + "baseShift": { + "type": "number", + "default": 0.0, + "doc": "Value that will be added to the encoding mix when decoding. Only used on custom encodings." + }, "volatile": { "type": "boolean", "default": false, @@ -598,7 +621,7 @@ } }, "hillshade": { - "doc": "Client-side hillshading visualization based on DEM data. Currently, the implementation only supports Mapbox Terrain RGB and Mapzen Terrarium tiles.", + "doc": "Client-side hillshading visualization based on DEM data. The implementation supports Mapbox Terrain RGB, Mapzen Terrarium tiles and custom encodings.", "sdk-support": { "basic functionality": { "js": "0.43.0", diff --git a/src/validate/validate_raster_dem_source.test.ts b/src/validate/validate_raster_dem_source.test.ts new file mode 100644 index 000000000..1f86191b8 --- /dev/null +++ b/src/validate/validate_raster_dem_source.test.ts @@ -0,0 +1,67 @@ +import validateSpec from './validate'; +import v8 from '../reference/v8.json' assert {type: 'json'}; +import validateRasterDEMSource from './validate_raster_dem_source'; +import {RasterDEMSourceSpecification} from '../types.g'; + +function checkErrorMessage(message: string, key: string, expectedType: string, foundType: string) { + expect(message).toContain(key); + expect(message).toContain(expectedType); + expect(message).toContain(foundType); +} + +describe('Validate source_raster_dem', () => { + test('Should pass when value is undefined', () => { + const errors = validateRasterDEMSource({validateSpec, value: undefined, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(0); + }); + + test('Should return error when value is not an object', () => { + const errors = validateRasterDEMSource({validateSpec, value: '' as unknown as RasterDEMSourceSpecification, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('object'); + expect(errors[0].message).toContain('expected'); + }); + + test('Should return error in case of unknown property', () => { + const errors = validateRasterDEMSource({validateSpec, value: {a: 1} as any, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(1); + expect(errors[0].message).toContain('a'); + expect(errors[0].message).toContain('unknown'); + }); + + test('Should return errors according to spec violations', () => { + const errors = validateRasterDEMSource({validateSpec, value: {type: 'raster-dem', url: {} as any, tiles: {} as any, encoding: 'foo' as any}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(3); + checkErrorMessage(errors[0].message, 'url', 'string', 'object'); + checkErrorMessage(errors[1].message, 'tiles', 'array', 'object'); + checkErrorMessage(errors[2].message, 'encoding', '[terrarium, mapbox, custom]', 'foo'); + }); + + test('Should return errors when custom encoding values are set but encoding is "mapbox"', () => { + const errors = validateRasterDEMSource({validateSpec, value: {type: 'raster-dem', encoding: 'mapbox', 'redFactor': 1.0, 'greenFactor': 1.0, 'blueFactor': 1.0, 'baseShift': 1.0}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(4); + checkErrorMessage(errors[0].message, 'redFactor', 'custom', 'mapbox'); + checkErrorMessage(errors[1].message, 'greenFactor', 'custom', 'mapbox'); + checkErrorMessage(errors[2].message, 'blueFactor', 'custom', 'mapbox'); + checkErrorMessage(errors[3].message, 'baseShift', 'custom', 'mapbox'); + }); + + test('Should return errors when custom encoding values are set but encoding is "terrarium"', () => { + const errors = validateRasterDEMSource({validateSpec, value: {type: 'raster-dem', encoding: 'terrarium', 'redFactor': 1.0, 'greenFactor': 1.0, 'blueFactor': 1.0, 'baseShift': 1.0}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(4); + checkErrorMessage(errors[0].message, 'redFactor', 'custom', 'terrarium'); + checkErrorMessage(errors[1].message, 'greenFactor', 'custom', 'terrarium'); + checkErrorMessage(errors[2].message, 'blueFactor', 'custom', 'terrarium'); + checkErrorMessage(errors[3].message, 'baseShift', 'custom', 'terrarium'); + }); + + test('Should pass when custom encoding values are set and encoding is "custom"', () => { + const errors = validateRasterDEMSource({validateSpec, value: {type: 'raster-dem', encoding: 'custom', 'redFactor': 1.0, 'greenFactor': 1.0, 'blueFactor': 1.0, 'baseShift': 1.0}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(0); + }); + + test('Should pass if everything is according to spec', () => { + const errors = validateRasterDEMSource({validateSpec, value: {type: 'raster-dem'}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(0); + }); +}); diff --git a/src/validate/validate_raster_dem_source.ts b/src/validate/validate_raster_dem_source.ts new file mode 100644 index 000000000..6b1b8a3f6 --- /dev/null +++ b/src/validate/validate_raster_dem_source.ts @@ -0,0 +1,58 @@ +import ValidationError from '../error/validation_error'; +import getType from '../util/get_type'; +import type {RasterDEMSourceSpecification, StyleSpecification} from '../types.g'; +import v8 from '../reference/v8.json' assert {type: 'json'}; +import {unbundle} from '../util/unbundle_jsonlint'; + +interface ValidateRasterDENSourceOptions { + sourceName?: string; + value: RasterDEMSourceSpecification; + styleSpec: typeof v8; + style: StyleSpecification; + validateSpec: Function; +} + +export default function validateRasterDEMSource( + options: ValidateRasterDENSourceOptions +): ValidationError[] { + + const sourceName = options.sourceName ?? ''; + const rasterDEM = options.value; + const styleSpec = options.styleSpec; + const rasterDEMSpec = styleSpec.source_raster_dem; + const style = options.style; + + let errors = []; + + const rootType = getType(rasterDEM); + if (rasterDEM === undefined) { + return errors; + } else if (rootType !== 'object') { + errors.push(new ValidationError('source_raster_dem', rasterDEM, `object expected, ${rootType} found`)); + return errors; + } + + const encoding = unbundle(rasterDEM.encoding); + const isCustomEncoding = encoding === 'custom'; + const customEncodingKeys = ['redFactor', 'greenFactor', 'blueFactor', 'baseShift']; + const encodingName = options.value.encoding ? `"${options.value.encoding}"` : 'Default'; + + for (const key in rasterDEM) { + if (!isCustomEncoding && customEncodingKeys.includes(key)) { + errors.push(new ValidationError(key, rasterDEM[key], `In "${sourceName}": "${key}" is only valid when "encoding" is set to "custom". ${encodingName} encoding found`)); + } else if (rasterDEMSpec[key]) { + errors = errors.concat(options.validateSpec({ + key, + value: rasterDEM[key], + valueSpec: rasterDEMSpec[key], + validateSpec: options.validateSpec, + style, + styleSpec + })); + } else { + errors.push(new ValidationError(key, rasterDEM[key], `unknown property "${key}"`)); + } + } + + return errors; +} diff --git a/src/validate/validate_source.ts b/src/validate/validate_source.ts index 5bb3a545c..a3ff80244 100644 --- a/src/validate/validate_source.ts +++ b/src/validate/validate_source.ts @@ -6,6 +6,7 @@ import validateEnum from './validate_enum'; import validateExpression from './validate_expression'; import validateString from './validate_string'; import getType from '../util/get_type'; +import validateRasterDEMSource from './validate_raster_dem_source'; const objectElementValidators = { promoteId: validatePromoteId @@ -28,7 +29,6 @@ export default function validateSource(options) { switch (type) { case 'vector': case 'raster': - case 'raster-dem': errors = validateObject({ key, value, @@ -39,6 +39,15 @@ export default function validateSource(options) { validateSpec, }); return errors; + case 'raster-dem': + errors = validateRasterDEMSource({ + sourceName: key, + value, + style: options.style, + styleSpec, + validateSpec, + }); + return errors; case 'geojson': errors = validateObject({