From b61d19b8ed84b37f729bba44503b8d69ab209c13 Mon Sep 17 00:00:00 2001 From: Arindam Bose Date: Fri, 20 Dec 2019 14:27:59 -0800 Subject: [PATCH] Allow programmatic construction of style even when one is not passed in initially ( h/t @stepankuzmin ) (#8924) --- src/style-spec/empty.js | 29 +++++++++++++++++++++++++++++ src/style/style.js | 8 ++++++++ src/ui/map.js | 14 +++++++++++++- test/unit/style-spec/empty.test.js | 15 +++++++++++++++ test/unit/ui/map.test.js | 17 +++++++++++++++++ test/util/index.js | 17 +++++++++-------- 6 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 src/style-spec/empty.js create mode 100644 test/unit/style-spec/empty.test.js diff --git a/src/style-spec/empty.js b/src/style-spec/empty.js new file mode 100644 index 00000000000..e3db069f92d --- /dev/null +++ b/src/style-spec/empty.js @@ -0,0 +1,29 @@ +import latest from './reference/latest'; + +export default function emptyStyle() { + const style = {}; + + const version = latest['$version']; + for (const styleKey in latest['$root']) { + const spec = latest['$root'][styleKey]; + + if (spec.required) { + let value = null; + if (styleKey === 'version') { + value = version; + } else { + if (spec.type === 'array') { + value = []; + } else { + value = {}; + } + } + + if (value != null) { + style[styleKey] = value; + } + } + } + + return style; +} diff --git a/src/style/style.js b/src/style/style.js index 596693ff7a4..bcd8d1c1ffb 100644 --- a/src/style/style.js +++ b/src/style/style.js @@ -27,6 +27,7 @@ import GeoJSONSource from '../source/geojson_source'; import styleSpec from '../style-spec/reference/latest'; import getWorkerPool from '../util/global_worker_pool'; import deref from '../style-spec/deref'; +import emptyStyle from '../style-spec/empty'; import diffStyles, {operations as diffOperations} from '../style-spec/diff'; import { registerForPluginStateChange, @@ -88,6 +89,8 @@ const ignoredDiffOperations = pick(diffOperations, [ 'setPitch' ]); +const empty = emptyStyle(); + export type StyleOptions = { validate?: boolean, localIdeographFontFamily?: string @@ -229,6 +232,11 @@ class Style extends Evented { }); } + loadEmpty() { + this.fire(new Event('dataloading', {dataType: 'style'})); + this._load(empty, false); + } + _load(json: StyleSpecification, validate: boolean) { if (validate && emitValidationErrors(this, validateStyle(json))) { return; diff --git a/src/ui/map.js b/src/ui/map.js index ba375ad25f3..cd94a280762 100755 --- a/src/ui/map.js +++ b/src/ui/map.js @@ -1216,6 +1216,14 @@ class Map extends Camera { return this; } + _lazyInitEmptyStyle() { + if (!this.style) { + this.style = new Style(this, {}); + this.style.setEventedParent(this, {style: this.style}); + this.style.loadEmpty(); + } + } + _diffStyle(style: StyleSpecification | string, options?: {diff?: boolean} & StyleOptions) { if (typeof style === 'string') { const url = this._requestManager.normalizeStyleURL(style); @@ -1307,6 +1315,7 @@ class Map extends Camera { * @see Raster DEM source: [Add hillshading](https://docs.mapbox.com/mapbox-gl-js/example/hillshade/) */ addSource(id: string, source: SourceSpecification) { + this._lazyInitEmptyStyle(); this.style.addSource(id, source); return this._update(true); } @@ -1359,6 +1368,7 @@ class Map extends Camera { * @param {Function} callback Called when the source type is ready or with an error argument if there is an error. */ addSourceType(name: string, SourceType: any, callback: Function) { + this._lazyInitEmptyStyle(); return this.style.addSourceType(name, SourceType, callback); } @@ -1439,7 +1449,7 @@ class Map extends Camera { addImage(id: string, image: HTMLImageElement | ImageData | {width: number, height: number, data: Uint8Array | Uint8ClampedArray} | StyleImageInterface, {pixelRatio = 1, sdf = false, stretchX, stretchY, content}: $Shape = {}) { - + this._lazyInitEmptyStyle(); const version = 0; if (image instanceof HTMLImageElement) { @@ -1627,6 +1637,7 @@ class Map extends Camera { * @see [Add a WMS source](https://www.mapbox.com/mapbox-gl-js/example/wms/) */ addLayer(layer: LayerSpecification | CustomLayerInterface, beforeId?: string) { + this._lazyInitEmptyStyle(); this.style.addLayer(layer, beforeId); return this._update(true); } @@ -1808,6 +1819,7 @@ class Map extends Camera { * @returns {Map} `this` */ setLight(light: LightSpecification, options: StyleSetterOptions = {}) { + this._lazyInitEmptyStyle(); this.style.setLight(light, options); return this._update(true); } diff --git a/test/unit/style-spec/empty.test.js b/test/unit/style-spec/empty.test.js new file mode 100644 index 00000000000..5184f4f0dea --- /dev/null +++ b/test/unit/style-spec/empty.test.js @@ -0,0 +1,15 @@ +import {test} from '../../util/test'; +import emptyStyle from '../../../src/style-spec/empty'; +import validateStyleMin from '../../../src/style-spec/validate_style.min'; + +test('it generates something', (t) => { + const style = emptyStyle(); + t.ok(style); + t.end(); +}); + +test('generated empty style is a valid style', (t) => { + const errors = validateStyleMin(emptyStyle()); + t.equal(errors.length, 0); + t.end(); +}); diff --git a/test/unit/ui/map.test.js b/test/unit/ui/map.test.js index 059291b980d..c110e41d100 100755 --- a/test/unit/ui/map.test.js +++ b/test/unit/ui/map.test.js @@ -388,6 +388,23 @@ test('Map', (t) => { }); }); + t.test('a layer can be added even if a map is created without a style', (t) => { + const map = createMap(t, {deleteStyle: true}); + const layer = { + id: 'background', + type: 'background' + }; + map.addLayer(layer); + t.end(); + }); + + t.test('a source can be added even if a map is created without a style', (t) => { + const map = createMap(t, {deleteStyle: true}); + const source = createStyleSource(); + map.addSource('fill', source); + t.end(); + }); + t.test('returns the style with added source and layer', (t) => { const style = createStyle(); const map = createMap(t, {style}); diff --git a/test/util/index.js b/test/util/index.js index 011d3a31529..e136124f9ab 100644 --- a/test/util/index.js +++ b/test/util/index.js @@ -4,13 +4,7 @@ import {extend} from '../../src/util/util'; export function createMap(t, options, callback) { const container = window.document.createElement('div'); - - Object.defineProperty(container, 'clientWidth', {value: 200, configurable: true}); - Object.defineProperty(container, 'clientHeight', {value: 200, configurable: true}); - - if (!options || !options.skipCSSStub) t.stub(Map.prototype, '_detectMissingCSS'); - - const map = new Map(extend({ + const defaultOptions = { container, interactive: false, attributionControl: false, @@ -20,8 +14,15 @@ export function createMap(t, options, callback) { "sources": {}, "layers": [] } - }, options)); + }; + + Object.defineProperty(container, 'clientWidth', {value: 200, configurable: true}); + Object.defineProperty(container, 'clientHeight', {value: 200, configurable: true}); + + if (!options || !options.skipCSSStub) t.stub(Map.prototype, '_detectMissingCSS'); + if (options && options.deleteStyle) delete defaultOptions.style; + const map = new Map(extend(defaultOptions, options)); if (callback) map.on('load', () => { callback(null, map); });