From 7631f32bb221af3df4b4f7490e2393397a223c1f Mon Sep 17 00:00:00 2001 From: Lucas Wojciechowski Date: Thu, 4 Feb 2016 14:05:15 -0800 Subject: [PATCH] Add Map#getStyle method fixes #1982 --- debug/site.js | 2 +- js/source/geojson_source.js | 7 ++ js/source/image_source.js | 11 +++ js/source/raster_tile_source.js | 8 ++ js/source/vector_tile_source.js | 5 ++ js/source/video_source.js | 11 +++ js/style/style.js | 27 +++++- js/style/style_declaration.js | 16 ++-- js/style/style_layer.js | 23 +++-- js/ui/map.js | 9 ++ js/util/util.js | 17 ++++ test/js/source/geojson_source.test.js | 31 +++++++ test/js/source/vector_tile_source.test.js | 29 +++++++ test/js/style/style.test.js | 2 +- test/js/style/style_layer.test.js | 101 +++++++++++++++++++++- test/js/ui/map.test.js | 69 ++++++++++++++- test/js/util/util.test.js | 17 ++++ 17 files changed, 360 insertions(+), 25 deletions(-) diff --git a/debug/site.js b/debug/site.js index 7020c4c2184..60e206f479d 100644 --- a/debug/site.js +++ b/debug/site.js @@ -11,7 +11,7 @@ var map = new mapboxgl.Map({ map.addControl(new mapboxgl.Navigation()); -map.on('style.load', function() { +map.on('load', function() { map.addSource('geojson', { "type": "geojson", "data": "/debug/route.json" diff --git a/js/source/geojson_source.js b/js/source/geojson_source.js index 3d3c87332de..af8efe11c7b 100644 --- a/js/source/geojson_source.js +++ b/js/source/geojson_source.js @@ -127,6 +127,13 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy } }, + serialize: function() { + return { + type: 'geojson', + data: this._data + }; + }, + getVisibleCoordinates: Source._getVisibleCoordinates, getTile: Source._getTile, diff --git a/js/source/image_source.js b/js/source/image_source.js index d4d6c3bbcaa..99cb5b38e29 100644 --- a/js/source/image_source.js +++ b/js/source/image_source.js @@ -31,6 +31,9 @@ module.exports = ImageSource; * map.removeSource('some id'); // remove */ function ImageSource(options) { + this.urls = options.urls; + this.coordinates = options.coordinates; + ajax.getImage(options.url, function(err, image) { // @TODO handle errors via event. if (err) return; @@ -148,5 +151,13 @@ ImageSource.prototype = util.inherit(Evented, { featuresIn: function(bbox, params, callback) { return callback(null, []); + }, + + serialize: function() { + return { + type: 'image', + urls: this.urls, + coordinates: this.coordinates + }; } }); diff --git a/js/source/raster_tile_source.js b/js/source/raster_tile_source.js index cf848e2850d..03a3b82c8a1 100644 --- a/js/source/raster_tile_source.js +++ b/js/source/raster_tile_source.js @@ -39,6 +39,14 @@ RasterTileSource.prototype = util.inherit(Evented, { // noop }, + serialize: function() { + return { + type: 'raster', + url: this.url, + tileSize: this.tileSize + }; + }, + getVisibleCoordinates: Source._getVisibleCoordinates, getTile: Source._getTile, diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index aca4747a524..a58a5d1ec09 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -9,6 +9,7 @@ module.exports = VectorTileSource; function VectorTileSource(options) { util.extend(this, util.pick(options, ['url', 'tileSize'])); + this._options = util.extend({ type: 'vector' }, options); if (this.tileSize !== 512) { throw new Error('vector tile sources must have a tileSize of 512'); @@ -45,6 +46,10 @@ VectorTileSource.prototype = util.inherit(Evented, { } }, + serialize: function() { + return util.extend({}, this._options); + }, + getVisibleCoordinates: Source._getVisibleCoordinates, getTile: Source._getTile, diff --git a/js/source/video_source.js b/js/source/video_source.js index f877ad7f026..4e77daa3319 100644 --- a/js/source/video_source.js +++ b/js/source/video_source.js @@ -33,6 +33,9 @@ module.exports = VideoSource; * map.removeSource('some id'); // remove */ function VideoSource(options) { + this.urls = options.urls; + this.coordinates = options.coordinates; + ajax.getVideo(options.urls, function(err, video) { // @TODO handle errors via event. if (err) return; @@ -168,5 +171,13 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype * featuresIn: function(bbox, params, callback) { return callback(null, []); + }, + + serialize: function() { + return { + type: 'video', + urls: this.urls, + coordinates: this.coordinates + }; } }); diff --git a/js/style/style.js b/js/style/style.js index 4f7e5312bd2..293152928d5 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -172,7 +172,7 @@ Style.prototype = util.inherit(Evented, { _broadcastLayers: function() { this.dispatcher.broadcast('set layers', this._order.map(function(id) { - return this._layers[id].serialize(); + return this._layers[id].serialize({includeRefProperties: true}); }, this)); }, @@ -384,6 +384,27 @@ Style.prototype = util.inherit(Evented, { return this.getLayer(layer).getPaintProperty(name, klass); }, + serialize: function() { + return util.filterObject({ + version: this.stylesheet.version, + name: this.stylesheet.name, + metadata: this.stylesheet.metadata, + center: this.stylesheet.center, + zoom: this.stylesheet.zoom, + bearing: this.stylesheet.bearing, + pitch: this.stylesheet.pitch, + sprite: this.stylesheet.sprite, + glyphs: this.stylesheet.glyphs, + transition: this.stylesheet.transition, + sources: util.mapObject(this.sources, function(source) { + return source.serialize(); + }), + layers: this._order.map(function(id) { + return this._layers[id].serialize(); + }, this) + }, function(value) { return value !== undefined; }); + }, + featuresAt: function(coord, params, callback) { this._queryFeatures('featuresAt', coord, params, callback); }, @@ -415,7 +436,9 @@ Style.prototype = util.inherit(Evented, { return this._layers[feature.layer] !== undefined; }.bind(this)) .map(function(feature) { - feature.layer = this._layers[feature.layer].serialize(); + feature.layer = this._layers[feature.layer].serialize({ + includeRefProperties: true + }); return feature; }.bind(this))); }.bind(this)); diff --git a/js/style/style_declaration.js b/js/style/style_declaration.js index e0f4beac815..1cf04753c90 100644 --- a/js/style/style_declaration.js +++ b/js/style/style_declaration.js @@ -10,22 +10,20 @@ function StyleDeclaration(reference, value) { this.transitionable = reference.transition; if (value == null) { - value = reference.default; + this.value = reference.default; + } else { + this.value = value; } // immutable representation of value. used for comparison - this.json = JSON.stringify(value); + this.json = JSON.stringify(this.value); - if (this.type === 'color') { - this.value = parseColor(value); - } else { - this.value = value; - } + var parsedValue = this.type === 'color' ? parseColor(this.value) : value; if (reference.function === 'interpolated') { - this.calculate = MapboxGLFunction.interpolated(this.value); + this.calculate = MapboxGLFunction.interpolated(parsedValue); } else { - this.calculate = MapboxGLFunction['piecewise-constant'](this.value); + this.calculate = MapboxGLFunction['piecewise-constant'](parsedValue); if (reference.transition) { this.calculate = transitioned(this.calculate); } diff --git a/js/style/style_layer.js b/js/style/style_layer.js index 965e8b7714f..ab353d63ea6 100644 --- a/js/style/style_layer.js +++ b/js/style/style_layer.js @@ -197,19 +197,14 @@ StyleLayer.prototype = { } }, - serialize: function() { + serialize: function(options) { var output = { 'id': this.id, 'ref': this.ref, 'metadata': this.metadata, - 'type': this.type, - 'source': this.source, - 'source-layer': this.sourceLayer, 'minzoom': this.minzoom, 'maxzoom': this.maxzoom, - 'filter': this.filter, - 'interactive': this.interactive, - 'layout': util.mapObject(this._layoutDeclarations, getDeclarationValue) + 'interactive': this.interactive }; for (var klass in this._paintDeclarations) { @@ -217,7 +212,19 @@ StyleLayer.prototype = { output[key] = util.mapObject(this._paintDeclarations[klass], getDeclarationValue); } - return output; + if (!this.ref || (options && options.includeRefProperties)) { + util.extend(output, { + 'type': this.type, + 'source': this.source, + 'source-layer': this.sourceLayer, + 'filter': this.filter, + 'layout': util.mapObject(this._layoutDeclarations, getDeclarationValue) + }); + } + + return util.filterObject(output, function(value, key) { + return value !== undefined && !(key === 'layout' && !Object.keys(value).length); + }); } }; diff --git a/js/ui/map.js b/js/ui/map.js index edd11474310..87a78cf9ee3 100644 --- a/js/ui/map.js +++ b/js/ui/map.js @@ -448,6 +448,15 @@ util.extend(Map.prototype, /** @lends Map.prototype */{ return this; }, + /** + * Get a style object that can be used to recreate the map's style + * + * @returns {Object} style + */ + getStyle: function() { + return this.style.serialize(); + }, + /** * Add a source to the map style. * diff --git a/js/util/util.js b/js/util/util.js index 78c443472b2..a2cd26a4380 100644 --- a/js/util/util.js +++ b/js/util/util.js @@ -429,3 +429,20 @@ exports.mapObject = function(input, iterator, context) { } return output; }; + +/** + * Create an object by filtering out values of an existing object + * @param {Object} input + * @param {Function} iterator + * @returns {Object} + * @private + */ +exports.filterObject = function(input, iterator, context) { + var output = {}; + for (var key in input) { + if (iterator.call(context || this, input[key], key, input)) { + output[key] = input[key]; + } + } + return output; +}; diff --git a/test/js/source/geojson_source.test.js b/test/js/source/geojson_source.test.js index 31b39faabad..0bd4b23d388 100644 --- a/test/js/source/geojson_source.test.js +++ b/test/js/source/geojson_source.test.js @@ -180,3 +180,34 @@ test('GeoJSONSource#update', function(t) { }); }); }); + +test('GeoJSONSource#serialize', function(t) { + + t.test('serialize source with inline data', function(t) { + var source = new GeoJSONSource({data: hawkHill}); + t.deepEqual(source.serialize(), { + type: 'geojson', + data: hawkHill + }); + t.end(); + }); + + t.test('serialize source with url', function(t) { + var source = new GeoJSONSource({data: 'local://data.json'}); + t.deepEqual(source.serialize(), { + type: 'geojson', + data: 'local://data.json' + }); + t.end(); + }); + + t.test('serialize source with updated data', function(t) { + var source = new GeoJSONSource({data: {}}); + source.setData(hawkHill); + t.deepEqual(source.serialize(), { + type: 'geojson', + data: hawkHill + }); + t.end(); + }); +}); diff --git a/test/js/source/vector_tile_source.test.js b/test/js/source/vector_tile_source.test.js index 0b052a7e8a0..9284d1e5990 100644 --- a/test/js/source/vector_tile_source.test.js +++ b/test/js/source/vector_tile_source.test.js @@ -70,7 +70,36 @@ test('VectorTileSource', function(t) { }); }); + t.test('serialize', function(t) { + var source = new VectorTileSource({ + url: "http://example.com" + }); + t.deepEqual(source.serialize(), { + type: 'vector', + url: "http://example.com" + }); + t.end(); + }); + + t.test('serialize TileJSON', function(t) { + var source = new VectorTileSource({ + minzoom: 1, + maxzoom: 10, + attribution: "Mapbox", + tiles: ["http://example.com/{z}/{x}/{y}.png"] + }); + t.deepEqual(source.serialize(), { + type: 'vector', + minzoom: 1, + maxzoom: 10, + attribution: "Mapbox", + tiles: ["http://example.com/{z}/{x}/{y}.png"] + }); + t.end(); + }); + t.test('after', function(t) { server.close(t.end); }); + }); diff --git a/test/js/style/style.test.js b/test/js/style/style.test.js index d8646e8c384..bfb94f67ee4 100644 --- a/test/js/style/style.test.js +++ b/test/js/style/style.test.js @@ -846,7 +846,7 @@ test('Style#featuresAt', function(t) { t.test('includes paint properties', function(t) { featuresInOrAt({}, function(err, results) { t.error(err); - t.deepEqual(results[0].layer.paint['line-color'], [ 1, 0, 0, 1 ]); + t.deepEqual(results[0].layer.paint['line-color'], 'red'); t.end(); }); }); diff --git a/test/js/style/style_layer.test.js b/test/js/style/style_layer.test.js index 6274c642a24..1081cd870a7 100644 --- a/test/js/style/style_layer.test.js +++ b/test/js/style/style_layer.test.js @@ -3,6 +3,7 @@ var test = require('prova'); var StyleLayer = require('../../../js/style/style_layer'); var FillStyleLayer = require('../../../js/style/style_layer/fill_style_layer'); +var util = require('../../../js/util/util'); test('StyleLayer', function(t) { t.test('sets properties from ref', function (t) { @@ -32,7 +33,7 @@ test('StyleLayer#setPaintProperty', function(t) { layer.setPaintProperty('background-color', 'blue'); - t.deepEqual(layer.getPaintProperty('background-color'), [0, 0, 1, 1]); + t.deepEqual(layer.getPaintProperty('background-color'), 'blue'); t.end(); }); @@ -47,7 +48,7 @@ test('StyleLayer#setPaintProperty', function(t) { layer.setPaintProperty('background-color', 'blue'); - t.deepEqual(layer.getPaintProperty('background-color'), [0, 0, 1, 1]); + t.deepEqual(layer.getPaintProperty('background-color'), 'blue'); t.end(); }); @@ -77,7 +78,7 @@ test('StyleLayer#setPaintProperty', function(t) { layer.setPaintProperty('background-color', 'blue', 'night'); - t.deepEqual(layer.getPaintProperty('background-color', 'night'), [0, 0, 1, 1]); + t.deepEqual(layer.getPaintProperty('background-color', 'night'), 'blue'); t.end(); }); @@ -188,3 +189,97 @@ test('StyleLayer#setLayoutProperty', function(t) { t.end(); }); }); + +test('StyleLayer#serialize', function(t) { + + function createSymbolLayer(layer) { + return util.extend({ + id: 'symbol', + type: 'symbol', + paint: { + 'text-color': 'blue' + }, + layout: { + 'text-transform': 'uppercase' + } + }, layer); + } + + function createRefedSymbolLayer(layer) { + return util.extend({ + id: 'symbol', + ref: 'symbol', + paint: { + 'text-color': 'red' + } + }, layer); + } + + t.test('serializes layers', function(t) { + t.deepEqual( + StyleLayer.create(createSymbolLayer()).serialize(), + createSymbolLayer() + ); + t.end(); + }); + + t.test('serializes refed layers', function(t) { + t.deepEqual( + StyleLayer.create(createRefedSymbolLayer(), createSymbolLayer()).serialize(), + createRefedSymbolLayer() + ); + t.end(); + }); + + t.test('serializes refed layers with ref properties', function(t) { + t.deepEqual( + StyleLayer.create( + createRefedSymbolLayer(), + StyleLayer.create(createSymbolLayer()) + ).serialize({includeRefProperties: true}), + { + id: "symbol", + type: "symbol", + paint: { "text-color": "red" }, + layout: { "text-transform": "uppercase" }, + ref: "symbol" + } + ); + t.end(); + }); + + t.test('serializes functions', function(t) { + var layerPaint = { + 'text-color': { + base: 2, + stops: [[0, 'red'], [1, 'blue']] + } + }; + + t.deepEqual( + StyleLayer.create(createSymbolLayer({ paint: layerPaint })).serialize().paint, + layerPaint + ); + t.end(); + }); + + t.test('serializes added paint properties', function(t) { + var layer = StyleLayer.create(createSymbolLayer()); + layer.setPaintProperty('text-halo-color', 'orange'); + + t.equal(layer.serialize().paint['text-halo-color'], 'orange'); + t.equal(layer.serialize().paint['text-color'], 'blue'); + + t.end(); + }); + + t.test('serializes added layout properties', function(t) { + var layer = StyleLayer.create(createSymbolLayer()); + layer.setLayoutProperty('text-size', 20); + + t.equal(layer.serialize().layout['text-transform'], 'uppercase'); + t.equal(layer.serialize().layout['text-size'], 20); + + t.end(); + }); +}); diff --git a/test/js/ui/map.test.js b/test/js/ui/map.test.js index 3238e231b74..1c274fa8882 100644 --- a/test/js/ui/map.test.js +++ b/test/js/ui/map.test.js @@ -173,6 +173,73 @@ test('Map', function(t) { }); + t.test('#getStyle', function(t) { + function createStyle() { + return { + version: 8, + center: [-73.9749, 40.7736], + zoom: 12.5, + bearing: 29, + pitch: 50, + sources: {}, + layers: [] + }; + } + + function createStyleSource() { + return { + type: "geojson", + data: { + type: "FeatureCollection", + features: [] + } + }; + } + + function createStyleLayer() { + return { + id: 'background', + type: 'background' + }; + } + + t.test('returns the style', function(t) { + var style = createStyle(); + var map = createMap({style: style}); + + map.on('load', function () { + t.deepEqual(map.getStyle(), style); + t.end(); + }); + }); + + t.test('returns the style with added sources', function(t) { + var style = createStyle(); + var map = createMap({style: style}); + + map.on('load', function () { + map.addSource('geojson', createStyleSource()); + t.deepEqual(map.getStyle(), extend(createStyle(), { + sources: {geojson: createStyleSource()} + })); + t.end(); + }); + }); + + t.test('returns the style with added layers', function(t) { + var style = createStyle(); + var map = createMap({style: style}); + + map.on('load', function () { + map.addLayer(createStyleLayer()); + t.deepEqual(map.getStyle(), extend(createStyle(), { + layers: [createStyleLayer()] + })); + t.end(); + }); + }); + }); + t.test('#resize', function(t) { t.test('sets width and height from container offsets', function(t) { var map = createMap(), @@ -566,7 +633,7 @@ test('Map', function(t) { map.on('style.load', function () { map.setPaintProperty('background', 'background-color', 'red'); - t.deepEqual(map.getPaintProperty('background', 'background-color'), [1, 0, 0, 1]); + t.deepEqual(map.getPaintProperty('background', 'background-color'), 'red'); t.end(); }); }); diff --git a/test/js/util/util.test.js b/test/js/util/util.test.js index d02e6662e46..7eaec7fcd46 100644 --- a/test/js/util/util.test.js +++ b/test/js/util/util.test.js @@ -240,6 +240,23 @@ test('util', function(t) { }, that), {map: 'BOX'}); }); + t.test('filterObject', function(t) { + t.plan(6); + t.deepEqual(util.filterObject({}, function() { t.ok(false); }), {}); + var that = {}; + util.filterObject({map: 'box'}, function(value, key, object) { + t.equal(value, 'box'); + t.equal(key, 'map'); + t.deepEqual(object, {map: 'box'}); + t.equal(this, that); + return true; + }, that); + t.deepEqual(util.filterObject({map: 'box', box: 'map'}, function(value) { + return value === 'box'; + }), {map: 'box'}); + t.end(); + }); + if (process.browser) { t.test('timed: no duration', function(t) { var context = { foo: 'bar' };