From 317c921b4a74950030bab1b7ad0ee85c05e5e8b5 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Tue, 18 Oct 2016 07:26:12 -0700 Subject: [PATCH] Retain correct layer order throughout the layout process --- js/source/geojson_worker_source.js | 6 +- js/source/source.js | 4 +- js/source/vector_tile_worker_source.js | 8 +- js/source/worker.js | 74 +++------------- js/source/worker_tile.js | 6 +- js/style/style_layer_index.js | 78 ++++++++++++++++ package.json | 2 +- .../source/vector_tile_worker_source.test.js | 14 ++- test/js/source/worker.test.js | 84 ++---------------- test/js/source/worker_tile.test.js | 88 +++++++------------ test/js/style/style_layer_index.test.js | 55 ++++++++++++ 11 files changed, 201 insertions(+), 218 deletions(-) create mode 100644 js/style/style_layer_index.js create mode 100644 test/js/style/style_layer_index.test.js diff --git a/js/source/geojson_worker_source.js b/js/source/geojson_worker_source.js index 9ce04e5d059..7aca58dcf9f 100644 --- a/js/source/geojson_worker_source.js +++ b/js/source/geojson_worker_source.js @@ -17,15 +17,15 @@ module.exports = GeoJSONWorkerSource; * This class is designed to be easily reused to support custom source types * for data formats that can be parsed/converted into an in-memory GeoJSON * representation. To do so, create it with - * `new GeoJSONWorkerSource(actor, styleLayers, customLoadGeoJSONFunction)`. For a full example, see [mapbox-gl-topojson](https://github.com/developmentseed/mapbox-gl-topojson). + * `new GeoJSONWorkerSource(actor, layerIndex, customLoadGeoJSONFunction)`. For a full example, see [mapbox-gl-topojson](https://github.com/developmentseed/mapbox-gl-topojson). * * @class GeoJSONWorkerSource * @private * @param {Function} [loadGeoJSON] Optional method for custom loading/parsing of GeoJSON based on parameters passed from the main-thread Source. See {@link GeoJSONWorkerSource#loadGeoJSON}. */ -function GeoJSONWorkerSource (actor, styleLayers, loadGeoJSON) { +function GeoJSONWorkerSource (actor, layerIndex, loadGeoJSON) { if (loadGeoJSON) { this.loadGeoJSON = loadGeoJSON; } - VectorTileWorkerSource.call(this, actor, styleLayers); + VectorTileWorkerSource.call(this, actor, layerIndex); } GeoJSONWorkerSource.prototype = util.inherit(VectorTileWorkerSource, /** @lends GeoJSONWorkerSource.prototype */ { diff --git a/js/source/source.js b/js/source/source.js index 29a5ea954e2..4a209b27c3f 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -122,9 +122,7 @@ exports.setType = function (name, type) { * * @class WorkerSource * @param {Actor} actor - * @param {object} styleLayers An accessor provided by the Worker to get the current style layers and layer families. - * @param {Function} styleLayers.getLayers - * @param {Function} styleLayers.getLayerFamilies + * @param {StyleLayerIndex} layerIndex */ /** diff --git a/js/source/vector_tile_worker_source.js b/js/source/vector_tile_worker_source.js index 17f9c10e751..31ed3d52f17 100644 --- a/js/source/vector_tile_worker_source.js +++ b/js/source/vector_tile_worker_source.js @@ -18,9 +18,9 @@ module.exports = VectorTileWorkerSource; * @private * @param {Function} [loadVectorData] Optional method for custom loading of a VectorTile object based on parameters passed from the main-thread Source. See {@link VectorTileWorkerSource#loadTile}. The default implementation simply loads the pbf at `params.url`. */ -function VectorTileWorkerSource (actor, styleLayers, loadVectorData) { +function VectorTileWorkerSource (actor, layerIndex, loadVectorData) { this.actor = actor; - this.styleLayers = styleLayers; + this.layerIndex = layerIndex; if (loadVectorData) { this.loadVectorData = loadVectorData; } @@ -59,7 +59,7 @@ VectorTileWorkerSource.prototype = { if (!vectorTile) return callback(null, null); workerTile.vectorTile = vectorTile; - workerTile.parse(vectorTile, this.styleLayers.getLayerFamilies(), this.actor, (err, result, transferrables) => { + workerTile.parse(vectorTile, this.layerIndex.families, this.actor, (err, result, transferrables) => { if (err) return callback(err); // Not transferring rawTileData because the worker needs to retain its copy. @@ -85,7 +85,7 @@ VectorTileWorkerSource.prototype = { uid = params.uid; if (loaded && loaded[uid]) { const workerTile = loaded[uid]; - workerTile.parse(workerTile.vectorTile, this.styleLayers.getLayerFamilies(), this.actor, callback); + workerTile.parse(workerTile.vectorTile, this.layerIndex.families, this.actor, callback); } }, diff --git a/js/source/worker.js b/js/source/worker.js index 4a5ed16f890..086bed385d1 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -1,12 +1,11 @@ 'use strict'; const Actor = require('../util/actor'); -const StyleLayer = require('../style/style_layer'); +const StyleLayerIndex = require('../style/style_layer_index'); const util = require('../util/util'); const VectorTileWorkerSource = require('./vector_tile_worker_source'); const GeoJSONWorkerSource = require('./geojson_worker_source'); -const featureFilter = require('feature-filter'); const assert = require('assert'); module.exports = function createWorker(self) { @@ -17,8 +16,7 @@ function Worker(self) { this.self = self; this.actor = new Actor(self, this); - this.layers = {}; - this.layerFamilies = {}; + this.layerIndexes = {}; this.workerSourceTypes = { vector: VectorTileWorkerSource, @@ -38,38 +36,11 @@ function Worker(self) { util.extend(Worker.prototype, { 'set layers': function(mapId, layerDefinitions) { - this.layers[mapId] = {}; - this['update layers'](mapId, layerDefinitions); + this.getLayerIndex(mapId).replace(layerDefinitions); }, 'update layers': function(mapId, layerDefinitions) { - const layers = this.layers[mapId]; - - // Update ref parents - for (const layer of layerDefinitions) { - if (!layer.ref) updateLayer(layer); - } - - // Update ref children - for (const layer of layerDefinitions) { - if (layer.ref) updateLayer(layer); - } - - function updateLayer(layer) { - if (layer.type !== 'fill' && layer.type !== 'line' && layer.type !== 'circle' && layer.type !== 'symbol') - return; - const refLayer = layer.ref && layers[layer.ref]; - let styleLayer = layers[layer.id]; - if (styleLayer) { - styleLayer.set(layer, refLayer); - } else { - styleLayer = layers[layer.id] = StyleLayer.create(layer, refLayer); - } - styleLayer.updatePaintTransitions({}, {transition: false}); - styleLayer.filter = featureFilter(styleLayer.filter); - } - - this.layerFamilies[mapId] = createLayerFamilies(this.layers[mapId]); + this.getLayerIndex(mapId).update(layerDefinitions); }, 'load tile': function(mapId, params, callback) { @@ -112,16 +83,18 @@ util.extend(Worker.prototype, { } }, + getLayerIndex: function(mapId) { + let layerIndexes = this.layerIndexes[mapId]; + if (!layerIndexes) { + layerIndexes = this.layerIndexes[mapId] = new StyleLayerIndex(); + } + return layerIndexes; + }, + getWorkerSource: function(mapId, type) { if (!this.workerSources[mapId]) this.workerSources[mapId] = {}; if (!this.workerSources[mapId][type]) { - // simple accessor object for passing to WorkerSources - const layers = { - getLayers: () => this.layers[mapId], - getLayerFamilies: () => this.layerFamilies[mapId] - }; - // use a wrapped actor so that we can attach a target mapId param // to any messages invoked by the WorkerSource const actor = { @@ -130,30 +103,9 @@ util.extend(Worker.prototype, { } }; - this.workerSources[mapId][type] = new this.workerSourceTypes[type](actor, layers); + this.workerSources[mapId][type] = new this.workerSourceTypes[type](actor, this.getLayerIndex(mapId)); } return this.workerSources[mapId][type]; } }); - -function createLayerFamilies(layers) { - const families = {}; - - for (const layerId in layers) { - const layer = layers[layerId]; - const parentLayerId = layer.ref || layer.id; - const parentLayer = layers[parentLayerId]; - - if (parentLayer.layout && parentLayer.layout.visibility === 'none') continue; - - families[parentLayerId] = families[parentLayerId] || []; - if (layerId === parentLayerId) { - families[parentLayerId].unshift(layer); - } else { - families[parentLayerId].push(layer); - } - } - - return families; -} diff --git a/js/source/worker_tile.js b/js/source/worker_tile.js index 9e16a79e041..592d2cc63da 100644 --- a/js/source/worker_tile.js +++ b/js/source/worker_tile.js @@ -47,8 +47,8 @@ WorkerTile.prototype.parse = function(data, layerFamilies, actor, callback) { // Map non-ref layers to buckets. let bucketIndex = 0; - for (const layerId in layerFamilies) { - const layer = layerFamilies[layerId][0]; + for (const family of layerFamilies) { + const layer = family[0]; const sourceLayerId = layer.sourceLayer || '_geojsonTileLayer'; assert(!layer.ref); @@ -62,7 +62,7 @@ WorkerTile.prototype.parse = function(data, layerFamilies, actor, callback) { const bucket = Bucket.create({ layer: layer, index: bucketIndex++, - childLayers: layerFamilies[layerId], + childLayers: family, zoom: this.zoom, overscaling: this.overscaling, showCollisionBoxes: this.showCollisionBoxes, diff --git a/js/style/style_layer_index.js b/js/style/style_layer_index.js new file mode 100644 index 00000000000..b57946f123a --- /dev/null +++ b/js/style/style_layer_index.js @@ -0,0 +1,78 @@ +'use strict'; + +const StyleLayer = require('./style_layer'); +const featureFilter = require('feature-filter'); + +class StyleLayerIndex { + constructor(layers) { + this.families = []; + if (layers) { + this.replace(layers); + } + } + + replace(layers) { + this._layers = {}; + this._order = []; + this.update(layers); + } + + _updateLayer(layer) { + const refLayer = layer.ref && this._layers[layer.ref]; + + let styleLayer = this._layers[layer.id]; + if (styleLayer) { + styleLayer.set(layer, refLayer); + } else { + styleLayer = this._layers[layer.id] = StyleLayer.create(layer, refLayer); + } + + styleLayer.updatePaintTransitions({}, {transition: false}); + styleLayer.filter = featureFilter(styleLayer.filter); + } + + update(layers) { + for (const layer of layers) { + if (!this._layers[layer.id]) { + this._order.push(layer.id); + } + } + + // Update ref parents + for (const layer of layers) { + if (!layer.ref) this._updateLayer(layer); + } + + // Update ref children + for (const layer of layers) { + if (layer.ref) this._updateLayer(layer); + } + + this.families = []; + const byParent = {}; + + for (const id of this._order) { + const layer = this._layers[id]; + const parent = layer.ref ? this._layers[layer.ref] : layer; + + if (parent.layout && parent.layout.visibility === 'none') { + continue; + } + + let family = byParent[parent.id]; + if (!family) { + family = []; + this.families.push(family); + byParent[parent.id] = family; + } + + if (layer.ref) { + family.push(layer); + } else { + family.unshift(layer); + } + } + } +} + +module.exports = StyleLayerIndex; diff --git a/package.json b/package.json index 7c2150cadd9..f1c4278121f 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "jsdom": "^9.4.2", "json-loader": "^0.5.4", "lodash": "^4.13.1", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#8db35f9130bce27102c5867d6542c42c074d9bfc", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#119551708b3621fb1e7066347730b1ea449ff8d6", "memory-fs": "^0.3.0", "minifyify": "^7.0.1", "npm-run-all": "^3.0.0", diff --git a/test/js/source/vector_tile_worker_source.test.js b/test/js/source/vector_tile_worker_source.test.js index b4cff258eef..ab5cbb37e5a 100644 --- a/test/js/source/vector_tile_worker_source.test.js +++ b/test/js/source/vector_tile_worker_source.test.js @@ -2,15 +2,11 @@ const test = require('mapbox-gl-js-test').test; const VectorTileWorkerSource = require('../../../js/source/vector_tile_worker_source'); - -const styleLayers = { - getLayers: function () {}, - getLayerFamilies: function () {} -}; +const StyleLayerIndex = require('../../../js/style/style_layer_index'); test('abortTile', (t) => { t.test('aborts pending request', (t) => { - const source = new VectorTileWorkerSource(null, styleLayers); + const source = new VectorTileWorkerSource(null, new StyleLayerIndex()); source.loadTile({ source: 'source', @@ -32,7 +28,7 @@ test('abortTile', (t) => { test('removeTile', (t) => { t.test('removes loaded tile', (t) => { - const source = new VectorTileWorkerSource(null, styleLayers); + const source = new VectorTileWorkerSource(null, new StyleLayerIndex()); source.loaded = { source: { @@ -55,7 +51,7 @@ test('removeTile', (t) => { test('redoPlacement', (t) => { t.test('on loaded tile', (t) => { - const source = new VectorTileWorkerSource(null, styleLayers); + const source = new VectorTileWorkerSource(null, new StyleLayerIndex()); const tile = { redoPlacement: function(angle, pitch, showCollisionBoxes) { t.equal(angle, 60); @@ -84,7 +80,7 @@ test('redoPlacement', (t) => { }); t.test('on loading tile', (t) => { - const source = new VectorTileWorkerSource(null, styleLayers); + const source = new VectorTileWorkerSource(null, new StyleLayerIndex()); const tile = {}; source.loading = {mapbox: {3: tile}}; diff --git a/test/js/source/worker.test.js b/test/js/source/worker.test.js index 74dadbd31d7..37e7b6ea6d7 100644 --- a/test/js/source/worker.test.js +++ b/test/js/source/worker.test.js @@ -1,7 +1,5 @@ 'use strict'; -/* jshint -W079 */ - const test = require('mapbox-gl-js-test').test; const Worker = require('../../../js/source/worker'); const window = require('../../../js/util/window'); @@ -30,55 +28,6 @@ test('load tile', (t) => { t.end(); }); -test('set layers', (t) => { - const worker = new Worker(_self); - - worker['set layers'](0, [ - { id: 'one', type: 'circle', paint: { 'circle-color': 'red' } }, - { id: 'two', type: 'circle', paint: { 'circle-color': 'green' } }, - { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } } - ]); - - t.equal(worker.layers[0].one.id, 'one'); - t.equal(worker.layers[0].two.id, 'two'); - t.equal(worker.layers[0].three.id, 'three'); - - t.equal(worker.layers[0].one.getPaintProperty('circle-color'), 'red'); - t.equal(worker.layers[0].two.getPaintProperty('circle-color'), 'green'); - t.equal(worker.layers[0].three.getPaintProperty('circle-color'), 'blue'); - - t.equal(worker.layerFamilies[0].one.length, 1); - t.equal(worker.layerFamilies[0].one[0].id, 'one'); - t.equal(worker.layerFamilies[0].two.length, 2); - t.equal(worker.layerFamilies[0].two[0].id, 'two'); - t.equal(worker.layerFamilies[0].two[1].id, 'three'); - - t.end(); -}); - -test('update layers', (t) => { - const worker = new Worker(_self); - - worker['set layers'](0, [ - { id: 'one', type: 'circle', paint: { 'circle-color': 'red' }, 'source': 'foo' }, - { id: 'two', type: 'circle', paint: { 'circle-color': 'green' }, 'source': 'foo' }, - { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } } - ]); - - worker['update layers'](0, [ - { id: 'one', type: 'circle', paint: { 'circle-color': 'cyan' }, 'source': 'bar' }, - { id: 'two', type: 'circle', paint: { 'circle-color': 'magenta' }, 'source': 'bar' }, - { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'yellow' } } - ]); - - t.equal(worker.layers[0].one.getPaintProperty('circle-color'), 'cyan'); - t.equal(worker.layers[0].two.getPaintProperty('circle-color'), 'magenta'); - t.equal(worker.layers[0].three.getPaintProperty('circle-color'), 'yellow'); - t.equal(worker.layers[0].three.source, 'bar'); - - t.end(); -}); - test('redo placement', (t) => { const worker = new Worker(_self); _self.registerWorkerSource('test', function() { @@ -91,41 +40,20 @@ test('redo placement', (t) => { worker['redo placement'](0, {type: 'test', mapbox: true}); }); -test('update layers isolates different instances\' data', (t) => { +test('isolates different instances\' data', (t) => { const worker = new Worker(_self); worker['set layers'](0, [ - { id: 'one', type: 'circle', paint: { 'circle-color': 'red' } }, - { id: 'two', type: 'circle', paint: { 'circle-color': 'green' } }, - { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } } + { id: 'one', type: 'circle' } ]); worker['set layers'](1, [ - { id: 'one', type: 'circle', paint: { 'circle-color': 'red' } }, - { id: 'two', type: 'circle', paint: { 'circle-color': 'green' } }, - { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } } - ]); - - worker['update layers'](1, [ - { id: 'one', type: 'circle', paint: { 'circle-color': 'cyan' } }, - { id: 'two', type: 'circle', paint: { 'circle-color': 'magenta' } }, - { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'yellow' } } + { id: 'one', type: 'circle' }, + { id: 'two', type: 'circle' }, ]); - t.equal(worker.layers[0].one.id, 'one'); - t.equal(worker.layers[0].two.id, 'two'); - t.equal(worker.layers[0].three.id, 'three'); - - t.equal(worker.layers[0].one.getPaintProperty('circle-color'), 'red'); - t.equal(worker.layers[0].two.getPaintProperty('circle-color'), 'green'); - t.equal(worker.layers[0].three.getPaintProperty('circle-color'), 'blue'); - - t.equal(worker.layerFamilies[0].one.length, 1); - t.equal(worker.layerFamilies[0].one[0].id, 'one'); - t.equal(worker.layerFamilies[0].two.length, 2); - t.equal(worker.layerFamilies[0].two[0].id, 'two'); - t.equal(worker.layerFamilies[0].two[1].id, 'three'); - + t.equal(worker.layerIndexes[0].families.length, 1); + t.equal(worker.layerIndexes[1].families.length, 2); t.end(); }); diff --git a/test/js/source/worker_tile.test.js b/test/js/source/worker_tile.test.js index c57c0601aa5..262af2dc657 100644 --- a/test/js/source/worker_tile.test.js +++ b/test/js/source/worker_tile.test.js @@ -4,9 +4,8 @@ const test = require('mapbox-gl-js-test').test; const WorkerTile = require('../../../js/source/worker_tile'); const Wrapper = require('../../../js/source/geojson_wrapper'); const TileCoord = require('../../../js/source/tile_coord'); -const StyleLayer = require('../../../js/style/style_layer'); +const StyleLayerIndex = require('../../../js/style/style_layer_index'); const util = require('../../../js/util/util'); -const featureFilter = require('feature-filter'); function createWorkerTile() { return new WorkerTile({ @@ -29,19 +28,14 @@ function createWrapper() { } test('WorkerTile#parse', (t) => { - const layerFamilies = { - test: [new StyleLayer({ - id: 'test', - source: 'source', - type: 'circle', - layout: {}, - compare: function () { return true; }, - filter: featureFilter() - })] - }; + const layerIndex = new StyleLayerIndex([{ + id: 'test', + source: 'source', + type: 'circle' + }]); const tile = createWorkerTile(); - tile.parse(createWrapper(), layerFamilies, {}, (err, result) => { + tile.parse(createWrapper(), layerIndex.families, {}, (err, result) => { t.ifError(err); t.ok(result.buckets[0]); t.end(); @@ -49,27 +43,19 @@ test('WorkerTile#parse', (t) => { }); test('WorkerTile#parse skips hidden layers', (t) => { - const layerFamilies = { - 'test': [new StyleLayer({ - id: 'test', - source: 'source', - type: 'circle', - layout: {}, - compare: function () { return true; }, - filter: featureFilter() - })], - 'test-hidden': [new StyleLayer({ - id: 'test-hidden', - source: 'source', - type: 'fill', - layout: { visibility: 'none' }, - compare: function () { return true; }, - filter: featureFilter() - })] - }; + const layerIndex = new StyleLayerIndex([{ + id: 'test', + source: 'source', + type: 'circle' + }, { + id: 'test-hidden', + source: 'source', + type: 'fill', + layout: { visibility: 'none' } + }]); const tile = createWorkerTile(); - tile.parse(createWrapper(), layerFamilies, {}, (err, result) => { + tile.parse(createWrapper(), layerIndex.families, {}, (err, result) => { t.ifError(err); t.equal(Object.keys(result.buckets[0].arrays).length, 1); t.end(); @@ -77,20 +63,15 @@ test('WorkerTile#parse skips hidden layers', (t) => { }); test('WorkerTile#parse skips layers without a corresponding source layer', (t) => { - const layerFamilies = { - 'test-sourceless': [new StyleLayer({ - id: 'test', - source: 'source', - 'source-layer': 'nonesuch', - type: 'fill', - layout: {}, - compare: function () { return true; }, - filter: featureFilter() - })] - }; + const layerIndex = new StyleLayerIndex([{ + id: 'test', + source: 'source', + 'source-layer': 'nonesuch', + type: 'fill' + }]); const tile = createWorkerTile(); - tile.parse({layers: {}}, layerFamilies, {}, (err, result) => { + tile.parse({layers: {}}, layerIndex.families, {}, (err, result) => { t.ifError(err); t.equal(result.buckets.length, 0); t.end(); @@ -98,17 +79,12 @@ test('WorkerTile#parse skips layers without a corresponding source layer', (t) = }); test('WorkerTile#parse warns once when encountering a v1 vector tile layer', (t) => { - const layerFamilies = { - 'test': [new StyleLayer({ - id: 'test', - source: 'source', - 'source-layer': 'test', - type: 'fill', - layout: {}, - compare: function () { return true; }, - filter: featureFilter() - })] - }; + const layerIndex = new StyleLayerIndex([{ + id: 'test', + source: 'source', + 'source-layer': 'test', + type: 'fill' + }]); const data = { layers: { @@ -121,7 +97,7 @@ test('WorkerTile#parse warns once when encountering a v1 vector tile layer', (t) t.stub(util, 'warnOnce'); const tile = createWorkerTile(); - tile.parse(data, layerFamilies, {}, (err) => { + tile.parse(data, layerIndex.families, {}, (err) => { t.ifError(err); t.ok(util.warnOnce.calledWithMatch(/does not use vector tile spec v2/)); t.end(); diff --git a/test/js/style/style_layer_index.test.js b/test/js/style/style_layer_index.test.js new file mode 100644 index 00000000000..5cd306f3be8 --- /dev/null +++ b/test/js/style/style_layer_index.test.js @@ -0,0 +1,55 @@ +'use strict'; + +const test = require('mapbox-gl-js-test').test; +const StyleLayerIndex = require('../../../js/style/style_layer_index'); + +test('StyleLayerIndex', (t) => { + const index = new StyleLayerIndex(); + t.deepEqual(index.families, []); + t.end(); +}); + +test('StyleLayerIndex#replace', (t) => { + const index = new StyleLayerIndex([ + { id: 'one', type: 'circle', paint: { 'circle-color': 'red' } }, + { id: 'two', type: 'circle', paint: { 'circle-color': 'green' } }, + { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } } + ]); + + t.equal(index.families.length, 2); + t.equal(index.families[0].length, 1); + t.equal(index.families[0][0].id, 'one'); + t.equal(index.families[1].length, 2); + t.equal(index.families[1][0].id, 'two'); + t.equal(index.families[1][1].id, 'three'); + + index.replace([]); + t.deepEqual(index.families, []); + + t.end(); +}); + +test('StyleLayerIndex#update', (t) => { + const index = new StyleLayerIndex([ + { id: 'one', type: 'circle', paint: { 'circle-color': 'red' }, 'source': 'foo' }, + { id: 'two', type: 'circle', paint: { 'circle-color': 'green' }, 'source': 'foo' }, + { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'blue' } } + ]); + + index.update([ + { id: 'one', type: 'circle', paint: { 'circle-color': 'cyan' }, 'source': 'bar' }, + { id: 'two', type: 'circle', paint: { 'circle-color': 'magenta' }, 'source': 'bar' }, + { id: 'three', ref: 'two', type: 'circle', paint: { 'circle-color': 'yellow' } } + ]); + + t.equal(index.families.length, 2); + t.equal(index.families[0].length, 1); + t.equal(index.families[0][0].getPaintProperty('circle-color'), 'cyan'); + t.equal(index.families[1].length, 2); + t.equal(index.families[1][0].getPaintProperty('circle-color'), 'magenta'); + t.equal(index.families[1][0].source, 'bar'); + t.equal(index.families[1][1].getPaintProperty('circle-color'), 'yellow'); + t.equal(index.families[1][1].source, 'bar'); + + t.end(); +});