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');