diff --git a/examples/layer-z-index.html b/examples/layer-z-index.html new file mode 100644 index 0000000000..b61ea00d03 --- /dev/null +++ b/examples/layer-z-index.html @@ -0,0 +1,28 @@ +--- +template: example.html +title: Z-index layer ordering example +shortdesc: Example of ordering layers using Z-index. +docs: > + +tags: "layer, ordering, z-index" +--- +
+
+
+
+
+
+ There are are two managed layers (square and triangle) and one unmanaged layer (star).
+ The Z-index determines the rendering order; with {square: 1, triangle: 0, star: unmanaged} indices, the rendering order is triangle, square and star on top. +
+
+
+ + +
diff --git a/examples/layer-z-index.js b/examples/layer-z-index.js new file mode 100644 index 0000000000..9aeed15bc2 --- /dev/null +++ b/examples/layer-z-index.js @@ -0,0 +1,92 @@ +goog.require('ol.Feature'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.geom.Point'); +goog.require('ol.layer.Vector'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Fill'); +goog.require('ol.style.RegularShape'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); + + +var stroke = new ol.style.Stroke({color: 'black', width: 1}); + +var styles = { + 'square': [new ol.style.Style({ + image: new ol.style.RegularShape({ + fill: new ol.style.Fill({color: 'blue'}), + stroke: stroke, + points: 4, + radius: 80, + angle: Math.PI / 4 + }) + })], + 'triangle': [new ol.style.Style({ + image: new ol.style.RegularShape({ + fill: new ol.style.Fill({color: 'red'}), + stroke: stroke, + points: 3, + radius: 80, + rotation: Math.PI / 4, + angle: 0 + }) + })], + 'star': [new ol.style.Style({ + image: new ol.style.RegularShape({ + fill: new ol.style.Fill({color: 'green'}), + stroke: stroke, + points: 5, + radius: 80, + radius2: 4, + angle: 0 + }) + })] +}; + + +function createLayer(coordinates, styles, zIndex) { + var feature = new ol.Feature(new ol.geom.Point(coordinates)); + feature.setStyle(styles); + + var source = new ol.source.Vector({ + features: [feature] + }); + + var vectorLayer = new ol.layer.Vector({ + source: source + }); + vectorLayer.setZIndex(zIndex); + + return vectorLayer; +} + +var layer0 = createLayer([40, 40], styles['star'], 0); +var layer1 = createLayer([0, 0], styles['square'], 1); +var layer2 = createLayer([0, 40], styles['triangle'], 0); + +var layers = []; +layers.push(layer1); +layers.push(layer2); + +var map = new ol.Map({ + layers: layers, + target: 'map', + view: new ol.View({ + center: [0, 0], + zoom: 18 + }) +}); + +layer0.setMap(map); + + +function bindInputs(id, layer) { + var idxInput = $('#idx' + id); + idxInput.on('input change', function() { + layer.setZIndex(parseInt(this.value, 10) || 0); + }); + idxInput.val(String(layer.getZIndex())); +} +bindInputs(1, layer1); +bindInputs(2, layer2); diff --git a/externs/olx.js b/externs/olx.js index cf5ba38d6e..343a750975 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -2927,6 +2927,7 @@ olx.layer; * saturation: (number|undefined), * visible: (boolean|undefined), * extent: (ol.Extent|undefined), + * zIndex: (number|undefined), * minResolution: (number|undefined), * maxResolution: (number|undefined)}} * @api @@ -2991,6 +2992,15 @@ olx.layer.BaseOptions.prototype.visible; olx.layer.BaseOptions.prototype.extent; +/** + * The z-index for layer rendering. At rendering time, the layers will be + * ordered, first by Z-index and then by position. The default Z-index is 0. + * @type {number|undefined} + * @api + */ +olx.layer.BaseOptions.prototype.zIndex; + + /** * The minimum resolution (inclusive) at which this layer will be visible. * @type {number|undefined} @@ -3016,6 +3026,7 @@ olx.layer.BaseOptions.prototype.maxResolution; * source: (ol.source.Source|undefined), * visible: (boolean|undefined), * extent: (ol.Extent|undefined), + * zIndex: (number|undefined), * minResolution: (number|undefined), * maxResolution: (number|undefined)}} * @api @@ -3090,6 +3101,15 @@ olx.layer.LayerOptions.prototype.visible; olx.layer.LayerOptions.prototype.extent; +/** + * The z-index for layer rendering. At rendering time, the layers will be + * ordered, first by Z-index and then by position. The default Z-index is 0. + * @type {number|undefined} + * @api + */ +olx.layer.LayerOptions.prototype.zIndex; + + /** * The minimum resolution (inclusive) at which this layer will be visible. * @type {number|undefined} @@ -3114,6 +3134,7 @@ olx.layer.LayerOptions.prototype.maxResolution; * saturation: (number|undefined), * visible: (boolean|undefined), * extent: (ol.Extent|undefined), + * zIndex: (number|undefined), * minResolution: (number|undefined), * maxResolution: (number|undefined), * layers: (Array.|ol.Collection.|undefined)}} @@ -3179,6 +3200,15 @@ olx.layer.GroupOptions.prototype.visible; olx.layer.GroupOptions.prototype.extent; +/** + * The z-index for layer rendering. At rendering time, the layers will be + * ordered, first by Z-index and then by position. The default Z-index is 0. + * @type {number|undefined} + * @api + */ +olx.layer.GroupOptions.prototype.zIndex; + + /** * The minimum resolution (inclusive) at which this layer will be visible. * @type {number|undefined} diff --git a/src/ol/layer/layer.js b/src/ol/layer/layer.js index 40e7052efd..9d646e4e23 100644 --- a/src/ol/layer/layer.js +++ b/src/ol/layer/layer.js @@ -172,6 +172,7 @@ ol.layer.Layer.prototype.setMap = function(map) { map, ol.render.EventType.PRECOMPOSE, function(evt) { var layerState = this.getLayerState(); layerState.managed = false; + layerState.zIndex = Infinity; evt.frameState.layerStatesArray.push(layerState); evt.frameState.layerStates[goog.getUid(this)] = layerState; }, false, this); diff --git a/src/ol/layer/layerbase.js b/src/ol/layer/layerbase.js index f3bd14e2c4..4f5faff1aa 100644 --- a/src/ol/layer/layerbase.js +++ b/src/ol/layer/layerbase.js @@ -19,6 +19,7 @@ ol.layer.LayerProperty = { SATURATION: 'saturation', VISIBLE: 'visible', EXTENT: 'extent', + Z_INDEX: 'zIndex', MAX_RESOLUTION: 'maxResolution', MIN_RESOLUTION: 'minResolution', SOURCE: 'source' @@ -36,6 +37,7 @@ ol.layer.LayerProperty = { * visible: boolean, * managed: boolean, * extent: (ol.Extent|undefined), + * zIndex: number, * maxResolution: number, * minResolution: number}} */ @@ -76,6 +78,8 @@ ol.layer.Base = function(options) { goog.isDef(options.saturation) ? options.saturation : 1; properties[ol.layer.LayerProperty.VISIBLE] = goog.isDef(options.visible) ? options.visible : true; + properties[ol.layer.LayerProperty.Z_INDEX] = + goog.isDef(options.zIndex) ? options.zIndex : 0; properties[ol.layer.LayerProperty.MAX_RESOLUTION] = goog.isDef(options.maxResolution) ? options.maxResolution : Infinity; properties[ol.layer.LayerProperty.MIN_RESOLUTION] = @@ -131,6 +135,7 @@ ol.layer.Base.prototype.getLayerState = function() { var sourceState = this.getSourceState(); var visible = this.getVisible(); var extent = this.getExtent(); + var zIndex = this.getZIndex(); var maxResolution = this.getMaxResolution(); var minResolution = this.getMinResolution(); return { @@ -144,6 +149,7 @@ ol.layer.Base.prototype.getLayerState = function() { visible: visible, managed: true, extent: extent, + zIndex: zIndex, maxResolution: maxResolution, minResolution: Math.max(minResolution, 0) }; @@ -242,6 +248,18 @@ ol.layer.Base.prototype.getVisible = function() { }; +/** + * Return the Z-index of the layer, which is used to order layers before + * rendering. The default Z-index is 0. + * @return {number} The Z-index of the layer. + * @observable + * @api + */ +ol.layer.Base.prototype.getZIndex = function() { + return /** @type {number} */ (this.get(ol.layer.LayerProperty.Z_INDEX)); +}; + + /** * Adjust the layer brightness. A value of -1 will render the layer completely * black. A value of 0 will leave the brightness unchanged. A value of 1 will @@ -364,3 +382,15 @@ ol.layer.Base.prototype.setSaturation = function(saturation) { ol.layer.Base.prototype.setVisible = function(visible) { this.set(ol.layer.LayerProperty.VISIBLE, visible); }; + + +/** + * Set Z-index of the layer, which is used to order layers before rendering. + * The default Z-index is 0. + * @param {number} zindex The z-index of the layer. + * @observable + * @api + */ +ol.layer.Base.prototype.setZIndex = function(zindex) { + this.set(ol.layer.LayerProperty.Z_INDEX, zindex); +}; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index 609dc09c02..edefe257ad 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -2,6 +2,7 @@ goog.provide('ol.renderer.canvas.Map'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.style'); @@ -168,6 +169,8 @@ ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) { this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState); var layerStatesArray = frameState.layerStatesArray; + goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex); + var viewResolution = frameState.viewState.resolution; var i, ii, layer, layerRenderer, layerState; for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { diff --git a/src/ol/renderer/dom/dommaprenderer.js b/src/ol/renderer/dom/dommaprenderer.js index bbe8e4e9ca..fd471785d8 100644 --- a/src/ol/renderer/dom/dommaprenderer.js +++ b/src/ol/renderer/dom/dommaprenderer.js @@ -1,5 +1,6 @@ goog.provide('ol.renderer.dom.Map'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); @@ -222,6 +223,8 @@ ol.renderer.dom.Map.prototype.renderFrame = function(frameState) { this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState); var layerStatesArray = frameState.layerStatesArray; + goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex); + var viewResolution = frameState.viewState.resolution; var i, ii, layer, layerRenderer, layerState; for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { diff --git a/src/ol/renderer/maprenderer.js b/src/ol/renderer/maprenderer.js index b2d71c6e83..b65f32ad7a 100644 --- a/src/ol/renderer/maprenderer.js +++ b/src/ol/renderer/maprenderer.js @@ -375,3 +375,13 @@ ol.renderer.Map.prototype.scheduleRemoveUnusedLayerRenderers = } } }; + + +/** + * @param {ol.layer.LayerState} state1 + * @param {ol.layer.LayerState} state2 + * @return {number} + */ +ol.renderer.Map.sortByZIndex = function(state1, state2) { + return state1.zIndex - state2.zIndex; +}; diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index 5dd63e3d3b..2a37a4d553 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -2,6 +2,7 @@ goog.provide('ol.renderer.webgl.Map'); +goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); @@ -471,6 +472,8 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { /** @type {Array.} */ var layerStatesToDraw = []; var layerStatesArray = frameState.layerStatesArray; + goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex); + var viewResolution = frameState.viewState.resolution; var i, ii, layerRenderer, layerState; for (i = 0, ii = layerStatesArray.length; i < ii; ++i) { diff --git a/test/spec/ol/layer/layer.test.js b/test/spec/ol/layer/layer.test.js index 72e1f12b84..a5618b8165 100644 --- a/test/spec/ol/layer/layer.test.js +++ b/test/spec/ol/layer/layer.test.js @@ -66,6 +66,7 @@ describe('ol.layer.Layer', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: Infinity, minResolution: 0 }); @@ -86,6 +87,7 @@ describe('ol.layer.Layer', function() { opacity: 0.5, saturation: 5, visible: false, + zIndex: 10, maxResolution: 500, minResolution: 0.25, foo: 42 @@ -111,6 +113,7 @@ describe('ol.layer.Layer', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 10, maxResolution: 500, minResolution: 0.25 }); @@ -194,6 +197,7 @@ describe('ol.layer.Layer', function() { layer.setVisible(false); layer.setMaxResolution(500); layer.setMinResolution(0.25); + layer.setZIndex(10); expect(layer.getLayerState()).to.eql({ layer: layer, brightness: -0.7, @@ -205,6 +209,7 @@ describe('ol.layer.Layer', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 10, maxResolution: 500, minResolution: 0.25 }); @@ -228,6 +233,7 @@ describe('ol.layer.Layer', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: Infinity, minResolution: 0 }); @@ -249,6 +255,7 @@ describe('ol.layer.Layer', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: Infinity, minResolution: 0 }); diff --git a/test/spec/ol/layer/layergroup.test.js b/test/spec/ol/layer/layergroup.test.js index 061fda329f..7cc123524c 100644 --- a/test/spec/ol/layer/layergroup.test.js +++ b/test/spec/ol/layer/layergroup.test.js @@ -54,6 +54,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: Infinity, minResolution: 0 }); @@ -160,6 +161,7 @@ describe('ol.layer.Group', function() { opacity: 0.5, saturation: 5, visible: false, + zIndex: 10, maxResolution: 500, minResolution: 0.25 }); @@ -183,6 +185,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 10, maxResolution: 500, minResolution: 0.25 }); @@ -235,6 +238,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: groupExtent, + zIndex: 0, maxResolution: 500, minResolution: 0.25 }); @@ -266,6 +270,7 @@ describe('ol.layer.Group', function() { layerGroup.setOpacity(0.3); layerGroup.setSaturation(0.3); layerGroup.setVisible(false); + layerGroup.setZIndex(10); var groupExtent = [-100, 50, 100, 50]; layerGroup.setExtent(groupExtent); layerGroup.setMaxResolution(500); @@ -281,6 +286,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: groupExtent, + zIndex: 10, maxResolution: 500, minResolution: 0.25 }); @@ -304,6 +310,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: Infinity, minResolution: 0 }); @@ -325,6 +332,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: Infinity, minResolution: 0 }); @@ -500,6 +508,7 @@ describe('ol.layer.Group', function() { managed: true, sourceState: ol.source.State.READY, extent: undefined, + zIndex: 0, maxResolution: 150, minResolution: 0.25 }); @@ -507,14 +516,60 @@ describe('ol.layer.Group', function() { goog.dispose(layerGroup); }); + it('let order of layers without Z-index unchanged', function() { + var layerGroup = new ol.layer.Group({ + layers: [layer1, layer2] + }); + + var layerStatesArray = layerGroup.getLayerStatesArray(); + var initialArray = layerStatesArray.slice(); + goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex); + expect(layerStatesArray[0]).to.eql(initialArray[0]); + expect(layerStatesArray[1]).to.eql(initialArray[1]); + + goog.dispose(layerGroup); + }); + + it('orders layer with higher Z-index on top', function() { + var layer10 = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: 'EPSG:4326' + }) + }); + layer10.setZIndex(10); + + var layerM1 = new ol.layer.Layer({ + source: new ol.source.Source({ + projection: 'EPSG:4326' + }) + }); + layerM1.setZIndex(-1); + + var layerGroup = new ol.layer.Group({ + layers: [layer1, layer10, layer2, layerM1] + }); + + var layerStatesArray = layerGroup.getLayerStatesArray(); + var initialArray = layerStatesArray.slice(); + goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex); + expect(layerStatesArray[0]).to.eql(initialArray[3]); + expect(layerStatesArray[1]).to.eql(initialArray[0]); + expect(layerStatesArray[2]).to.eql(initialArray[2]); + expect(layerStatesArray[3]).to.eql(initialArray[1]); + + goog.dispose(layer10); + goog.dispose(layerM1); + goog.dispose(layerGroup); + }); + goog.dispose(layer1); goog.dispose(layer2); goog.dispose(layer3); - }); }); +goog.require('goog.array'); goog.require('goog.dispose'); goog.require('goog.events.EventType'); goog.require('goog.events.Listener'); @@ -523,6 +578,7 @@ goog.require('ol.ObjectEventType'); goog.require('ol.extent'); goog.require('ol.layer.Layer'); goog.require('ol.layer.Group'); +goog.require('ol.renderer.Map'); goog.require('ol.source.Source'); goog.require('ol.source.State'); goog.require('ol.Collection');