From 397b5e0b14bdcca2979e135ee1229db166010446 Mon Sep 17 00:00:00 2001 From: Harel M Date: Mon, 8 Jan 2024 23:44:40 +0200 Subject: [PATCH] Add Sky spec (#478) * Style Spec for sky and fog (#298) * Copy style-spec pieces from https://github.com/maplibre/maplibre-gl-js/pull/1713 * add missing SkySpecification * fix lint error * Add tests, improve documentation, add to the style-spec docs * Fix lint * Update changelog --------- Co-authored-by: Andrew Calcutt --- CHANGELOG.md | 1 + build/generate-style-spec.ts | 4 ++ docs/src/pages.tsx | 1 + docs/src/routes/glyphs/index.tsx | 3 +- docs/src/routes/light/index.tsx | 2 +- docs/src/routes/sky/index.tsx | 26 ++++++++++++ docs/src/routes/terrain/index.tsx | 7 +++- src/reference/v8.json | 68 +++++++++++++++++++++++++++++++ src/validate/validate.ts | 2 + src/validate/validate_sky.test.ts | 40 ++++++++++++++++++ src/validate/validate_sky.ts | 44 ++++++++++++++++++++ src/validate_style.min.ts | 2 + src/validate_style.ts | 1 + 13 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 docs/src/routes/sky/index.tsx create mode 100644 src/validate/validate_sky.test.ts create mode 100644 src/validate/validate_sky.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a1842943..0fae19468 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * Add terrain to diff method and improve type. This also removes the `operations` from the API [#460](https://github.com/maplibre/maplibre-style-spec/pull/460) * Improve the type of `data` in the `GeoJSONSourceSpecification` for TypeScript [#463](https://github.com/maplibre/maplibre-style-spec/pull/463). * Add expression tests to this repo [#476](https://github.com/maplibre/maplibre-style-spec/pull/476) +* Add Sky spec, this is only the definition, no implementation at this point, only validation [#478](https://github.com/maplibre/maplibre-style-spec/pull/478) ### 🐞 Bug fixes diff --git a/build/generate-style-spec.ts b/build/generate-style-spec.ts index 6e0dafd87..1e8ed3dc0 100644 --- a/build/generate-style-spec.ts +++ b/build/generate-style-spec.ts @@ -34,6 +34,8 @@ function propertyType(property) { } case 'light': return 'LightSpecification'; + case 'sky': + return 'SkySpecification'; case 'sources': return '{[_: string]: SourceSpecification}'; case '*': @@ -312,6 +314,8 @@ ${objectDeclaration('StyleSpecification', spec.$root)} ${objectDeclaration('LightSpecification', spec.light)} +${objectDeclaration('SkySpecification', spec.sky)} + ${objectDeclaration('TerrainSpecification', spec.terrain)} ${spec.source.map(key => { diff --git a/docs/src/pages.tsx b/docs/src/pages.tsx index 69368039c..e3ae5e8e6 100644 --- a/docs/src/pages.tsx +++ b/docs/src/pages.tsx @@ -8,6 +8,7 @@ export const pages: {path: string; title: string}[] = [ {title: 'Sources', path: 'sources/'}, {title: 'Sprite', path: 'sprite/'}, {title: 'Terrain', path: 'terrain/'}, + {title: 'Sky', path: 'sky/'}, {title: 'Transition', path: 'transition/'}, {title: 'Types', path: 'types/'}, {title: 'Deprecations', path: 'deprecations/'}, diff --git a/docs/src/routes/glyphs/index.tsx b/docs/src/routes/glyphs/index.tsx index fed7ff36f..6cf670bb8 100644 --- a/docs/src/routes/glyphs/index.tsx +++ b/docs/src/routes/glyphs/index.tsx @@ -1,4 +1,5 @@ import {Markdown} from '~/components/markdown/markdown'; +import ref from '../../../../src/reference/latest'; function glyphs() { const md = ` @@ -7,7 +8,7 @@ function glyphs() { A style's \`glyphs\` property provides a URL template for loading signed-distance-field glyph sets in PBF format. \`\`\`json -"glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf" +"glyphs": ${JSON.stringify(ref.$root.glyphs.example, null, 2)} \`\`\` This URL template should include two tokens: diff --git a/docs/src/routes/light/index.tsx b/docs/src/routes/light/index.tsx index 171f7e3f7..89f232f21 100644 --- a/docs/src/routes/light/index.tsx +++ b/docs/src/routes/light/index.tsx @@ -20,7 +20,7 @@ A style's \`light\` property provides a global light source for that style. Sinc
- +
); } diff --git a/docs/src/routes/sky/index.tsx b/docs/src/routes/sky/index.tsx new file mode 100644 index 000000000..d5d3d9346 --- /dev/null +++ b/docs/src/routes/sky/index.tsx @@ -0,0 +1,26 @@ +import {Markdown} from '~/components/markdown/markdown'; +import ref from '../../../../src/reference/latest'; +import {Items} from '../../components/items/items'; +function Root() { + + const md = `# Sky + +Add sky and fog to the map. + +This feautre is still under development and is not yet available in the latest release. + +\`\`\`json +"sky": ${JSON.stringify(ref.$root.sky.example, null, 2)} +\`\`\` + +`; + + return ( +
+ + +
+ ); +} + +export default Root; diff --git a/docs/src/routes/terrain/index.tsx b/docs/src/routes/terrain/index.tsx index bf3ac3acc..f2543b226 100644 --- a/docs/src/routes/terrain/index.tsx +++ b/docs/src/routes/terrain/index.tsx @@ -5,8 +5,13 @@ function Root() { const md = `# Terrain -Add 3D terrain to the map.`; +Add 3D terrain to the map. +\`\`\`json +"terrain": ${JSON.stringify(ref.$root.terrain.example, null, 2)} +\`\`\` + +`; return (
diff --git a/src/reference/v8.json b/src/reference/v8.json index 2f4cfc49f..2f8876b9d 100644 --- a/src/reference/v8.json +++ b/src/reference/v8.json @@ -63,6 +63,16 @@ "intensity": 0.4 } }, + "sky": { + "type": "sky", + "doc": "The map's sky configuration.", + "example": { + "sky-color": "#199EF3", + "fog-color": "#00ff00", + "horizon-blend": 0.5, + "fog-blend": 0.6 + } + }, "terrain": { "type": "terrain", "doc": "The terrain configuration.", @@ -3902,6 +3912,64 @@ } } }, + "sky": { + "sky-color": { + "type": "color", + "property-type": "data-constant", + "default": "#88C6FC", + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "The base color for the sky." + }, + "fog-color": { + "type": "color", + "property-type": "data-constant", + "default": "#ffffff", + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "The base color for the fog." + }, + "fog-blend": { + "type": "number", + "property-type": "data-constant", + "default": 0.5, + "minimum": 0, + "maximum": 1, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "How to blend the fog over the 3D terrain. A value between 0 and 1. Where 0 is the map center and 1 is the horizon" + }, + "horizon-blend": { + "type": "number", + "property-type": "data-constant", + "default": 0.8, + "minimum": 0, + "maximum": 1, + "expression": { + "interpolated": true, + "parameters": [ + "zoom" + ] + }, + "transition": true, + "doc": "How to blend the fog and sky color at the horizon. A value between 0 and 1. Where 0 is the horizon and 1 is map-height / 2" + } + }, "terrain": { "source": { "type": "string", diff --git a/src/validate/validate.ts b/src/validate/validate.ts index 19b690298..06c1e692c 100644 --- a/src/validate/validate.ts +++ b/src/validate/validate.ts @@ -17,6 +17,7 @@ import validateFilter from './validate_filter'; import validateLayer from './validate_layer'; import validateSource from './validate_source'; import validateLight from './validate_light'; +import validateSky from './validate_sky'; import validateTerrain from './validate_terrain'; import validateString from './validate_string'; import validateFormatted from './validate_formatted'; @@ -41,6 +42,7 @@ const VALIDATORS = { 'object': validateObject, 'source': validateSource, 'light': validateLight, + 'sky': validateSky, 'terrain': validateTerrain, 'string': validateString, 'formatted': validateFormatted, diff --git a/src/validate/validate_sky.test.ts b/src/validate/validate_sky.test.ts new file mode 100644 index 000000000..a2af8343c --- /dev/null +++ b/src/validate/validate_sky.test.ts @@ -0,0 +1,40 @@ +import validateSky from './validate_sky'; +import validateSpec from './validate'; +import v8 from '../reference/v8.json' assert {type: 'json'}; +import {SkySpecification} from '../types.g'; + +describe('Validate sky', () => { + it('Should pass when value is undefined', () => { + const errors = validateSky({validateSpec, value: undefined, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(0); + }); + + test('Should return error when value is not an object', () => { + const errors = validateSky({validateSpec, value: '' as unknown as SkySpecification, 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 = validateSky({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 = validateSky({validateSpec, value: {'sky-color': 1 as any, 'fog-color': 2 as any, 'horizon-blend': {} as any, 'fog-blend': 'foo' as any}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(4); + expect(errors[0].message).toBe('sky-color: color expected, number found'); + expect(errors[1].message).toBe('fog-color: color expected, number found'); + expect(errors[2].message).toBe('horizon-blend: missing required property "stops"'); + expect(errors[3].message).toBe('fog-blend: number expected, string found'); + }); + + test('Should pass if everything is according to spec', () => { + const errors = validateSky({validateSpec, value: {'sky-color': 'red', 'fog-color': '#123456', 'horizon-blend': 1, 'fog-blend': 0}, styleSpec: v8, style: {} as any}); + expect(errors).toHaveLength(0); + }); +}); + diff --git a/src/validate/validate_sky.ts b/src/validate/validate_sky.ts new file mode 100644 index 000000000..66e735912 --- /dev/null +++ b/src/validate/validate_sky.ts @@ -0,0 +1,44 @@ +import ValidationError from '../error/validation_error'; +import getType from '../util/get_type'; +import validate from './validate'; +import v8 from '../reference/v8.json' assert {type: 'json'}; +import {SkySpecification, StyleSpecification} from '../types.g'; + +interface ValidateSkyOptions { + sourceName?: string; + value: SkySpecification; + styleSpec: typeof v8; + style: StyleSpecification; + validateSpec: Function; +} + +export default function validateSky(options: ValidateSkyOptions) { + const sky = options.value; + const styleSpec = options.styleSpec; + const skySpec = styleSpec.sky; + const style = options.style; + + const rootType = getType(sky); + if (sky === undefined) { + return []; + } else if (rootType !== 'object') { + return [new ValidationError('sky', sky, `object expected, ${rootType} found`)]; + } + + let errors = []; + for (const key in sky) { + if (skySpec[key]) { + errors = errors.concat(validate({ + key, + value: sky[key], + valueSpec: skySpec[key], + style, + styleSpec + })); + } else { + errors = errors.concat([new ValidationError(key, sky[key], `unknown property "${key}"`)]); + } + } + + return errors; +} diff --git a/src/validate_style.min.ts b/src/validate_style.min.ts index 428591a08..4188496b0 100644 --- a/src/validate_style.min.ts +++ b/src/validate_style.min.ts @@ -5,6 +5,7 @@ import latestStyleSpec from './reference/latest'; import validateSource from './validate/validate_source'; import validateLight from './validate/validate_light'; +import validateSky from './validate/validate_sky'; import validateTerrain from './validate/validate_terrain'; import validateLayer from './validate/validate_layer'; import validateFilter from './validate/validate_filter'; @@ -65,6 +66,7 @@ validateStyleMin.source = wrapCleanErrors(injectValidateSpec(validateSource)); validateStyleMin.sprite = wrapCleanErrors(injectValidateSpec(validateSprite)); validateStyleMin.glyphs = wrapCleanErrors(injectValidateSpec(validateGlyphsUrl)); validateStyleMin.light = wrapCleanErrors(injectValidateSpec(validateLight)); +validateStyleMin.sky = wrapCleanErrors(injectValidateSpec(validateSky)); validateStyleMin.terrain = wrapCleanErrors(injectValidateSpec(validateTerrain)); validateStyleMin.layer = wrapCleanErrors(injectValidateSpec(validateLayer)); validateStyleMin.filter = wrapCleanErrors(injectValidateSpec(validateFilter)); diff --git a/src/validate_style.ts b/src/validate_style.ts index 403a60eb5..27ad502b0 100644 --- a/src/validate_style.ts +++ b/src/validate_style.ts @@ -34,6 +34,7 @@ export default function validateStyle(style: StyleSpecification | string | Buffe export const source = validateStyleMin.source; export const light = validateStyleMin.light; +export const sky = validateStyleMin.sky; export const terrain = validateStyleMin.terrain; export const layer = validateStyleMin.layer; export const filter = validateStyleMin.filter;