diff --git a/examples/data/Butterfly.png b/examples/data/Butterfly.png
new file mode 100644
index 00000000000..0a32d018ce2
Binary files /dev/null and b/examples/data/Butterfly.png differ
diff --git a/examples/draw-features.js b/examples/draw-features.js
index 59cc07984e0..dc66e2c1f70 100644
--- a/examples/draw-features.js
+++ b/examples/draw-features.js
@@ -38,6 +38,7 @@ var vector = new ol.layer.Vector({
var map = new ol.Map({
layers: [raster, vector],
+ renderer: exampleNS.getRendererFromQueryString(),
target: 'map',
view: new ol.View({
center: [-11000000, 4600000],
diff --git a/examples/dynamic-data.js b/examples/dynamic-data.js
index 00d87e840ea..793d4ef7615 100644
--- a/examples/dynamic-data.js
+++ b/examples/dynamic-data.js
@@ -1,11 +1,14 @@
+goog.require('ol.Feature');
goog.require('ol.Map');
goog.require('ol.View');
goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.Point');
goog.require('ol.layer.Tile');
goog.require('ol.source.MapQuest');
goog.require('ol.style.Circle');
goog.require('ol.style.Fill');
goog.require('ol.style.Stroke');
+goog.require('ol.style.Style');
var map = new ol.Map({
@@ -14,6 +17,7 @@ var map = new ol.Map({
source: new ol.source.MapQuest({layer: 'sat'})
})
],
+ renderer: exampleNS.getRendererFromQueryString(),
target: 'map',
view: new ol.View({
center: [0, 0],
@@ -28,6 +32,20 @@ var imageStyle = new ol.style.Circle({
stroke: new ol.style.Stroke({color: 'red', width: 1})
});
+var headInnerImageStyle = new ol.style.Style({
+ image: new ol.style.Circle({
+ radius: 2,
+ snapToPixel: false,
+ fill: new ol.style.Fill({color: 'blue'})
+ })
+});
+
+var headOuterImageStyle = new ol.style.Circle({
+ radius: 5,
+ snapToPixel: false,
+ fill: new ol.style.Fill({color: 'black'})
+});
+
var n = 200;
var omegaTheta = 30000; // Rotation period in ms
var R = 7e6;
@@ -48,6 +66,14 @@ map.on('postcompose', function(event) {
vectorContext.setImageStyle(imageStyle);
vectorContext.drawMultiPointGeometry(
new ol.geom.MultiPoint(coordinates), null);
+
+ var headPoint = new ol.geom.Point(coordinates[coordinates.length - 1]);
+ var headFeature = new ol.Feature(headPoint);
+ vectorContext.drawFeature(headFeature, headInnerImageStyle);
+
+ vectorContext.setImageStyle(headOuterImageStyle);
+ vectorContext.drawMultiPointGeometry(headPoint, null);
+
map.render();
});
map.render();
diff --git a/examples/icon-sprite-webgl.html b/examples/icon-sprite-webgl.html
new file mode 100644
index 00000000000..7f074097463
--- /dev/null
+++ b/examples/icon-sprite-webgl.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+ Icon sprites with WebGL example
+
+
+
+
+
+
+
+
+
+
+
+
+
Icon sprite with WebGL example
+
Icon sprite with WebGL.
+
+
See the icon-sprite-webgl.js source to see how this is done.
+
In this example a sprite image is used for the icon styles. Using a sprite is required to get good performance with WebGL.
+
+
webgl, icon, sprite, vector, point
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/icon-sprite-webgl.js b/examples/icon-sprite-webgl.js
new file mode 100644
index 00000000000..26933f30f13
--- /dev/null
+++ b/examples/icon-sprite-webgl.js
@@ -0,0 +1,111 @@
+goog.require('ol.Feature');
+goog.require('ol.FeatureOverlay');
+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.Icon');
+goog.require('ol.style.Style');
+
+
+var iconInfo = [{
+ offset: [0, 0],
+ opacity: 1.0,
+ rotateWithView: true,
+ rotation: 0.0,
+ scale: 1.0,
+ size: [55, 55]
+}, {
+ offset: [110, 86],
+ opacity: 0.75,
+ rotateWithView: false,
+ rotation: Math.PI / 2.0,
+ scale: 1.25,
+ size: [55, 55]
+}, {
+ offset: [55, 0],
+ opacity: 0.5,
+ rotateWithView: true,
+ rotation: Math.PI / 3.0,
+ scale: 1.5,
+ size: [55, 86]
+}, {
+ offset: [212, 0],
+ opacity: 1.0,
+ rotateWithView: true,
+ rotation: 0.0,
+ scale: 1.0,
+ size: [44, 44]
+}];
+
+var i;
+
+var iconCount = iconInfo.length;
+var icons = new Array(iconCount);
+for (i = 0; i < iconCount; ++i) {
+ var info = iconInfo[i];
+ icons[i] = new ol.style.Icon({
+ offset: info.offset,
+ opacity: info.opacity,
+ rotateWithView: info.rotateWithView,
+ rotation: info.rotation,
+ scale: info.scale,
+ size: info.size,
+ src: 'data/Butterfly.png'
+ });
+}
+
+var featureCount = 50000;
+var features = new Array(featureCount);
+var feature, geometry;
+var e = 25000000;
+for (i = 0; i < featureCount; ++i) {
+ geometry = new ol.geom.Point(
+ [2 * e * Math.random() - e, 2 * e * Math.random() - e]);
+ feature = new ol.Feature(geometry);
+ feature.setStyle(
+ new ol.style.Style({
+ image: icons[i % (iconCount - 1)]
+ })
+ );
+ features[i] = feature;
+}
+
+var vectorSource = new ol.source.Vector({
+ features: features
+});
+var vector = new ol.layer.Vector({
+ source: vectorSource
+});
+
+// Use the "webgl" renderer by default.
+var renderer = exampleNS.getRendererFromQueryString();
+if (!renderer) {
+ renderer = 'webgl';
+}
+
+var map = new ol.Map({
+ renderer: renderer,
+ layers: [vector],
+ target: document.getElementById('map'),
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 5
+ })
+});
+
+var overlayFeatures = [];
+for (i = 0; i < featureCount; i += 30) {
+ var clone = features[i].clone();
+ clone.setStyle(null);
+ overlayFeatures.push(clone);
+}
+
+var featureOverlay = new ol.FeatureOverlay({
+ map: map,
+ style: new ol.style.Style({
+ image: icons[iconCount - 1]
+ }),
+ features: overlayFeatures
+});
diff --git a/examples/symbol-atlas-webgl.html b/examples/symbol-atlas-webgl.html
new file mode 100644
index 00000000000..666bc43ab5b
--- /dev/null
+++ b/examples/symbol-atlas-webgl.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+ Symbols with WebGL example
+
+
+
+
+
+
+
+
+
+
+
+
+
Symbols with WebGL example
+
Using symbols in an atlas with WebGL.
+
+
When using symbol styles with WebGL, OpenLayers would render the symbol
+ on a temporary image and would create a WebGL texture for each image. For a
+ better performance, it is recommended to use atlas images (similar to
+ image sprites with CSS), so that the number of textures is reduced. OpenLayers
+ provides an AtlasManager
, which when passed to the constructor
+ of a symbol style, will create atlases for the symbols.
+
See the symbol-atlas-webgl.js source to see how this is done.
+
+
webgl, symbol, atlas, vector, point
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/symbol-atlas-webgl.js b/examples/symbol-atlas-webgl.js
new file mode 100644
index 00000000000..22423c2adf1
--- /dev/null
+++ b/examples/symbol-atlas-webgl.js
@@ -0,0 +1,123 @@
+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.AtlasManager');
+goog.require('ol.style.Circle');
+goog.require('ol.style.Fill');
+goog.require('ol.style.RegularShape');
+goog.require('ol.style.Stroke');
+goog.require('ol.style.Style');
+
+var atlasManager = new ol.style.AtlasManager({
+ // we increase the initial size so that all symbols fit into
+ // a single atlas image
+ initialSize: 512
+});
+
+var symbolInfo = [{
+ opacity: 1.0,
+ scale: 1.0,
+ fillColor: 'rgba(255, 153, 0, 0.4)',
+ strokeColor: 'rgba(255, 204, 0, 0.2)'
+}, {
+ opacity: 0.75,
+ scale: 1.25,
+ fillColor: 'rgba(70, 80, 224, 0.4)',
+ strokeColor: 'rgba(12, 21, 138, 0.2)'
+}, {
+ opacity: 0.5,
+ scale: 1.5,
+ fillColor: 'rgba(66, 150, 79, 0.4)',
+ strokeColor: 'rgba(20, 99, 32, 0.2)'
+}, {
+ opacity: 1.0,
+ scale: 1.0,
+ fillColor: 'rgba(176, 61, 35, 0.4)',
+ strokeColor: 'rgba(145, 43, 20, 0.2)'
+}];
+
+var radiuses = [3, 6, 9, 15, 19, 25];
+var symbolCount = symbolInfo.length * radiuses.length * 2;
+var symbols = [];
+var i, j;
+for (i = 0; i < symbolInfo.length; ++i) {
+ var info = symbolInfo[i];
+ for (j = 0; j < radiuses.length; ++j) {
+ // circle symbol
+ symbols.push(new ol.style.Circle({
+ opacity: info.opacity,
+ scale: info.scale,
+ radius: radiuses[j],
+ fill: new ol.style.Fill({
+ color: info.fillColor
+ }),
+ stroke: new ol.style.Stroke({
+ color: info.strokeColor,
+ width: 1
+ }),
+ // by passing the atlas manager to the symbol,
+ // the symbol will be added to an atlas
+ atlasManager: atlasManager
+ }));
+
+ // star symbol
+ symbols.push(new ol.style.RegularShape({
+ points: 8,
+ opacity: info.opacity,
+ scale: info.scale,
+ radius: radiuses[j],
+ radius2: radiuses[j] * 0.7,
+ angle: 1.4,
+ fill: new ol.style.Fill({
+ color: info.fillColor
+ }),
+ stroke: new ol.style.Stroke({
+ color: info.strokeColor,
+ width: 1
+ }),
+ atlasManager: atlasManager
+ }));
+ }
+}
+
+var featureCount = 50000;
+var features = new Array(featureCount);
+var feature, geometry;
+var e = 25000000;
+for (i = 0; i < featureCount; ++i) {
+ geometry = new ol.geom.Point(
+ [2 * e * Math.random() - e, 2 * e * Math.random() - e]);
+ feature = new ol.Feature(geometry);
+ feature.setStyle(
+ new ol.style.Style({
+ image: symbols[i % symbolCount]
+ })
+ );
+ features[i] = feature;
+}
+
+var vectorSource = new ol.source.Vector({
+ features: features
+});
+var vector = new ol.layer.Vector({
+ source: vectorSource
+});
+
+// Use the "webgl" renderer by default.
+var renderer = exampleNS.getRendererFromQueryString();
+if (!renderer) {
+ renderer = 'webgl';
+}
+
+var map = new ol.Map({
+ renderer: renderer,
+ layers: [vector],
+ target: document.getElementById('map'),
+ view: new ol.View({
+ center: [0, 0],
+ zoom: 4
+ })
+});
diff --git a/externs/olx.js b/externs/olx.js
index e850627709a..05013fc9b47 100644
--- a/externs/olx.js
+++ b/externs/olx.js
@@ -5438,7 +5438,8 @@ olx.style;
* @typedef {{fill: (ol.style.Fill|undefined),
* radius: number,
* snapToPixel: (boolean|undefined),
- * stroke: (ol.style.Stroke|undefined)}}
+ * stroke: (ol.style.Stroke|undefined),
+ * atlasManager: (ol.style.AtlasManager|undefined)}}
* @api
*/
olx.style.CircleOptions;
@@ -5482,6 +5483,16 @@ olx.style.CircleOptions.prototype.snapToPixel;
olx.style.CircleOptions.prototype.stroke;
+/**
+ * The atlas manager to use for this circle. When using WebGL it is
+ * recommended to use an atlas manager to avoid texture switching.
+ * If an atlas manager is given, the circle is added to an atlas.
+ * By default no atlas manager is used.
+ * @type {ol.style.AtlasManager|undefined}
+ */
+olx.style.CircleOptions.prototype.atlasManager;
+
+
/**
* @typedef {{color: (ol.Color|string|undefined)}}
* @api
@@ -5661,7 +5672,8 @@ olx.style.IconOptions.prototype.src;
* radius2: (number|undefined),
* angle: (number|undefined),
* snapToPixel: (boolean|undefined),
- * stroke: (ol.style.Stroke|undefined)}}
+ * stroke: (ol.style.Stroke|undefined),
+ * atlasManager: (ol.style.AtlasManager|undefined)}}
* @api
*/
olx.style.RegularShapeOptions;
@@ -5740,6 +5752,16 @@ olx.style.RegularShapeOptions.prototype.snapToPixel;
olx.style.RegularShapeOptions.prototype.stroke;
+/**
+ * The atlas manager to use for this symbol. When using WebGL it is
+ * recommended to use an atlas manager to avoid texture switching.
+ * If an atlas manager is given, the symbol is added to an atlas.
+ * By default no atlas manager is used.
+ * @type {ol.style.AtlasManager|undefined}
+ */
+olx.style.RegularShapeOptions.prototype.atlasManager;
+
+
/**
* @typedef {{color: (ol.Color|string|undefined),
* lineCap: (string|undefined),
@@ -6279,3 +6301,41 @@ olx.ViewState.prototype.resolution;
* @api
*/
olx.ViewState.prototype.rotation;
+
+
+/**
+ * @typedef {{initialSize: (number|undefined),
+ * maxSize: (number|undefined),
+ * space: (number|undefined)}}
+ * @api
+ */
+olx.style.AtlasManagerOptions;
+
+
+/**
+ * The size in pixels of the first atlas image. If no value is given the
+ * `ol.INITIAL_ATLAS_SIZE` compile-time constant will be used.
+ * @type {number|undefined}
+ * @api
+ */
+olx.style.AtlasManagerOptions.prototype.initialSize;
+
+
+/**
+ * The maximum size in pixels of atlas images. If no value is given then
+ * the `ol.MAX_ATLAS_SIZE` compile-time constant will be used. And if
+ * `ol.MAX_ATLAS_SIZE` is set to `-1` (the default) then
+ * `ol.WEBGL_MAX_TEXTURE_SIZE` will used if WebGL is supported. Otherwise
+ * 2048 is used.
+ * @type {number|undefined}
+ * @api
+ */
+olx.style.AtlasManagerOptions.prototype.maxSize;
+
+
+/**
+ * The space in pixels between images (default: 1).
+ * @type {number|undefined}
+ * @api
+ */
+olx.style.AtlasManagerOptions.prototype.space;
diff --git a/src/ol/has.js b/src/ol/has.js
index 1472c8b53c6..f86249f9767 100644
--- a/src/ol/has.js
+++ b/src/ol/has.js
@@ -119,21 +119,32 @@ ol.has.MSPOINTER = !!(goog.global.navigator.msPointerEnabled);
* @type {boolean}
* @api stable
*/
-ol.has.WEBGL = ol.ENABLE_WEBGL && (
- /**
- * @return {boolean} WebGL supported.
- */
- function() {
- if (!('WebGLRenderingContext' in goog.global)) {
- return false;
- }
+ol.has.WEBGL;
+
+
+(function() {
+ if (ol.ENABLE_WEBGL) {
+ var hasWebGL = false;
+ var textureSize;
+ var /** @type {Array.} */ extensions = [];
+
+ if ('WebGLRenderingContext' in goog.global) {
try {
var canvas = /** @type {HTMLCanvasElement} */
(goog.dom.createElement(goog.dom.TagName.CANVAS));
- return !goog.isNull(ol.webgl.getContext(canvas, {
+ var gl = ol.webgl.getContext(canvas, {
failIfMajorPerformanceCaveat: true
- }));
- } catch (e) {
- return false;
- }
- })();
+ });
+ if (!goog.isNull(gl)) {
+ hasWebGL = true;
+ textureSize = /** @type {number} */
+ (gl.getParameter(gl.MAX_TEXTURE_SIZE));
+ extensions = gl.getSupportedExtensions();
+ }
+ } catch (e) {}
+ }
+ ol.has.WEBGL = hasWebGL;
+ ol.WEBGL_EXTENSIONS = extensions;
+ ol.WEBGL_MAX_TEXTURE_SIZE = textureSize;
+ }
+})();
diff --git a/src/ol/ol.js b/src/ol/ol.js
index 6e0968f9d36..01ca3649399 100644
--- a/src/ol/ol.js
+++ b/src/ol/ol.js
@@ -152,6 +152,13 @@ ol.ENABLE_WEBGL = true;
ol.LEGACY_IE_SUPPORT = false;
+/**
+ * @define {number} The size in pixels of the first atlas image. Default is
+ * `256`.
+ */
+ol.INITIAL_ATLAS_SIZE = 256;
+
+
/**
* The page is loaded using HTTPS.
* @const
@@ -175,6 +182,14 @@ ol.IS_LEGACY_IE = goog.userAgent.IE &&
ol.KEYBOARD_PAN_DURATION = 100;
+/**
+ * @define {number} The maximum size in pixels of atlas images. Default is
+ * `-1`, meaning it is not used (and `ol.ol.WEBGL_MAX_TEXTURE_SIZE` is
+ * used instead).
+ */
+ol.MAX_ATLAS_SIZE = -1;
+
+
/**
* @define {number} Maximum mouse wheel delta.
*/
@@ -219,6 +234,24 @@ ol.SIMPLIFY_TOLERANCE = 0.5;
ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024;
+/**
+ * The maximum supported WebGL texture size in pixels. If WebGL is not
+ * supported, the value is set to `undefined`.
+ * @const
+ * @type {number|undefined}
+ * @api
+ */
+ol.WEBGL_MAX_TEXTURE_SIZE; // value is set in `ol.has`
+
+
+/**
+ * List of supported WebGL extensions.
+ * @const
+ * @type {Array.}
+ */
+ol.WEBGL_EXTENSIONS; // value is set in `ol.has`
+
+
/**
* @define {number} Zoom slider animation duration.
*/
diff --git a/src/ol/render/canvas/canvasreplay.js b/src/ol/render/canvas/canvasreplay.js
index 5d7b35aef6f..e2ab0f38e08 100644
--- a/src/ol/render/canvas/canvasreplay.js
+++ b/src/ol/render/canvas/canvasreplay.js
@@ -2011,7 +2011,7 @@ ol.render.canvas.ReplayGroup.prototype.forEachGeometryAtPixel = function(
/**
- * @inheritDoc
+ * FIXME empty description for jsdoc
*/
ol.render.canvas.ReplayGroup.prototype.finish = function() {
var zKey;
diff --git a/src/ol/render/ireplay.js b/src/ol/render/ireplay.js
index 3f4d7c122a1..78e2faf67a0 100644
--- a/src/ol/render/ireplay.js
+++ b/src/ol/render/ireplay.js
@@ -35,13 +35,6 @@ ol.render.IReplayGroup = function() {
};
-/**
- * FIXME empty description for jsdoc
- */
-ol.render.IReplayGroup.prototype.finish = function() {
-};
-
-
/**
* @param {number|undefined} zIndex Z index.
* @param {ol.render.ReplayType} replayType Replay type.
diff --git a/src/ol/render/ivectorcontext.js b/src/ol/render/ivectorcontext.js
index 22ba1248318..e331bec0db5 100644
--- a/src/ol/render/ivectorcontext.js
+++ b/src/ol/render/ivectorcontext.js
@@ -5,8 +5,8 @@ goog.provide('ol.render.IVectorContext');
/**
- * VectorContext interface. Currently implemented by
- * {@link ol.render.canvas.Immediate}
+ * VectorContext interface. Implemented by
+ * {@link ol.render.canvas.Immediate} and {@link ol.render.webgl.Immediate}.
* @interface
*/
ol.render.IVectorContext = function() {
@@ -15,7 +15,7 @@ ol.render.IVectorContext = function() {
/**
* @param {number} zIndex Z index.
- * @param {function(ol.render.canvas.Immediate)} callback Callback.
+ * @param {function(ol.render.IVectorContext)} callback Callback.
*/
ol.render.IVectorContext.prototype.drawAsync = function(zIndex, callback) {
};
diff --git a/src/ol/render/webgl/webglimagecolor.glsl b/src/ol/render/webgl/webglimagecolor.glsl
new file mode 100644
index 00000000000..56e045f4025
--- /dev/null
+++ b/src/ol/render/webgl/webglimagecolor.glsl
@@ -0,0 +1,46 @@
+//! NAMESPACE=ol.render.webgl.imagereplay.shader.Color
+//! CLASS=ol.render.webgl.imagereplay.shader.Color
+
+
+//! COMMON
+varying vec2 v_texCoord;
+varying float v_opacity;
+
+//! VERTEX
+attribute vec2 a_position;
+attribute vec2 a_texCoord;
+attribute vec2 a_offsets;
+attribute float a_opacity;
+attribute float a_rotateWithView;
+
+uniform mat4 u_projectionMatrix;
+uniform mat4 u_offsetScaleMatrix;
+uniform mat4 u_offsetRotateMatrix;
+
+void main(void) {
+ mat4 offsetMatrix = u_offsetScaleMatrix;
+ if (a_rotateWithView == 1.0) {
+ offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;
+ }
+ vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);
+ gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;
+ v_texCoord = a_texCoord;
+ v_opacity = a_opacity;
+}
+
+
+//! FRAGMENT
+// @see https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/filters/skia/SkiaImageFilterBuilder.cpp
+uniform mat4 u_colorMatrix;
+uniform float u_opacity;
+uniform sampler2D u_image;
+
+void main(void) {
+ vec4 texColor = texture2D(u_image, v_texCoord);
+ float alpha = texColor.a * v_opacity * u_opacity;
+ if (alpha == 0.0) {
+ discard;
+ }
+ gl_FragColor.a = alpha;
+ gl_FragColor.rgb = (u_colorMatrix * vec4(texColor.rgb, 1.)).rgb;
+}
diff --git a/src/ol/render/webgl/webglimagecolorshader.js b/src/ol/render/webgl/webglimagecolorshader.js
new file mode 100644
index 00000000000..94f1b8a74bc
--- /dev/null
+++ b/src/ol/render/webgl/webglimagecolorshader.js
@@ -0,0 +1,153 @@
+// This file is automatically generated, do not edit
+goog.provide('ol.render.webgl.imagereplay.shader.Color');
+
+goog.require('ol.webgl.shader');
+
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Fragment}
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.ColorFragment = function() {
+ goog.base(this, ol.render.webgl.imagereplay.shader.ColorFragment.SOURCE);
+};
+goog.inherits(ol.render.webgl.imagereplay.shader.ColorFragment, ol.webgl.shader.Fragment);
+goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.ColorFragment);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.ColorFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\n// @see https://svn.webkit.org/repository/webkit/trunk/Source/WebCore/platform/graphics/filters/skia/SkiaImageFilterBuilder.cpp\nuniform mat4 u_colorMatrix;\nuniform float u_opacity;\nuniform sampler2D u_image;\n\nvoid main(void) {\n vec4 texColor = texture2D(u_image, v_texCoord);\n float alpha = texColor.a * v_opacity * u_opacity;\n if (alpha == 0.0) {\n discard;\n }\n gl_FragColor.a = alpha;\n gl_FragColor.rgb = (u_colorMatrix * vec4(texColor.rgb, 1.)).rgb;\n}\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.ColorFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform mat4 k;uniform float l;uniform sampler2D m;void main(void){vec4 texColor=texture2D(m,a);float alpha=texColor.a*b*l;if(alpha==0.0){discard;}gl_FragColor.a=alpha;gl_FragColor.rgb=(k*vec4(texColor.rgb,1.)).rgb;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.ColorFragment.SOURCE = goog.DEBUG ?
+ ol.render.webgl.imagereplay.shader.ColorFragment.DEBUG_SOURCE :
+ ol.render.webgl.imagereplay.shader.ColorFragment.OPTIMIZED_SOURCE;
+
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Vertex}
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.ColorVertex = function() {
+ goog.base(this, ol.render.webgl.imagereplay.shader.ColorVertex.SOURCE);
+};
+goog.inherits(ol.render.webgl.imagereplay.shader.ColorVertex, ol.webgl.shader.Vertex);
+goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.ColorVertex);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.ColorVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n mat4 offsetMatrix = u_offsetScaleMatrix;\n if (a_rotateWithView == 1.0) {\n offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n }\n vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.ColorVertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.ColorVertex.SOURCE = goog.DEBUG ?
+ ol.render.webgl.imagereplay.shader.ColorVertex.DEBUG_SOURCE :
+ ol.render.webgl.imagereplay.shader.ColorVertex.OPTIMIZED_SOURCE;
+
+
+
+/**
+ * @constructor
+ * @param {WebGLRenderingContext} gl GL.
+ * @param {WebGLProgram} program Program.
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.Color.Locations = function(gl, program) {
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_colorMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_colorMatrix' : 'k');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_image = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_image' : 'm');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_offsetRotateMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_offsetScaleMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_opacity = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_opacity' : 'l');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_projectionMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_projectionMatrix' : 'h');
+
+ /**
+ * @type {number}
+ */
+ this.a_offsets = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_offsets' : 'e');
+
+ /**
+ * @type {number}
+ */
+ this.a_opacity = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_opacity' : 'f');
+
+ /**
+ * @type {number}
+ */
+ this.a_position = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_position' : 'c');
+
+ /**
+ * @type {number}
+ */
+ this.a_rotateWithView = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_rotateWithView' : 'g');
+
+ /**
+ * @type {number}
+ */
+ this.a_texCoord = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_texCoord' : 'd');
+};
diff --git a/src/ol/render/webgl/webglimagedefault.glsl b/src/ol/render/webgl/webglimagedefault.glsl
new file mode 100644
index 00000000000..15d2bce128f
--- /dev/null
+++ b/src/ol/render/webgl/webglimagedefault.glsl
@@ -0,0 +1,44 @@
+//! NAMESPACE=ol.render.webgl.imagereplay.shader.Default
+//! CLASS=ol.render.webgl.imagereplay.shader.Default
+
+
+//! COMMON
+varying vec2 v_texCoord;
+varying float v_opacity;
+
+//! VERTEX
+attribute vec2 a_position;
+attribute vec2 a_texCoord;
+attribute vec2 a_offsets;
+attribute float a_opacity;
+attribute float a_rotateWithView;
+
+uniform mat4 u_projectionMatrix;
+uniform mat4 u_offsetScaleMatrix;
+uniform mat4 u_offsetRotateMatrix;
+
+void main(void) {
+ mat4 offsetMatrix = u_offsetScaleMatrix;
+ if (a_rotateWithView == 1.0) {
+ offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;
+ }
+ vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);
+ gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;
+ v_texCoord = a_texCoord;
+ v_opacity = a_opacity;
+}
+
+
+//! FRAGMENT
+uniform float u_opacity;
+uniform sampler2D u_image;
+
+void main(void) {
+ vec4 texColor = texture2D(u_image, v_texCoord);
+ gl_FragColor.rgb = texColor.rgb;
+ float alpha = texColor.a * v_opacity * u_opacity;
+ if (alpha == 0.0) {
+ discard;
+ }
+ gl_FragColor.a = alpha;
+}
diff --git a/src/ol/render/webgl/webglimagedefaultshader.js b/src/ol/render/webgl/webglimagedefaultshader.js
new file mode 100644
index 00000000000..7df0fe26c5e
--- /dev/null
+++ b/src/ol/render/webgl/webglimagedefaultshader.js
@@ -0,0 +1,147 @@
+// This file is automatically generated, do not edit
+goog.provide('ol.render.webgl.imagereplay.shader.Default');
+
+goog.require('ol.webgl.shader');
+
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Fragment}
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment = function() {
+ goog.base(this, ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE);
+};
+goog.inherits(ol.render.webgl.imagereplay.shader.DefaultFragment, ol.webgl.shader.Fragment);
+goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultFragment);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\nuniform float u_opacity;\nuniform sampler2D u_image;\n\nvoid main(void) {\n vec4 texColor = texture2D(u_image, v_texCoord);\n gl_FragColor.rgb = texColor.rgb;\n float alpha = texColor.a * v_opacity * u_opacity;\n if (alpha == 0.0) {\n discard;\n }\n gl_FragColor.a = alpha;\n}\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform float k;uniform sampler2D l;void main(void){vec4 texColor=texture2D(l,a);gl_FragColor.rgb=texColor.rgb;float alpha=texColor.a*b*k;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE = goog.DEBUG ?
+ ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE :
+ ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE;
+
+
+
+/**
+ * @constructor
+ * @extends {ol.webgl.shader.Vertex}
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex = function() {
+ goog.base(this, ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE);
+};
+goog.inherits(ol.render.webgl.imagereplay.shader.DefaultVertex, ol.webgl.shader.Vertex);
+goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultVertex);
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n mat4 offsetMatrix = u_offsetScaleMatrix;\n if (a_rotateWithView == 1.0) {\n offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n }\n vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n v_texCoord = a_texCoord;\n v_opacity = a_opacity;\n}\n\n\n';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}';
+
+
+/**
+ * @const
+ * @type {string}
+ */
+ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE = goog.DEBUG ?
+ ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE :
+ ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE;
+
+
+
+/**
+ * @constructor
+ * @param {WebGLRenderingContext} gl GL.
+ * @param {WebGLProgram} program Program.
+ * @struct
+ */
+ol.render.webgl.imagereplay.shader.Default.Locations = function(gl, program) {
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_image = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_image' : 'l');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_offsetRotateMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_offsetScaleMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_opacity = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_opacity' : 'k');
+
+ /**
+ * @type {WebGLUniformLocation}
+ */
+ this.u_projectionMatrix = gl.getUniformLocation(
+ program, goog.DEBUG ? 'u_projectionMatrix' : 'h');
+
+ /**
+ * @type {number}
+ */
+ this.a_offsets = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_offsets' : 'e');
+
+ /**
+ * @type {number}
+ */
+ this.a_opacity = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_opacity' : 'f');
+
+ /**
+ * @type {number}
+ */
+ this.a_position = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_position' : 'c');
+
+ /**
+ * @type {number}
+ */
+ this.a_rotateWithView = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_rotateWithView' : 'g');
+
+ /**
+ * @type {number}
+ */
+ this.a_texCoord = gl.getAttribLocation(
+ program, goog.DEBUG ? 'a_texCoord' : 'd');
+};
diff --git a/src/ol/render/webgl/webglimmediate.js b/src/ol/render/webgl/webglimmediate.js
index 375b0c96cf5..5095504b7a1 100644
--- a/src/ol/render/webgl/webglimmediate.js
+++ b/src/ol/render/webgl/webglimmediate.js
@@ -1,4 +1,8 @@
goog.provide('ol.render.webgl.Immediate');
+goog.require('goog.array');
+goog.require('goog.object');
+goog.require('ol.extent');
+goog.require('ol.render.webgl.ReplayGroup');
@@ -6,22 +10,103 @@ goog.provide('ol.render.webgl.Immediate');
* @constructor
* @implements {ol.render.IVectorContext}
* @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {ol.Extent} extent Extent.
* @param {number} pixelRatio Pixel ratio.
* @struct
*/
-ol.render.webgl.Immediate = function(context, pixelRatio) {
+ol.render.webgl.Immediate = function(context,
+ center, resolution, rotation, size, extent, pixelRatio) {
+
+ /**
+ * @private
+ */
+ this.context_ = context;
+
+ /**
+ * @private
+ */
+ this.center_ = center;
+
+ /**
+ * @private
+ */
+ this.extent_ = extent;
+
+ /**
+ * @private
+ */
+ this.pixelRatio_ = pixelRatio;
+
+ /**
+ * @private
+ */
+ this.size_ = size;
+
+ /**
+ * @private
+ */
+ this.rotation_ = rotation;
+
+ /**
+ * @private
+ */
+ this.resolution_ = resolution;
+
+ /**
+ * @private
+ * @type {ol.style.Image}
+ */
+ this.imageStyle_ = null;
+
+ /**
+ * @private
+ * @type {Object.>}
+ */
+ this.callbacksByZIndex_ = {};
};
/**
- * @inheritDoc
+ * FIXME: empty description for jsdoc
+ */
+ol.render.webgl.Immediate.prototype.flush = function() {
+ /** @type {Array.} */
+ var zs = goog.array.map(goog.object.getKeys(this.callbacksByZIndex_), Number);
+ goog.array.sort(zs);
+ var i, ii, callbacks, j, jj;
+ for (i = 0, ii = zs.length; i < ii; ++i) {
+ callbacks = this.callbacksByZIndex_[zs[i].toString()];
+ for (j = 0, jj = callbacks.length; j < jj; ++j) {
+ callbacks[j](this);
+ }
+ }
+};
+
+
+/**
+ * @param {number} zIndex Z index.
+ * @param {function(ol.render.webgl.Immediate)} callback Callback.
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawAsync = function(zIndex, callback) {
+ var zIndexKey = zIndex.toString();
+ var callbacks = this.callbacksByZIndex_[zIndexKey];
+ if (goog.isDef(callbacks)) {
+ callbacks.push(callback);
+ } else {
+ this.callbacksByZIndex_[zIndexKey] = [callback];
+ }
};
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawCircleGeometry =
function(circleGeometry, data) {
@@ -30,29 +115,83 @@ ol.render.webgl.Immediate.prototype.drawCircleGeometry =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawFeature = function(feature, style) {
+ var geometry = feature.getGeometry();
+ if (!goog.isDefAndNotNull(geometry) ||
+ !ol.extent.intersects(this.extent_, geometry.getExtent())) {
+ return;
+ }
+ var zIndex = style.getZIndex();
+ if (!goog.isDef(zIndex)) {
+ zIndex = 0;
+ }
+ this.drawAsync(zIndex, function(render) {
+ render.setFillStrokeStyle(style.getFill(), style.getStroke());
+ render.setImageStyle(style.getImage());
+ render.setTextStyle(style.getText());
+ var type = geometry.getType();
+ var renderGeometry = ol.render.webgl.Immediate.GEOMETRY_RENDERERS_[type];
+ // Do not assert since all kinds of geometries are not handled yet.
+ // In spite, render what we support.
+ if (renderGeometry) {
+ renderGeometry.call(render, geometry, null);
+ }
+ });
};
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry =
function(geometryCollectionGeometry, data) {
+ var geometries = geometryCollectionGeometry.getGeometriesArray();
+ var renderers = ol.render.webgl.Immediate.GEOMETRY_RENDERERS_;
+ var i, ii;
+ for (i = 0, ii = geometries.length; i < ii; ++i) {
+ var geometry = geometries[i];
+ var geometryRenderer = renderers[geometry.getType()];
+ // Do not assert since all kinds of geometries are not handled yet.
+ // In order to support hierarchies, delegate instead what we can to
+ // valid renderers.
+ if (geometryRenderer) {
+ geometryRenderer.call(this, geometry, data);
+ }
+ }
};
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawPointGeometry =
function(pointGeometry, data) {
+ var context = this.context_;
+ var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_);
+ var replay = replayGroup.getReplay(0, ol.render.ReplayType.IMAGE);
+ replay.setImageStyle(this.imageStyle_);
+ replay.drawPointGeometry(pointGeometry, data);
+ replay.finish(context);
+ // default colors
+ var opacity = 1;
+ var brightness = 0;
+ var contrast = 1;
+ var hue = 0;
+ var saturation = 1;
+ replay.replay(this.context_, this.center_, this.resolution_, this.rotation_,
+ this.size_, this.extent_, this.pixelRatio_, opacity, brightness,
+ contrast, hue, saturation, {});
+ replay.getDeleteResourcesFunction(context)();
};
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawLineStringGeometry =
function(lineStringGeometry, data) {
@@ -61,6 +200,7 @@ ol.render.webgl.Immediate.prototype.drawLineStringGeometry =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry =
function(multiLineStringGeometry, data) {
@@ -69,14 +209,32 @@ ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawMultiPointGeometry =
function(multiPointGeometry, data) {
+ var context = this.context_;
+ var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_);
+ var replay = replayGroup.getReplay(0, ol.render.ReplayType.IMAGE);
+ replay.setImageStyle(this.imageStyle_);
+ replay.drawMultiPointGeometry(multiPointGeometry, data);
+ replay.finish(context);
+ // default colors
+ var opacity = 1;
+ var brightness = 0;
+ var contrast = 1;
+ var hue = 0;
+ var saturation = 1;
+ replay.replay(this.context_, this.center_, this.resolution_, this.rotation_,
+ this.size_, this.extent_, this.pixelRatio_, opacity, brightness,
+ contrast, hue, saturation, {});
+ replay.getDeleteResourcesFunction(context)();
};
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawMultiPolygonGeometry =
function(multiPolygonGeometry, data) {
@@ -85,6 +243,7 @@ ol.render.webgl.Immediate.prototype.drawMultiPolygonGeometry =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawPolygonGeometry =
function(polygonGeometry, data) {
@@ -93,6 +252,7 @@ ol.render.webgl.Immediate.prototype.drawPolygonGeometry =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.drawText =
function(flatCoordinates, offset, end, stride, geometry, data) {
@@ -101,6 +261,7 @@ ol.render.webgl.Immediate.prototype.drawText =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.setFillStrokeStyle =
function(fillStyle, strokeStyle) {
@@ -109,13 +270,31 @@ ol.render.webgl.Immediate.prototype.setFillStrokeStyle =
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.setImageStyle = function(imageStyle) {
+ this.imageStyle_ = imageStyle;
};
/**
* @inheritDoc
+ * @api
*/
ol.render.webgl.Immediate.prototype.setTextStyle = function(textStyle) {
};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.}
+ */
+ol.render.webgl.Immediate.GEOMETRY_RENDERERS_ = {
+ 'Point': ol.render.webgl.Immediate.prototype.drawPointGeometry,
+ 'MultiPoint': ol.render.webgl.Immediate.prototype.drawMultiPointGeometry,
+ 'GeometryCollection':
+ ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry
+};
diff --git a/src/ol/render/webgl/webglreplay.js b/src/ol/render/webgl/webglreplay.js
new file mode 100644
index 00000000000..a2263934cab
--- /dev/null
+++ b/src/ol/render/webgl/webglreplay.js
@@ -0,0 +1,841 @@
+goog.provide('ol.render.webgl.ImageReplay');
+goog.provide('ol.render.webgl.ReplayGroup');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.functions');
+goog.require('goog.object');
+goog.require('goog.vec.Mat4');
+goog.require('ol.color.Matrix');
+goog.require('ol.extent');
+goog.require('ol.render.IReplayGroup');
+goog.require('ol.render.webgl.imagereplay.shader.Color');
+goog.require('ol.render.webgl.imagereplay.shader.Default');
+goog.require('ol.vec.Mat4');
+goog.require('ol.webgl.Buffer');
+
+
+
+/**
+ * @constructor
+ * @implements {ol.render.IVectorContext}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Max extent.
+ * @protected
+ * @struct
+ */
+ol.render.webgl.ImageReplay = function(tolerance, maxExtent) {
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.anchorX_ = undefined;
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.anchorY_ = undefined;
+
+ /**
+ * @private
+ * @type {ol.color.Matrix}
+ */
+ this.colorMatrix_ = new ol.color.Matrix();
+
+ /**
+ * The origin of the coordinate system for the point coordinates sent to
+ * the GPU. To eliminate jitter caused by precision problems in the GPU
+ * we use the "Rendering Relative to Eye" technique described in the "3D
+ * Engine Design for Virtual Globes" book.
+ * @private
+ * @type {ol.Coordinate}
+ */
+ this.origin_ = ol.extent.getCenter(maxExtent);
+
+ /**
+ * @type {ol.Extent}
+ * @private
+ */
+ this.extent_ = ol.extent.createEmpty();
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.groupIndices_ = [];
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.height_ = undefined;
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.images_ = [];
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.imageHeight_ = undefined;
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.imageWidth_ = undefined;
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.indices_ = [];
+
+ /**
+ * @type {ol.webgl.Buffer}
+ * @private
+ */
+ this.indicesBuffer_ = null;
+
+ /**
+ * @private
+ * @type {ol.render.webgl.imagereplay.shader.Color.Locations}
+ */
+ this.colorLocations_ = null;
+
+ /**
+ * @private
+ * @type {ol.render.webgl.imagereplay.shader.Default.Locations}
+ */
+ this.defaultLocations_ = null;
+
+ /**
+ * @private
+ * @type {number|undefined}
+ */
+ this.opacity_ = undefined;
+
+ /**
+ * @type {!goog.vec.Mat4.Number}
+ * @private
+ */
+ this.offsetRotateMatrix_ = goog.vec.Mat4.createNumberIdentity();
+
+ /**
+ * @type {!goog.vec.Mat4.Number}
+ * @private
+ */
+ this.offsetScaleMatrix_ = goog.vec.Mat4.createNumberIdentity();
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.originX_ = undefined;
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.originY_ = undefined;
+
+ /**
+ * @type {!goog.vec.Mat4.Number}
+ * @private
+ */
+ this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity();
+
+ /**
+ * @private
+ * @type {boolean|undefined}
+ */
+ this.rotateWithView_ = undefined;
+
+ /**
+ * @private
+ * @type {number|undefined}
+ */
+ this.rotation_ = undefined;
+
+ /**
+ * @private
+ * @type {number|undefined}
+ */
+ this.scale_ = undefined;
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.textures_ = [];
+
+ /**
+ * @type {Array.}
+ * @private
+ */
+ this.vertices_ = [];
+
+ /**
+ * @type {ol.webgl.Buffer}
+ * @private
+ */
+ this.verticesBuffer_ = null;
+
+ /**
+ * @type {number|undefined}
+ * @private
+ */
+ this.width_ = undefined;
+
+};
+
+
+/**
+ * @param {ol.webgl.Context} context WebGL context.
+ * @return {function()} Delete resources function.
+ */
+ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction =
+ function(context) {
+ // We only delete our stuff here. The shaders and the program may
+ // be used by other ImageReplay instances (for other layers). And
+ // they will be deleted when disposing of the ol.webgl.Context
+ // object.
+ goog.asserts.assert(!goog.isNull(this.verticesBuffer_));
+ goog.asserts.assert(!goog.isNull(this.indicesBuffer_));
+ var verticesBuffer = this.verticesBuffer_;
+ var indicesBuffer = this.indicesBuffer_;
+ var textures = this.textures_;
+ var gl = context.getGL();
+ return function() {
+ if (!gl.isContextLost()) {
+ var i, ii;
+ for (i = 0, ii = textures.length; i < ii; ++i) {
+ gl.deleteTexture(textures[i]);
+ }
+ }
+ context.deleteBuffer(verticesBuffer);
+ context.deleteBuffer(indicesBuffer);
+ };
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawAsync = goog.abstractMethod;
+
+
+/**
+ * @param {Array.} flatCoordinates Flat coordinates.
+ * @param {number} offset Offset.
+ * @param {number} end End.
+ * @param {number} stride Stride.
+ * @return {number} My end.
+ * @private
+ */
+ol.render.webgl.ImageReplay.prototype.drawCoordinates_ =
+ function(flatCoordinates, offset, end, stride) {
+ goog.asserts.assert(goog.isDef(this.anchorX_));
+ goog.asserts.assert(goog.isDef(this.anchorY_));
+ goog.asserts.assert(goog.isDef(this.height_));
+ goog.asserts.assert(goog.isDef(this.imageHeight_));
+ goog.asserts.assert(goog.isDef(this.imageWidth_));
+ goog.asserts.assert(goog.isDef(this.opacity_));
+ goog.asserts.assert(goog.isDef(this.originX_));
+ goog.asserts.assert(goog.isDef(this.originY_));
+ goog.asserts.assert(goog.isDef(this.rotateWithView_));
+ goog.asserts.assert(goog.isDef(this.rotation_));
+ goog.asserts.assert(goog.isDef(this.scale_));
+ goog.asserts.assert(goog.isDef(this.width_));
+ var anchorX = this.anchorX_;
+ var anchorY = this.anchorY_;
+ var height = this.height_;
+ var imageHeight = this.imageHeight_;
+ var imageWidth = this.imageWidth_;
+ var opacity = this.opacity_;
+ var originX = this.originX_;
+ var originY = this.originY_;
+ var rotateWithView = this.rotateWithView_ ? 1.0 : 0.0;
+ var rotation = this.rotation_;
+ var scale = this.scale_;
+ var width = this.width_;
+ var cos = Math.cos(rotation);
+ var sin = Math.sin(rotation);
+ var numIndices = this.indices_.length;
+ var numVertices = this.vertices_.length;
+ var i, n, offsetX, offsetY, x, y;
+ for (i = offset; i < end; i += stride) {
+ x = flatCoordinates[i] - this.origin_[0];
+ y = flatCoordinates[i + 1] - this.origin_[1];
+
+ // There are 4 vertices per [x, y] point, one for each corner of the
+ // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if
+ // WebGL supported Geometry Shaders (which can emit new vertices), but that
+ // is not currently the case.
+ //
+ // And each vertex includes 8 values: the x and y coordinates, the x and
+ // y offsets used to calculate the position of the corner, the u and
+ // v texture coordinates for the corner, the opacity, and whether the
+ // the image should be rotated with the view (rotateWithView).
+
+ n = numVertices / 8;
+
+ // bottom-left corner
+ offsetX = -scale * anchorX;
+ offsetY = -scale * (height - anchorY);
+ this.vertices_[numVertices++] = x;
+ this.vertices_[numVertices++] = y;
+ this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+ this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+ this.vertices_[numVertices++] = originX / imageWidth;
+ this.vertices_[numVertices++] = (originY + height) / imageHeight;
+ this.vertices_[numVertices++] = opacity;
+ this.vertices_[numVertices++] = rotateWithView;
+
+ // bottom-right corner
+ offsetX = scale * (width - anchorX);
+ offsetY = -scale * (height - anchorY);
+ this.vertices_[numVertices++] = x;
+ this.vertices_[numVertices++] = y;
+ this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+ this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+ this.vertices_[numVertices++] = (originX + width) / imageWidth;
+ this.vertices_[numVertices++] = (originY + height) / imageHeight;
+ this.vertices_[numVertices++] = opacity;
+ this.vertices_[numVertices++] = rotateWithView;
+
+ // top-right corner
+ offsetX = scale * (width - anchorX);
+ offsetY = scale * anchorY;
+ this.vertices_[numVertices++] = x;
+ this.vertices_[numVertices++] = y;
+ this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+ this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+ this.vertices_[numVertices++] = (originX + width) / imageWidth;
+ this.vertices_[numVertices++] = originY / imageHeight;
+ this.vertices_[numVertices++] = opacity;
+ this.vertices_[numVertices++] = rotateWithView;
+
+ // top-left corner
+ offsetX = -scale * anchorX;
+ offsetY = scale * anchorY;
+ this.vertices_[numVertices++] = x;
+ this.vertices_[numVertices++] = y;
+ this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
+ this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
+ this.vertices_[numVertices++] = originX / imageWidth;
+ this.vertices_[numVertices++] = originY / imageHeight;
+ this.vertices_[numVertices++] = opacity;
+ this.vertices_[numVertices++] = rotateWithView;
+
+ this.indices_[numIndices++] = n;
+ this.indices_[numIndices++] = n + 1;
+ this.indices_[numIndices++] = n + 2;
+ this.indices_[numIndices++] = n;
+ this.indices_[numIndices++] = n + 2;
+ this.indices_[numIndices++] = n + 3;
+ }
+
+ return numVertices;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawCircleGeometry = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawFeature = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawGeometryCollectionGeometry =
+ goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawLineStringGeometry =
+ goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawMultiLineStringGeometry =
+ goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry =
+ function(multiPointGeometry, data) {
+ ol.extent.extend(this.extent_, multiPointGeometry.getExtent());
+ var flatCoordinates = multiPointGeometry.getFlatCoordinates();
+ var stride = multiPointGeometry.getStride();
+ this.drawCoordinates_(
+ flatCoordinates, 0, flatCoordinates.length, stride);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawMultiPolygonGeometry =
+ goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawPointGeometry =
+ function(pointGeometry, data) {
+ ol.extent.extend(this.extent_, pointGeometry.getExtent());
+ var flatCoordinates = pointGeometry.getFlatCoordinates();
+ var stride = pointGeometry.getStride();
+ this.drawCoordinates_(
+ flatCoordinates, 0, flatCoordinates.length, stride);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawPolygonGeometry = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.drawText = goog.abstractMethod;
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ */
+ol.render.webgl.ImageReplay.prototype.finish = function(context) {
+ var gl = context.getGL();
+
+ this.groupIndices_.push(this.indices_.length);
+ goog.asserts.assert(this.images_.length == this.groupIndices_.length);
+
+ // create, bind, and populate the vertices buffer
+ this.verticesBuffer_ = new ol.webgl.Buffer(this.vertices_);
+ context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_);
+
+ var indices = this.indices_;
+ var bits = context.hasOESElementIndexUint ? 32 : 16;
+ goog.asserts.assert(indices[indices.length - 1] < Math.pow(2, bits),
+ 'Too large element index detected [%s] (OES_element_index_uint "%s")',
+ indices[indices.length - 1], context.hasOESElementIndexUint);
+
+ // create, bind, and populate the indices buffer
+ this.indicesBuffer_ = new ol.webgl.Buffer(indices);
+ context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);
+
+ goog.asserts.assert(this.textures_.length === 0);
+
+ // create textures
+ var texture, image, uid;
+ /** @type {Object.} */
+ var texturePerImage = {};
+ var i;
+ var ii = this.images_.length;
+ for (i = 0; i < ii; ++i) {
+ image = this.images_[i];
+
+ uid = goog.getUid(image).toString();
+ if (goog.object.containsKey(texturePerImage, uid)) {
+ texture = goog.object.get(texturePerImage, uid);
+ } else {
+ texture = gl.createTexture();
+ gl.bindTexture(goog.webgl.TEXTURE_2D, texture);
+ gl.texParameteri(goog.webgl.TEXTURE_2D,
+ goog.webgl.TEXTURE_WRAP_S, goog.webgl.CLAMP_TO_EDGE);
+ gl.texParameteri(goog.webgl.TEXTURE_2D,
+ goog.webgl.TEXTURE_WRAP_T, goog.webgl.CLAMP_TO_EDGE);
+ gl.texParameteri(goog.webgl.TEXTURE_2D,
+ goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.LINEAR);
+ gl.texParameteri(goog.webgl.TEXTURE_2D,
+ goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.LINEAR);
+ gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, goog.webgl.RGBA,
+ goog.webgl.UNSIGNED_BYTE, image);
+ goog.object.set(texturePerImage, uid, texture);
+ }
+ this.textures_[i] = texture;
+ }
+
+ goog.asserts.assert(this.textures_.length == this.groupIndices_.length);
+
+ this.anchorX_ = undefined;
+ this.anchorY_ = undefined;
+ this.height_ = undefined;
+ this.images_ = null;
+ this.imageHeight_ = undefined;
+ this.imageWidth_ = undefined;
+ this.indices_ = null;
+ this.opacity_ = undefined;
+ this.originX_ = undefined;
+ this.originY_ = undefined;
+ this.rotateWithView_ = undefined;
+ this.rotation_ = undefined;
+ this.scale_ = undefined;
+ this.vertices_ = null;
+ this.width_ = undefined;
+};
+
+
+/**
+ * @return {ol.Extent} Extent.
+ */
+ol.render.webgl.ImageReplay.prototype.getExtent = function() {
+ return this.extent_;
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {number} brightness Global brightness.
+ * @param {number} contrast Global contrast.
+ * @param {number} hue Global hue.
+ * @param {number} saturation Global saturation.
+ * @param {Object} skippedFeaturesHash Ids of features to skip.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ImageReplay.prototype.replay = function(context,
+ center, resolution, rotation, size, extent, pixelRatio,
+ opacity, brightness, contrast, hue, saturation, skippedFeaturesHash) {
+ var gl = context.getGL();
+
+ // bind the vertices buffer
+ goog.asserts.assert(!goog.isNull(this.verticesBuffer_));
+ context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_);
+
+ // bind the indices buffer
+ goog.asserts.assert(!goog.isNull(this.indicesBuffer_));
+ context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);
+
+ var useColor = brightness || contrast != 1 || hue || saturation != 1;
+
+ // get the program
+ var fragmentShader, vertexShader;
+ if (useColor) {
+ fragmentShader =
+ ol.render.webgl.imagereplay.shader.ColorFragment.getInstance();
+ vertexShader =
+ ol.render.webgl.imagereplay.shader.ColorVertex.getInstance();
+ } else {
+ fragmentShader =
+ ol.render.webgl.imagereplay.shader.DefaultFragment.getInstance();
+ vertexShader =
+ ol.render.webgl.imagereplay.shader.DefaultVertex.getInstance();
+ }
+ var program = context.getProgram(fragmentShader, vertexShader);
+
+ // get the locations
+ var locations;
+ if (useColor) {
+ if (goog.isNull(this.colorLocations_)) {
+ locations =
+ new ol.render.webgl.imagereplay.shader.Color.Locations(gl, program);
+ this.colorLocations_ = locations;
+ } else {
+ locations = this.colorLocations_;
+ }
+ } else {
+ if (goog.isNull(this.defaultLocations_)) {
+ locations =
+ new ol.render.webgl.imagereplay.shader.Default.Locations(gl, program);
+ this.defaultLocations_ = locations;
+ } else {
+ locations = this.defaultLocations_;
+ }
+ }
+
+ // use the program (FIXME: use the return value)
+ context.useProgram(program);
+
+ // enable the vertex attrib arrays
+ gl.enableVertexAttribArray(locations.a_position);
+ gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT,
+ false, 32, 0);
+
+ gl.enableVertexAttribArray(locations.a_offsets);
+ gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT,
+ false, 32, 8);
+
+ gl.enableVertexAttribArray(locations.a_texCoord);
+ gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT,
+ false, 32, 16);
+
+ gl.enableVertexAttribArray(locations.a_opacity);
+ gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT,
+ false, 32, 24);
+
+ gl.enableVertexAttribArray(locations.a_rotateWithView);
+ gl.vertexAttribPointer(locations.a_rotateWithView, 1, goog.webgl.FLOAT,
+ false, 32, 28);
+
+ // set the "uniform" values
+ var projectionMatrix = this.projectionMatrix_;
+ ol.vec.Mat4.makeTransform2D(projectionMatrix,
+ 0.0, 0.0,
+ 2 / (resolution * size[0]),
+ 2 / (resolution * size[1]),
+ -rotation,
+ -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1]));
+
+ var offsetScaleMatrix = this.offsetScaleMatrix_;
+ goog.vec.Mat4.makeScale(offsetScaleMatrix, 2 / size[0], 2 / size[1], 1);
+
+ var offsetRotateMatrix = this.offsetRotateMatrix_;
+ goog.vec.Mat4.makeIdentity(offsetRotateMatrix);
+ if (rotation !== 0) {
+ goog.vec.Mat4.rotateZ(offsetRotateMatrix, -rotation);
+ }
+
+ gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix);
+ gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, offsetScaleMatrix);
+ gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false,
+ offsetRotateMatrix);
+ gl.uniform1f(locations.u_opacity, opacity);
+ if (useColor) {
+ gl.uniformMatrix4fv(locations.u_colorMatrix, false,
+ this.colorMatrix_.getMatrix(brightness, contrast, hue, saturation));
+ }
+
+ // draw!
+ goog.asserts.assert(this.textures_.length == this.groupIndices_.length);
+ var i, ii, start;
+ for (i = 0, ii = this.textures_.length, start = 0; i < ii; ++i) {
+ gl.bindTexture(goog.webgl.TEXTURE_2D, this.textures_[i]);
+ var end = this.groupIndices_[i];
+ var numItems = end - start;
+ var offsetInBytes = start * (context.hasOESElementIndexUint ? 4 : 2);
+ var elementType = context.hasOESElementIndexUint ?
+ goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT;
+ gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes);
+ start = end;
+ }
+
+ // disable the vertex attrib arrays
+ gl.disableVertexAttribArray(locations.a_position);
+ gl.disableVertexAttribArray(locations.a_offsets);
+ gl.disableVertexAttribArray(locations.a_texCoord);
+ gl.disableVertexAttribArray(locations.a_opacity);
+ gl.disableVertexAttribArray(locations.a_rotateWithView);
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = goog.abstractMethod;
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) {
+ var anchor = imageStyle.getAnchor();
+ goog.asserts.assert(!goog.isNull(anchor));
+ var image = imageStyle.getImage(1);
+ goog.asserts.assert(!goog.isNull(image));
+ var imageSize = imageStyle.getImageSize();
+ goog.asserts.assert(!goog.isNull(imageSize));
+ var opacity = imageStyle.getOpacity();
+ goog.asserts.assert(goog.isDef(opacity));
+ var origin = imageStyle.getOrigin();
+ goog.asserts.assert(!goog.isNull(origin));
+ var rotateWithView = imageStyle.getRotateWithView();
+ goog.asserts.assert(goog.isDef(rotateWithView));
+ var rotation = imageStyle.getRotation();
+ goog.asserts.assert(goog.isDef(rotation));
+ var size = imageStyle.getSize();
+ goog.asserts.assert(!goog.isNull(size));
+ var scale = imageStyle.getScale();
+ goog.asserts.assert(goog.isDef(scale));
+
+ if (this.images_.length === 0) {
+ this.images_.push(image);
+ } else {
+ var currentImage = this.images_[this.images_.length - 1];
+ if (goog.getUid(currentImage) != goog.getUid(image)) {
+ this.groupIndices_.push(this.indices_.length);
+ goog.asserts.assert(this.groupIndices_.length == this.images_.length);
+ this.images_.push(image);
+ }
+ }
+
+ this.anchorX_ = anchor[0];
+ this.anchorY_ = anchor[1];
+ this.height_ = size[1];
+ this.imageHeight_ = imageSize[1];
+ this.imageWidth_ = imageSize[0];
+ this.opacity_ = opacity;
+ this.originX_ = origin[0];
+ this.originY_ = origin[1];
+ this.rotation_ = rotation;
+ this.rotateWithView_ = rotateWithView;
+ this.scale_ = scale;
+ this.width_ = size[0];
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ImageReplay.prototype.setTextStyle = goog.abstractMethod;
+
+
+
+/**
+ * @constructor
+ * @implements {ol.render.IReplayGroup}
+ * @param {number} tolerance Tolerance.
+ * @param {ol.Extent} maxExtent Max extent.
+ * @struct
+ */
+ol.render.webgl.ReplayGroup = function(tolerance, maxExtent) {
+
+ /**
+ * @type {ol.Extent}
+ * @private
+ */
+ this.maxExtent_ = maxExtent;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ this.tolerance_ = tolerance;
+
+ /**
+ * ImageReplay only is supported at this point.
+ * @type {Object.}
+ * @private
+ */
+ this.replays_ = {};
+
+};
+
+
+/**
+ * @param {ol.webgl.Context} context WebGL context.
+ * @return {function()} Delete resources function.
+ */
+ol.render.webgl.ReplayGroup.prototype.getDeleteResourcesFunction =
+ function(context) {
+ var functions = [];
+ var replayKey;
+ for (replayKey in this.replays_) {
+ functions.push(
+ this.replays_[replayKey].getDeleteResourcesFunction(context));
+ }
+ return goog.functions.sequence.apply(null, functions);
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ */
+ol.render.webgl.ReplayGroup.prototype.finish = function(context) {
+ var replayKey;
+ for (replayKey in this.replays_) {
+ this.replays_[replayKey].finish(context);
+ }
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ReplayGroup.prototype.getReplay =
+ function(zIndex, replayType) {
+ var replay = this.replays_[replayType];
+ if (!goog.isDef(replay)) {
+ var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType];
+ goog.asserts.assert(goog.isDef(constructor));
+ replay = new constructor(this.tolerance_, this.maxExtent_);
+ this.replays_[replayType] = replay;
+ }
+ return replay;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.render.webgl.ReplayGroup.prototype.isEmpty = function() {
+ return goog.object.isEmpty(this.replays_);
+};
+
+
+/**
+ * @param {ol.webgl.Context} context Context.
+ * @param {ol.Coordinate} center Center.
+ * @param {number} resolution Resolution.
+ * @param {number} rotation Rotation.
+ * @param {ol.Size} size Size.
+ * @param {ol.Extent} extent Extent.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {number} opacity Global opacity.
+ * @param {number} brightness Global brightness.
+ * @param {number} contrast Global contrast.
+ * @param {number} hue Global hue.
+ * @param {number} saturation Global saturation.
+ * @param {Object} skippedFeaturesHash Ids of features to skip.
+ * @return {T|undefined} Callback result.
+ * @template T
+ */
+ol.render.webgl.ReplayGroup.prototype.replay = function(context,
+ center, resolution, rotation, size, extent, pixelRatio,
+ opacity, brightness, contrast, hue, saturation, skippedFeaturesHash) {
+ var i, ii, replay, result;
+ for (i = 0, ii = ol.render.REPLAY_ORDER.length; i < ii; ++i) {
+ replay = this.replays_[ol.render.REPLAY_ORDER[i]];
+ if (goog.isDef(replay) &&
+ ol.extent.intersects(extent, replay.getExtent())) {
+ result = replay.replay(context,
+ center, resolution, rotation, size, extent, pixelRatio,
+ opacity, brightness, contrast, hue, saturation, skippedFeaturesHash);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return undefined;
+};
+
+
+/**
+ * @const
+ * @private
+ * @type {Object.}
+ */
+ol.render.webgl.BATCH_CONSTRUCTORS_ = {
+ 'Image': ol.render.webgl.ImageReplay
+};
diff --git a/src/ol/renderer/canvas/canvaslayerrenderer.js b/src/ol/renderer/canvas/canvaslayerrenderer.js
index 25538ddf569..74bca84a42d 100644
--- a/src/ol/renderer/canvas/canvaslayerrenderer.js
+++ b/src/ol/renderer/canvas/canvaslayerrenderer.js
@@ -207,6 +207,14 @@ ol.renderer.canvas.Layer.prototype.getTransform = function(frameState) {
};
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.layer.LayerState} layerState Layer state.
+ * @return {boolean} whether composeFrame should be called.
+ */
+ol.renderer.canvas.Layer.prototype.prepareFrame = goog.abstractMethod;
+
+
/**
* @param {ol.Size} size Size.
* @return {boolean} True when the canvas with the current size does not exceed
diff --git a/src/ol/renderer/dom/domlayerrenderer.js b/src/ol/renderer/dom/domlayerrenderer.js
index df94a74b2e6..072e32435ea 100644
--- a/src/ol/renderer/dom/domlayerrenderer.js
+++ b/src/ol/renderer/dom/domlayerrenderer.js
@@ -46,3 +46,11 @@ ol.renderer.dom.Layer.prototype.composeFrame = goog.nullFunction;
ol.renderer.dom.Layer.prototype.getTarget = function() {
return this.target;
};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.layer.LayerState} layerState Layer state.
+ * @return {boolean} whether composeFrame should be called.
+ */
+ol.renderer.dom.Layer.prototype.prepareFrame = goog.abstractMethod;
diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js
index ef9ee9029d4..356561bb0f1 100644
--- a/src/ol/renderer/layerrenderer.js
+++ b/src/ol/renderer/layerrenderer.js
@@ -6,7 +6,6 @@ goog.require('ol.ImageState');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.layer.Layer');
-goog.require('ol.layer.LayerState');
goog.require('ol.source.Source');
goog.require('ol.source.State');
goog.require('ol.source.Tile');
@@ -95,14 +94,6 @@ ol.renderer.Layer.prototype.handleImageChange = function(event) {
};
-/**
- * @param {olx.FrameState} frameState Frame state.
- * @param {ol.layer.LayerState} layerState Layer state.
- * @return {boolean} whether composeFrame should be called.
- */
-ol.renderer.Layer.prototype.prepareFrame = goog.abstractMethod;
-
-
/**
* @protected
*/
diff --git a/src/ol/renderer/webgl/webglimagelayerrenderer.js b/src/ol/renderer/webgl/webglimagelayerrenderer.js
index b8ce5a1fa07..6f31e1cf428 100644
--- a/src/ol/renderer/webgl/webglimagelayerrenderer.js
+++ b/src/ol/renderer/webgl/webglimagelayerrenderer.js
@@ -101,7 +101,7 @@ ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtPixel =
* @inheritDoc
*/
ol.renderer.webgl.ImageLayer.prototype.prepareFrame =
- function(frameState, layerState) {
+ function(frameState, layerState, context) {
var gl = this.getWebGLMapRenderer().getGL();
diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js
index 823ffcc1848..4f09f7b881c 100644
--- a/src/ol/renderer/webgl/webgllayerrenderer.js
+++ b/src/ol/renderer/webgl/webgllayerrenderer.js
@@ -10,7 +10,7 @@ goog.require('ol.render.webgl.Immediate');
goog.require('ol.renderer.Layer');
goog.require('ol.renderer.webgl.map.shader.Color');
goog.require('ol.renderer.webgl.map.shader.Default');
-goog.require('ol.structs.Buffer');
+goog.require('ol.webgl.Buffer');
@@ -26,9 +26,9 @@ ol.renderer.webgl.Layer = function(mapRenderer, layer) {
/**
* @private
- * @type {ol.structs.Buffer}
+ * @type {ol.webgl.Buffer}
*/
- this.arrayBuffer_ = new ol.structs.Buffer([
+ this.arrayBuffer_ = new ol.webgl.Buffer([
-1, -1, 0, 0,
1, -1, 1, 0,
-1, 1, 0, 1,
@@ -237,7 +237,16 @@ ol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ =
function(type, context, frameState) {
var layer = this.getLayer();
if (layer.hasListener(type)) {
- var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio);
+ var viewState = frameState.viewState;
+ var resolution = viewState.resolution;
+ var pixelRatio = frameState.pixelRatio;
+ var extent = frameState.extent;
+ var center = viewState.center;
+ var rotation = viewState.rotation;
+ var size = frameState.size;
+
+ var render = new ol.render.webgl.Immediate(
+ context, center, resolution, rotation, size, extent, pixelRatio);
var composeEvent = new ol.render.Event(
type, layer, render, null, frameState, null, context);
layer.dispatchEvent(composeEvent);
@@ -286,3 +295,12 @@ ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() {
this.framebuffer = null;
this.framebufferDimension = undefined;
};
+
+
+/**
+ * @param {olx.FrameState} frameState Frame state.
+ * @param {ol.layer.LayerState} layerState Layer state.
+ * @param {ol.webgl.Context} context Context.
+ * @return {boolean} whether composeFrame should be called.
+ */
+ol.renderer.webgl.Layer.prototype.prepareFrame = goog.abstractMethod;
diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js
index 12d5982bee1..b7d65306700 100644
--- a/src/ol/renderer/webgl/webglmaprenderer.js
+++ b/src/ol/renderer/webgl/webglmaprenderer.js
@@ -20,13 +20,17 @@ goog.require('ol.dom');
goog.require('ol.layer.Image');
goog.require('ol.layer.Layer');
goog.require('ol.layer.Tile');
+goog.require('ol.layer.Vector');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.webgl.Immediate');
+goog.require('ol.render.webgl.ReplayGroup');
goog.require('ol.renderer.Map');
+goog.require('ol.renderer.vector');
goog.require('ol.renderer.webgl.ImageLayer');
goog.require('ol.renderer.webgl.Layer');
goog.require('ol.renderer.webgl.TileLayer');
+goog.require('ol.renderer.webgl.VectorLayer');
goog.require('ol.source.State');
goog.require('ol.structs.LRUCache');
goog.require('ol.structs.PriorityQueue');
@@ -250,6 +254,8 @@ ol.renderer.webgl.Map.prototype.createLayerRenderer = function(layer) {
return new ol.renderer.webgl.ImageLayer(this, layer);
} else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
return new ol.renderer.webgl.TileLayer(this, layer);
+ } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
+ return new ol.renderer.webgl.VectorLayer(this, layer);
} else {
goog.asserts.fail();
return null;
@@ -266,11 +272,40 @@ ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ =
function(type, frameState) {
var map = this.getMap();
if (map.hasListener(type)) {
- var context = this.getContext();
- var render = new ol.render.webgl.Immediate(context, frameState.pixelRatio);
- var composeEvent = new ol.render.Event(
- type, map, render, null, frameState, null, context);
+ var context = this.context_;
+
+ var extent = frameState.extent;
+ var size = frameState.size;
+ var viewState = frameState.viewState;
+ var pixelRatio = frameState.pixelRatio;
+
+ var resolution = viewState.resolution;
+ var center = viewState.center;
+ var rotation = viewState.rotation;
+ var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio);
+
+ var vectorContext = new ol.render.webgl.Immediate(context,
+ center, resolution, rotation, size, extent, pixelRatio);
+ var replayGroup = new ol.render.webgl.ReplayGroup(tolerance, extent);
+ var composeEvent = new ol.render.Event(type, map, vectorContext,
+ replayGroup, frameState, null, context);
map.dispatchEvent(composeEvent);
+
+ replayGroup.finish(context);
+ if (!replayGroup.isEmpty()) {
+ // use default color values
+ var opacity = 1;
+ var brightness = 0;
+ var contrast = 1;
+ var hue = 0;
+ var saturation = 1;
+ replayGroup.replay(context, center, resolution, rotation, size, extent,
+ pixelRatio, opacity, brightness, contrast, hue, saturation, {});
+ }
+ replayGroup.getDeleteResourcesFunction(context)();
+
+ vectorContext.flush();
+ this.replayGroup = replayGroup;
}
};
@@ -455,7 +490,7 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) {
layerState.sourceState == ol.source.State.READY) {
layerRenderer = this.getLayerRenderer(layerState.layer);
goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer);
- if (layerRenderer.prepareFrame(frameState, layerState)) {
+ if (layerRenderer.prepareFrame(frameState, layerState, context)) {
layerStatesToDraw.push(layerState);
}
}
diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js
index b9cfb26b244..005eebcf382 100644
--- a/src/ol/renderer/webgl/webgltilelayerrenderer.js
+++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js
@@ -16,8 +16,8 @@ goog.require('ol.layer.Tile');
goog.require('ol.math');
goog.require('ol.renderer.webgl.Layer');
goog.require('ol.renderer.webgl.tilelayer.shader');
-goog.require('ol.structs.Buffer');
goog.require('ol.tilecoord');
+goog.require('ol.webgl.Buffer');
@@ -52,9 +52,9 @@ ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) {
/**
* @private
- * @type {ol.structs.Buffer}
+ * @type {ol.webgl.Buffer}
*/
- this.renderArrayBuffer_ = new ol.structs.Buffer([
+ this.renderArrayBuffer_ = new ol.webgl.Buffer([
0, 0, 0, 1,
1, 0, 1, 1,
0, 1, 0, 0,
@@ -107,11 +107,10 @@ ol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() {
* @inheritDoc
*/
ol.renderer.webgl.TileLayer.prototype.prepareFrame =
- function(frameState, layerState) {
+ function(frameState, layerState, context) {
var mapRenderer = this.getWebGLMapRenderer();
- var context = mapRenderer.getContext();
- var gl = mapRenderer.getGL();
+ var gl = context.getGL();
var viewState = frameState.viewState;
var projection = viewState.projection;
diff --git a/src/ol/renderer/webgl/webglvectorlayerrenderer.js b/src/ol/renderer/webgl/webglvectorlayerrenderer.js
new file mode 100644
index 00000000000..b52f6ac512e
--- /dev/null
+++ b/src/ol/renderer/webgl/webglvectorlayerrenderer.js
@@ -0,0 +1,240 @@
+goog.provide('ol.renderer.webgl.VectorLayer');
+
+goog.require('goog.array');
+goog.require('goog.asserts');
+goog.require('goog.events');
+goog.require('ol.ViewHint');
+goog.require('ol.extent');
+goog.require('ol.layer.Vector');
+goog.require('ol.render.webgl.ReplayGroup');
+goog.require('ol.renderer.vector');
+goog.require('ol.renderer.webgl.Layer');
+
+
+
+/**
+ * @constructor
+ * @extends {ol.renderer.webgl.Layer}
+ * @param {ol.renderer.Map} mapRenderer Map renderer.
+ * @param {ol.layer.Vector} vectorLayer Vector layer.
+ */
+ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) {
+
+ goog.base(this, mapRenderer, vectorLayer);
+
+ /**
+ * @private
+ * @type {boolean}
+ */
+ this.dirty_ = false;
+
+ /**
+ * @private
+ * @type {number}
+ */
+ this.renderedRevision_ = -1;
+
+ /**
+ * @private
+ * @type {number}
+ */
+ this.renderedResolution_ = NaN;
+
+ /**
+ * @private
+ * @type {ol.Extent}
+ */
+ this.renderedExtent_ = ol.extent.createEmpty();
+
+ /**
+ * @private
+ * @type {function(ol.Feature, ol.Feature): number|null}
+ */
+ this.renderedRenderOrder_ = null;
+
+ /**
+ * @private
+ * @type {ol.render.webgl.ReplayGroup}
+ */
+ this.replayGroup_ = null;
+
+};
+goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer);
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.composeFrame =
+ function(frameState, layerState, context) {
+ var viewState = frameState.viewState;
+ var replayGroup = this.replayGroup_;
+ if (!goog.isNull(replayGroup) && !replayGroup.isEmpty()) {
+ replayGroup.replay(context,
+ viewState.center, viewState.resolution, viewState.rotation,
+ frameState.size, frameState.extent, frameState.pixelRatio,
+ layerState.opacity, layerState.brightness, layerState.contrast,
+ layerState.hue, layerState.saturation, frameState.skippedFeatureUids);
+ }
+
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() {
+ var replayGroup = this.replayGroup_;
+ if (!goog.isNull(replayGroup)) {
+ var mapRenderer = this.getWebGLMapRenderer();
+ var context = mapRenderer.getContext();
+ replayGroup.getDeleteResourcesFunction(context)();
+ this.replayGroup_ = null;
+ }
+ goog.base(this, 'disposeInternal');
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtPixel =
+ function(coordinate, frameState, callback, thisArg) {
+};
+
+
+/**
+ * Handle changes in image style state.
+ * @param {goog.events.Event} event Image style change event.
+ * @private
+ */
+ol.renderer.webgl.VectorLayer.prototype.handleImageChange_ =
+ function(event) {
+ this.renderIfReadyAndVisible();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.renderer.webgl.VectorLayer.prototype.prepareFrame =
+ function(frameState, layerState, context) {
+
+ var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
+ goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector);
+ var vectorSource = vectorLayer.getSource();
+
+ this.updateAttributions(
+ frameState.attributions, vectorSource.getAttributions());
+ this.updateLogos(frameState, vectorSource);
+
+ if (!this.dirty_ && (frameState.viewHints[ol.ViewHint.ANIMATING] ||
+ frameState.viewHints[ol.ViewHint.INTERACTING])) {
+ return true;
+ }
+
+ var frameStateExtent = frameState.extent;
+ var viewState = frameState.viewState;
+ var projection = viewState.projection;
+ var resolution = viewState.resolution;
+ var pixelRatio = frameState.pixelRatio;
+ var vectorLayerRevision = vectorLayer.getRevision();
+ var vectorLayerRenderOrder = vectorLayer.getRenderOrder();
+ if (!goog.isDef(vectorLayerRenderOrder)) {
+ vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
+ }
+
+ if (!this.dirty_ &&
+ this.renderedResolution_ == resolution &&
+ this.renderedRevision_ == vectorLayerRevision &&
+ this.renderedRenderOrder_ == vectorLayerRenderOrder &&
+ ol.extent.containsExtent(this.renderedExtent_, frameStateExtent)) {
+ return true;
+ }
+
+ var extent = this.renderedExtent_;
+ var xBuffer = ol.extent.getWidth(frameStateExtent) / 4;
+ var yBuffer = ol.extent.getHeight(frameStateExtent) / 4;
+ extent[0] = frameStateExtent[0] - xBuffer;
+ extent[1] = frameStateExtent[1] - yBuffer;
+ extent[2] = frameStateExtent[2] + xBuffer;
+ extent[3] = frameStateExtent[3] + yBuffer;
+
+ if (!goog.isNull(this.replayGroup_)) {
+ frameState.postRenderFunctions.push(
+ this.replayGroup_.getDeleteResourcesFunction(context));
+ }
+
+ this.dirty_ = false;
+
+ var replayGroup = new ol.render.webgl.ReplayGroup(
+ ol.renderer.vector.getTolerance(resolution, pixelRatio),
+ extent);
+ vectorSource.loadFeatures(extent, resolution, projection);
+ var renderFeature =
+ /**
+ * @param {ol.Feature} feature Feature.
+ * @this {ol.renderer.webgl.VectorLayer}
+ */
+ function(feature) {
+ var styles;
+ if (goog.isDef(feature.getStyleFunction())) {
+ styles = feature.getStyleFunction().call(feature, resolution);
+ } else if (goog.isDef(vectorLayer.getStyleFunction())) {
+ styles = vectorLayer.getStyleFunction()(feature, resolution);
+ }
+ if (goog.isDefAndNotNull(styles)) {
+ var dirty = this.renderFeature(
+ feature, resolution, pixelRatio, styles, replayGroup);
+ this.dirty_ = this.dirty_ || dirty;
+ }
+ };
+ if (!goog.isNull(vectorLayerRenderOrder)) {
+ /** @type {Array.} */
+ var features = [];
+ vectorSource.forEachFeatureInExtentAtResolution(extent, resolution,
+ /**
+ * @param {ol.Feature} feature Feature.
+ */
+ function(feature) {
+ features.push(feature);
+ }, this);
+ goog.array.sort(features, vectorLayerRenderOrder);
+ goog.array.forEach(features, renderFeature, this);
+ } else {
+ vectorSource.forEachFeatureInExtentAtResolution(
+ extent, resolution, renderFeature, this);
+ }
+ replayGroup.finish(context);
+
+ this.renderedResolution_ = resolution;
+ this.renderedRevision_ = vectorLayerRevision;
+ this.renderedRenderOrder_ = vectorLayerRenderOrder;
+ this.replayGroup_ = replayGroup;
+
+ return true;
+};
+
+
+/**
+ * @param {ol.Feature} feature Feature.
+ * @param {number} resolution Resolution.
+ * @param {number} pixelRatio Pixel ratio.
+ * @param {Array.} styles Array of styles
+ * @param {ol.render.webgl.ReplayGroup} replayGroup Replay group.
+ * @return {boolean} `true` if an image is loading.
+ */
+ol.renderer.webgl.VectorLayer.prototype.renderFeature =
+ function(feature, resolution, pixelRatio, styles, replayGroup) {
+ if (!goog.isDefAndNotNull(styles)) {
+ return false;
+ }
+ var i, ii, loading = false;
+ for (i = 0, ii = styles.length; i < ii; ++i) {
+ loading = ol.renderer.vector.renderFeature(
+ replayGroup, feature, styles[i],
+ ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
+ feature, this.handleImageChange_, this) || loading;
+ }
+ return loading;
+};
diff --git a/src/ol/structs/buffer.js b/src/ol/structs/buffer.js
deleted file mode 100644
index 29ddb99b560..00000000000
--- a/src/ol/structs/buffer.js
+++ /dev/null
@@ -1,257 +0,0 @@
-goog.provide('ol.structs.Buffer');
-
-goog.require('goog.array');
-goog.require('goog.asserts');
-goog.require('goog.webgl');
-goog.require('ol');
-goog.require('ol.structs.IntegerSet');
-
-
-/**
- * @enum {number}
- */
-ol.structs.BufferUsage = {
- STATIC_DRAW: goog.webgl.STATIC_DRAW,
- STREAM_DRAW: goog.webgl.STREAM_DRAW,
- DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW
-};
-
-
-
-/**
- * @constructor
- * @param {Array.=} opt_arr Array.
- * @param {number=} opt_used Used.
- * @param {number=} opt_usage Usage.
- * @struct
- */
-ol.structs.Buffer = function(opt_arr, opt_used, opt_usage) {
-
- /**
- * @private
- * @type {Array.}
- */
- this.arr_ = goog.isDef(opt_arr) ? opt_arr : [];
-
- /**
- * @private
- * @type {Array.}
- */
- this.dirtySets_ = [];
-
- /**
- * @private
- * @type {ol.structs.IntegerSet}
- */
- this.freeSet_ = new ol.structs.IntegerSet();
-
- var used = goog.isDef(opt_used) ? opt_used : this.arr_.length;
- if (used < this.arr_.length) {
- this.freeSet_.addRange(used, this.arr_.length);
- }
- if (ol.BUFFER_REPLACE_UNUSED_ENTRIES_WITH_NANS) {
- var arr = this.arr_;
- var n = arr.length;
- var i;
- for (i = used; i < n; ++i) {
- arr[i] = NaN;
- }
- }
-
- /**
- * @private
- * @type {?Float32Array}
- */
- this.split32_ = null;
-
- /**
- * @private
- * @type {ol.structs.IntegerSet}
- */
- this.split32DirtySet_ = null;
-
- /**
- * @private
- * @type {number}
- */
- this.usage_ = goog.isDef(opt_usage) ?
- opt_usage : ol.structs.BufferUsage.STATIC_DRAW;
-
-};
-
-
-/**
- * @param {number} size Size.
- * @return {number} Offset.
- */
-ol.structs.Buffer.prototype.allocate = function(size) {
- goog.asserts.assert(size > 0);
- var offset = this.freeSet_.findRange(size);
- goog.asserts.assert(offset != -1); // FIXME
- this.freeSet_.removeRange(offset, offset + size);
- return offset;
-};
-
-
-/**
- * @param {Array.} values Values.
- * @return {number} Offset.
- */
-ol.structs.Buffer.prototype.add = function(values) {
- var size = values.length;
- var offset = this.allocate(size);
- var i;
- for (i = 0; i < size; ++i) {
- this.arr_[offset + i] = values[i];
- }
- this.markDirty(size, offset);
- return offset;
-};
-
-
-/**
- * @param {ol.structs.IntegerSet} dirtySet Dirty set.
- */
-ol.structs.Buffer.prototype.addDirtySet = function(dirtySet) {
- goog.asserts.assert(!goog.array.contains(this.dirtySets_, dirtySet));
- this.dirtySets_.push(dirtySet);
-};
-
-
-/**
- * @param {function(this: T, number, number)} f Callback.
- * @param {T=} opt_this The object to use as `this` in `f`.
- * @template T
- */
-ol.structs.Buffer.prototype.forEachRange = function(f, opt_this) {
- if (this.arr_.length !== 0) {
- this.freeSet_.forEachRangeInverted(0, this.arr_.length, f, opt_this);
- }
-};
-
-
-/**
- * @return {Array.} Array.
- */
-ol.structs.Buffer.prototype.getArray = function() {
- return this.arr_;
-};
-
-
-/**
- * @return {number} Count.
- */
-ol.structs.Buffer.prototype.getCount = function() {
- return this.arr_.length - this.freeSet_.getSize();
-};
-
-
-/**
- * @return {ol.structs.IntegerSet} Free set.
- */
-ol.structs.Buffer.prototype.getFreeSet = function() {
- return this.freeSet_;
-};
-
-
-/**
- * Returns a Float32Array twice the length of the buffer containing each value
- * split into two 32-bit floating point values that, when added together,
- * approximate the original value. Even indicies contain the high bits, odd
- * indicies contain the low bits.
- * @see http://blogs.agi.com/insight3d/index.php/2008/09/03/precisions-precisions/
- * @return {Float32Array} Split.
- */
-ol.structs.Buffer.prototype.getSplit32 = function() {
- var arr = this.arr_;
- var n = arr.length;
- if (goog.isNull(this.split32DirtySet_)) {
- this.split32DirtySet_ = new ol.structs.IntegerSet([0, n]);
- this.addDirtySet(this.split32DirtySet_);
- }
- if (goog.isNull(this.split32_)) {
- this.split32_ = new Float32Array(2 * n);
- }
- var split32 = this.split32_;
- this.split32DirtySet_.forEachRange(function(start, stop) {
- var doubleHigh, i, j, value;
- for (i = start, j = 2 * start; i < stop; ++i, j += 2) {
- value = arr[i];
- if (value < 0) {
- doubleHigh = 65536 * Math.floor(-value / 65536);
- split32[j] = -doubleHigh;
- split32[j + 1] = value + doubleHigh;
- } else {
- doubleHigh = 65536 * Math.floor(value / 65536);
- split32[j] = doubleHigh;
- split32[j + 1] = value - doubleHigh;
- }
- }
- });
- this.split32DirtySet_.clear();
- return this.split32_;
-};
-
-
-/**
- * @return {number} Usage.
- */
-ol.structs.Buffer.prototype.getUsage = function() {
- return this.usage_;
-};
-
-
-/**
- * @param {number} size Size.
- * @param {number} offset Offset.
- */
-ol.structs.Buffer.prototype.markDirty = function(size, offset) {
- var i, ii;
- for (i = 0, ii = this.dirtySets_.length; i < ii; ++i) {
- this.dirtySets_[i].addRange(offset, offset + size);
- }
-};
-
-
-/**
- * @param {number} size Size.
- * @param {number} offset Offset.
- */
-ol.structs.Buffer.prototype.remove = function(size, offset) {
- var i, ii;
- this.freeSet_.addRange(offset, offset + size);
- for (i = 0, ii = this.dirtySets_.length; i < ii; ++i) {
- this.dirtySets_[i].removeRange(offset, offset + size);
- }
- if (ol.BUFFER_REPLACE_UNUSED_ENTRIES_WITH_NANS) {
- var arr = this.arr_;
- for (i = 0; i < size; ++i) {
- arr[offset + i] = NaN;
- }
- }
-};
-
-
-/**
- * @param {ol.structs.IntegerSet} dirtySet Dirty set.
- */
-ol.structs.Buffer.prototype.removeDirtySet = function(dirtySet) {
- var removed = goog.array.remove(this.dirtySets_, dirtySet);
- goog.asserts.assert(removed);
-};
-
-
-/**
- * @param {Array.} values Values.
- * @param {number} offset Offset.
- */
-ol.structs.Buffer.prototype.set = function(values, offset) {
- var arr = this.arr_;
- var n = values.length;
- goog.asserts.assert(0 <= offset && offset + n <= arr.length);
- var i;
- for (i = 0; i < n; ++i) {
- arr[offset + i] = values[i];
- }
- this.markDirty(n, offset);
-};
diff --git a/src/ol/structs/checksum.js b/src/ol/structs/checksum.js
new file mode 100644
index 00000000000..ff72308ad21
--- /dev/null
+++ b/src/ol/structs/checksum.js
@@ -0,0 +1,16 @@
+goog.provide('ol.structs.IHasChecksum');
+
+
+
+/**
+ * @interface
+ */
+ol.structs.IHasChecksum = function() {
+};
+
+
+/**
+ * @return {string} The checksum.
+ */
+ol.structs.IHasChecksum.prototype.getChecksum = function() {
+};
diff --git a/src/ol/structs/integerset.js b/src/ol/structs/integerset.js
deleted file mode 100644
index de716c6842f..00000000000
--- a/src/ol/structs/integerset.js
+++ /dev/null
@@ -1,330 +0,0 @@
-goog.provide('ol.structs.IntegerSet');
-
-goog.require('goog.asserts');
-
-
-
-/**
- * A set of integers represented as a set of integer ranges.
- * This implementation is designed for the case when the number of distinct
- * integer ranges is small.
- * @constructor
- * @struct
- * @param {Array.=} opt_arr Array.
- */
-ol.structs.IntegerSet = function(opt_arr) {
-
- /**
- * @private
- * @type {Array.}
- */
- this.arr_ = goog.isDef(opt_arr) ? opt_arr : [];
-
- if (goog.DEBUG) {
- this.assertValid();
- }
-
-};
-
-
-/**
- * @param {number} addStart Start.
- * @param {number} addStop Stop.
- */
-ol.structs.IntegerSet.prototype.addRange = function(addStart, addStop) {
- goog.asserts.assert(addStart <= addStop);
- if (addStart == addStop) {
- return;
- }
- var arr = this.arr_;
- var n = arr.length;
- var i;
- for (i = 0; i < n; i += 2) {
- if (addStart <= arr[i]) {
- // FIXME check if splice is really needed
- arr.splice(i, 0, addStart, addStop);
- this.compactRanges_();
- return;
- }
- }
- arr.push(addStart, addStop);
- this.compactRanges_();
-};
-
-
-/**
- * FIXME empty description for jsdoc
- */
-ol.structs.IntegerSet.prototype.assertValid = function() {
- var arr = this.arr_;
- var n = arr.length;
- goog.asserts.assert(n % 2 === 0);
- var i;
- for (i = 1; i < n; ++i) {
- goog.asserts.assert(arr[i] > arr[i - 1]);
- }
-};
-
-
-/**
- * FIXME empty description for jsdoc
- */
-ol.structs.IntegerSet.prototype.clear = function() {
- this.arr_.length = 0;
-};
-
-
-/**
- * @private
- */
-ol.structs.IntegerSet.prototype.compactRanges_ = function() {
- var arr = this.arr_;
- var n = arr.length;
- var rangeIndex = 0;
- var i;
- for (i = 0; i < n; i += 2) {
- if (arr[i] == arr[i + 1]) {
- // pass
- } else if (rangeIndex > 0 &&
- arr[rangeIndex - 2] <= arr[i] &&
- arr[i] <= arr[rangeIndex - 1]) {
- arr[rangeIndex - 1] = Math.max(arr[rangeIndex - 1], arr[i + 1]);
- } else {
- arr[rangeIndex++] = arr[i];
- arr[rangeIndex++] = arr[i + 1];
- }
- }
- arr.length = rangeIndex;
-};
-
-
-/**
- * Finds the start of smallest range that is at least of length minSize, or -1
- * if no such range exists.
- * @param {number} minSize Minimum size.
- * @return {number} Index.
- */
-ol.structs.IntegerSet.prototype.findRange = function(minSize) {
- goog.asserts.assert(minSize > 0);
- var arr = this.arr_;
- var n = arr.length;
- var bestIndex = -1;
- var bestSize, i, size;
- for (i = 0; i < n; i += 2) {
- size = arr[i + 1] - arr[i];
- if (size == minSize) {
- return arr[i];
- } else if (size > minSize && (bestIndex == -1 || size < bestSize)) {
- bestIndex = arr[i];
- bestSize = size;
- }
- }
- return bestIndex;
-};
-
-
-/**
- * Calls f with each integer range.
- * @param {function(this: T, number, number)} f Callback.
- * @param {T=} opt_this The object to use as `this` in `f`.
- * @template T
- */
-ol.structs.IntegerSet.prototype.forEachRange = function(f, opt_this) {
- var arr = this.arr_;
- var n = arr.length;
- var i;
- for (i = 0; i < n; i += 2) {
- f.call(opt_this, arr[i], arr[i + 1]);
- }
-};
-
-
-/**
- * Calls f with each integer range not in [start, stop) - 'this'.
- * @param {number} start Start.
- * @param {number} stop Stop.
- * @param {function(this: T, number, number)} f Callback.
- * @param {T=} opt_this The object to use as `this` in `f`.
- * @template T
- */
-ol.structs.IntegerSet.prototype.forEachRangeInverted =
- function(start, stop, f, opt_this) {
- goog.asserts.assert(start < stop);
- var arr = this.arr_;
- var n = arr.length;
- if (n === 0) {
- f.call(opt_this, start, stop);
- } else {
- if (start < arr[0]) {
- f.call(opt_this, start, arr[0]);
- }
- var i;
- for (i = 1; i < n - 1; i += 2) {
- f.call(opt_this, arr[i], arr[i + 1]);
- }
- if (arr[n - 1] < stop) {
- f.call(opt_this, arr[n - 1], stop);
- }
- }
-};
-
-
-/**
- * @return {Array.} Array.
- */
-ol.structs.IntegerSet.prototype.getArray = function() {
- return this.arr_;
-};
-
-
-/**
- * Returns the first element in the set, or -1 if the set is empty.
- * @return {number} Start.
- */
-ol.structs.IntegerSet.prototype.getFirst = function() {
- return this.arr_.length === 0 ? -1 : this.arr_[0];
-};
-
-
-/**
- * Returns the first integer after the last element in the set, or -1 if the
- * set is empty.
- * @return {number} Last.
- */
-ol.structs.IntegerSet.prototype.getLast = function() {
- var n = this.arr_.length;
- return n === 0 ? -1 : this.arr_[n - 1];
-};
-
-
-/**
- * Returns the number of integers in the set.
- * @return {number} Size.
- */
-ol.structs.IntegerSet.prototype.getSize = function() {
- var arr = this.arr_;
- var n = arr.length;
- var size = 0;
- var i;
- for (i = 0; i < n; i += 2) {
- size += arr[i + 1] - arr[i];
- }
- return size;
-};
-
-
-/**
- * @param {number} start Start.
- * @param {number} stop Stop.
- * @return {boolean} Intersects range.
- */
-ol.structs.IntegerSet.prototype.intersectsRange = function(start, stop) {
- goog.asserts.assert(start <= stop);
- if (start == stop) {
- return false;
- } else {
- var arr = this.arr_;
- var n = arr.length;
- var i = 0;
- for (i = 0; i < n; i += 2) {
- if (arr[i] <= start && start < arr[i + 1] ||
- arr[i] < stop && stop - 1 < arr[i + 1] ||
- start < arr[i] && arr[i + 1] <= stop) {
- return true;
- }
- }
- return false;
- }
-};
-
-
-/**
- * @return {boolean} Is empty.
- */
-ol.structs.IntegerSet.prototype.isEmpty = function() {
- return this.arr_.length === 0;
-};
-
-
-/**
- * @return {Array.} Array.
- */
-ol.structs.IntegerSet.prototype.pack = function() {
- return this.arr_;
-};
-
-
-/**
- * @param {number} removeStart Start.
- * @param {number} removeStop Stop.
- */
-ol.structs.IntegerSet.prototype.removeRange =
- function(removeStart, removeStop) {
- // FIXME this could be more efficient
- goog.asserts.assert(removeStart <= removeStop);
- var arr = this.arr_;
- var n = arr.length;
- var i;
- for (i = 0; i < n; i += 2) {
- if (removeStop < arr[i] || arr[i + 1] < removeStart) {
- continue;
- } else if (arr[i] > removeStop) {
- break;
- }
- if (removeStart < arr[i]) {
- if (removeStop == arr[i]) {
- break;
- } else if (removeStop < arr[i + 1]) {
- arr[i] = Math.max(arr[i], removeStop);
- break;
- } else {
- arr.splice(i, 2);
- i -= 2;
- n -= 2;
- }
- } else if (removeStart == arr[i]) {
- if (removeStop < arr[i + 1]) {
- arr[i] = removeStop;
- break;
- } else if (removeStop == arr[i + 1]) {
- arr.splice(i, 2);
- break;
- } else {
- arr.splice(i, 2);
- i -= 2;
- n -= 2;
- }
- } else {
- if (removeStop < arr[i + 1]) {
- arr.splice(i, 2, arr[i], removeStart, removeStop, arr[i + 1]);
- break;
- } else if (removeStop == arr[i + 1]) {
- arr[i + 1] = removeStart;
- break;
- } else {
- arr[i + 1] = removeStart;
- }
- }
- }
- this.compactRanges_();
-};
-
-
-if (goog.DEBUG) {
-
- /**
- * @return {string} String.
- */
- ol.structs.IntegerSet.prototype.toString = function() {
- var arr = this.arr_;
- var n = arr.length;
- var result = new Array(n / 2);
- var resultIndex = 0;
- var i;
- for (i = 0; i < n; i += 2) {
- result[resultIndex++] = arr[i] + '-' + arr[i + 1];
- }
- return result.join(', ');
- };
-
-}
diff --git a/src/ol/style/atlasmanager.js b/src/ol/style/atlasmanager.js
new file mode 100644
index 00000000000..a3cb260ecad
--- /dev/null
+++ b/src/ol/style/atlasmanager.js
@@ -0,0 +1,440 @@
+goog.provide('ol.style.Atlas');
+goog.provide('ol.style.AtlasManager');
+
+goog.require('goog.asserts');
+goog.require('goog.dom');
+goog.require('goog.dom.TagName');
+goog.require('goog.object');
+goog.require('ol');
+
+
+/**
+ * Provides information for an image inside an atlas manager.
+ * `offsetX` and `offsetY` is the position of the image inside
+ * the atlas image `image`.
+ * `hitOffsetX` and `hitOffsetY` ist the position of the hit-detection image
+ * inside the hit-detection atlas image `hitImage` (only when a hit-detection
+ * image was created for this image).
+ * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement,
+ * hitOffsetX: number, hitOffsetY: number, hitImage: HTMLCanvasElement}}
+ */
+ol.style.AtlasManagerInfo;
+
+
+
+/**
+ * Manages the creation of image atlases.
+ *
+ * Images added to this manager will be inserted into an atlas, which
+ * will be used for rendering.
+ * The `size` given in the constructor is the size for the first
+ * atlas. After that, when new atlases are created, they will have
+ * twice the size as the latest atlas (until `maxSize` is reached).
+ *
+ * If an application uses many images or very large images, it is recommended
+ * to set a higher `size` value to avoid the creation of too many atlases.
+ *
+ * @constructor
+ * @struct
+ * @api
+ * @param {olx.style.AtlasManagerOptions=} opt_options Options.
+ */
+ol.style.AtlasManager = function(opt_options) {
+
+ var options = goog.isDef(opt_options) ? opt_options : {};
+
+ /**
+ * The size in pixels of the latest atlas image.
+ * @private
+ * @type {number}
+ */
+ this.currentSize_ = goog.isDef(options.initialSize) ?
+ options.initialSize : ol.INITIAL_ATLAS_SIZE;
+
+ /**
+ * The maximum size in pixels of atlas images.
+ * @private
+ * @type {number}
+ */
+ this.maxSize_ = goog.isDef(options.maxSize) ?
+ options.maxSize : ol.MAX_ATLAS_SIZE != -1 ?
+ ol.MAX_ATLAS_SIZE : goog.isDef(ol.WEBGL_MAX_TEXTURE_SIZE) ?
+ ol.WEBGL_MAX_TEXTURE_SIZE : 2048;
+
+ /**
+ * The size in pixels between images.
+ * @private
+ * @type {number}
+ */
+ this.space_ = goog.isDef(options.space) ? options.space : 1;
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.atlases_ = [new ol.style.Atlas(this.currentSize_, this.space_)];
+
+ /**
+ * The size in pixels of the latest atlas image for hit-detection images.
+ * @private
+ * @type {number}
+ */
+ this.currentHitSize_ = this.currentSize_;
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.hitAtlases_ = [new ol.style.Atlas(this.currentHitSize_, this.space_)];
+};
+
+
+/**
+ * @param {string} id The identifier of the entry to check.
+ * @return {?ol.style.AtlasManagerInfo} The position and atlas image for the
+ * entry, or `null` if the entry is not part of the atlas manager.
+ */
+ol.style.AtlasManager.prototype.getInfo = function(id) {
+ /** @type {?ol.style.AtlasInfo} */
+ var info = this.getInfo_(this.atlases_, id);
+
+ if (goog.isNull(info)) {
+ return null;
+ }
+ /** @type {?ol.style.AtlasInfo} */
+ var hitInfo = this.getInfo_(this.hitAtlases_, id);
+
+ return this.mergeInfos_(info, hitInfo);
+};
+
+
+/**
+ * @private
+ * @param {Array.} atlases The atlases to search.
+ * @param {string} id The identifier of the entry to check.
+ * @return {?ol.style.AtlasInfo} The position and atlas image for the entry,
+ * or `null` if the entry is not part of the atlases.
+ */
+ol.style.AtlasManager.prototype.getInfo_ = function(atlases, id) {
+ var atlas, info, i, ii;
+ for (i = 0, ii = atlases.length; i < ii; ++i) {
+ atlas = atlases[i];
+ info = atlas.get(id);
+ if (!goog.isNull(info)) {
+ return info;
+ }
+ }
+ return null;
+};
+
+
+/**
+ * @private
+ * @param {ol.style.AtlasInfo} info The info for the real image.
+ * @param {?ol.style.AtlasInfo} hitInfo The info for the hit-detection
+ * image.
+ * @return {?ol.style.AtlasManagerInfo} The position and atlas image for the
+ * entry, or `null` if the entry is not part of the atlases.
+ */
+ol.style.AtlasManager.prototype.mergeInfos_ = function(info, hitInfo) {
+ return /** @type {ol.style.AtlasManagerInfo} */ ({
+ offsetX: info.offsetX,
+ offsetY: info.offsetY,
+ image: info.image,
+ hitOffsetX: goog.isNull(hitInfo) ? undefined : hitInfo.offsetX,
+ hitOffsetY: goog.isNull(hitInfo) ? undefined : hitInfo.offsetY,
+ hitImage: goog.isNull(hitInfo) ? undefined : hitInfo.image
+ });
+};
+
+
+/**
+ * Add an image to the atlas manager.
+ *
+ * If an entry for the given id already exists, the entry will
+ * be overridden (but the space on the atlas graphic will not be freed).
+ *
+ * If `renderHitCallback` is provided, the image (or the hit-detection version
+ * of the image) will be rendered into a separate hit-detection atlas image.
+ *
+ * @param {string} id The identifier of the entry to add.
+ * @param {number} width The width.
+ * @param {number} height The height.
+ * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
+ * Called to render the new image onto an atlas image.
+ * @param {function(CanvasRenderingContext2D, number, number)=}
+ * opt_renderHitCallback Called to render a hit-detection image onto a hit
+ * detection atlas image.
+ * @param {Object=} opt_this Value to use as `this` when executing
+ * `renderCallback` and `renderHitCallback`.
+ * @return {?ol.style.AtlasManagerInfo} The position and atlas image for the
+ * entry, or `null` if the image is too big.
+ */
+ol.style.AtlasManager.prototype.add =
+ function(id, width, height,
+ renderCallback, opt_renderHitCallback, opt_this) {
+ if (width + this.space_ > this.maxSize_ ||
+ height + this.space_ > this.maxSize_) {
+ return null;
+ }
+
+ /** @type {?ol.style.AtlasInfo} */
+ var info = this.add_(false,
+ id, width, height, renderCallback, opt_this);
+ if (goog.isNull(info)) {
+ return null;
+ }
+
+ /** @type {?ol.style.AtlasInfo} */
+ var hitInfo = null;
+ if (goog.isDef(opt_renderHitCallback)) {
+ hitInfo = this.add_(true,
+ id, width, height, opt_renderHitCallback, opt_this);
+ }
+ return this.mergeInfos_(info, hitInfo);
+};
+
+
+/**
+ * @private
+ * @param {boolean} isHitAtlas If the hit-detection atlases are used.
+ * @param {string} id The identifier of the entry to add.
+ * @param {number} width The width.
+ * @param {number} height The height.
+ * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
+ * Called to render the new image onto an atlas image.
+ * @param {Object=} opt_this Value to use as `this` when executing
+ * `renderCallback` and `renderHitCallback`.
+ * @return {?ol.style.AtlasInfo} The position and atlas image for the entry,
+ * or `null` if the image is too big.
+ */
+ol.style.AtlasManager.prototype.add_ =
+ function(isHitAtlas, id, width, height,
+ renderCallback, opt_this) {
+ var atlases = (isHitAtlas) ? this.hitAtlases_ : this.atlases_;
+ var atlas, info, i, ii;
+ for (i = 0, ii = atlases.length; i < ii; ++i) {
+ atlas = atlases[i];
+ info = atlas.add(id, width, height, renderCallback, opt_this);
+ if (!goog.isNull(info)) {
+ return info;
+ } else if (goog.isNull(info) && i === ii - 1) {
+ // the entry could not be added to one of the existing atlases,
+ // create a new atlas that is twice as big and try to add to this one.
+ var size;
+ if (isHitAtlas) {
+ size = Math.min(this.currentHitSize_ * 2, this.maxSize_);
+ this.currentHitSize_ = size;
+ } else {
+ size = Math.min(this.currentSize_ * 2, this.maxSize_);
+ this.currentSize_ = size;
+ }
+ atlas = new ol.style.Atlas(size, this.space_);
+ atlases.push(atlas);
+ // run the loop another time
+ ++ii;
+ }
+ }
+ goog.asserts.fail();
+};
+
+
+/**
+ * Provides information for an image inside an atlas.
+ * `offsetX` and `offsetY` are the position of the image inside
+ * the atlas image `image`.
+ * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}}
+ */
+ol.style.AtlasInfo;
+
+
+
+/**
+ * This class facilitates the creation of image atlases.
+ *
+ * Images added to an atlas will be rendered onto a single
+ * atlas canvas. The distribution of images on the canvas is
+ * managed with the bin packing algorithm described in:
+ * http://www.blackpawn.com/texts/lightmaps/
+ *
+ * @constructor
+ * @struct
+ * @param {number} size The size in pixels of the sprite image.
+ * @param {number} space The space in pixels between images.
+ * Because texture coordinates are float values, the edges of
+ * images might not be completely correct (in a way that the
+ * edges overlap when being rendered). To avoid this we add a
+ * padding around each image.
+ */
+ol.style.Atlas = function(size, space) {
+
+ /**
+ * @private
+ * @type {number}
+ */
+ this.space_ = space;
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}];
+
+ /**
+ * @private
+ * @type {Object.}
+ */
+ this.entries_ = {};
+
+ /**
+ * @private
+ * @type {HTMLCanvasElement}
+ */
+ this.canvas_ = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ this.canvas_.width = size;
+ this.canvas_.height = size;
+
+ /**
+ * @private
+ * @type {CanvasRenderingContext2D}
+ */
+ this.context_ = /** @type {CanvasRenderingContext2D} */
+ (this.canvas_.getContext('2d'));
+};
+
+
+/**
+ * @param {string} id The identifier of the entry to check.
+ * @return {?ol.style.AtlasInfo}
+ */
+ol.style.Atlas.prototype.get = function(id) {
+ return /** @type {?ol.style.AtlasInfo} */ (
+ goog.object.get(this.entries_, id, null));
+};
+
+
+/**
+ * @param {string} id The identifier of the entry to add.
+ * @param {number} width The width.
+ * @param {number} height The height.
+ * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
+ * Called to render the new image onto an atlas image.
+ * @param {Object=} opt_this Value to use as `this` when executing
+ * `renderCallback`.
+ * @return {?ol.style.AtlasInfo} The position and atlas image for the entry.
+ */
+ol.style.Atlas.prototype.add =
+ function(id, width, height, renderCallback, opt_this) {
+ var block, i, ii;
+ for (i = 0, ii = this.emptyBlocks_.length; i < ii; ++i) {
+ block = this.emptyBlocks_[i];
+ if (block.width >= width + this.space_ &&
+ block.height >= height + this.space_) {
+ // we found a block that is big enough for our entry
+ var entry = {
+ offsetX: block.x + this.space_,
+ offsetY: block.y + this.space_,
+ image: this.canvas_
+ };
+ this.entries_[id] = entry;
+
+ // render the image on the atlas image
+ renderCallback.call(opt_this, this.context_,
+ block.x + this.space_, block.y + this.space_);
+
+ // split the block after the insertion, either horizontally or vertically
+ this.split_(i, block, width + this.space_, height + this.space_);
+
+ return entry;
+ }
+ }
+
+ // there is no space for the new entry in this atlas
+ return null;
+};
+
+
+/**
+ * @private
+ * @param {number} index The index of the block.
+ * @param {ol.style.Atlas.Block} block The block to split.
+ * @param {number} width The width of the entry to insert.
+ * @param {number} height The height of the entry to insert.
+ */
+ol.style.Atlas.prototype.split_ =
+ function(index, block, width, height) {
+ var deltaWidth = block.width - width;
+ var deltaHeight = block.height - height;
+
+ /** @type {ol.style.Atlas.Block} */
+ var newBlock1;
+ /** @type {ol.style.Atlas.Block} */
+ var newBlock2;
+
+ if (deltaWidth > deltaHeight) {
+ // split vertically
+ // block right of the inserted entry
+ newBlock1 = {
+ x: block.x + width,
+ y: block.y,
+ width: block.width - width,
+ height: block.height
+ };
+
+ // block below the inserted entry
+ newBlock2 = {
+ x: block.x,
+ y: block.y + height,
+ width: width,
+ height: block.height - height
+ };
+ this.updateBlocks_(index, newBlock1, newBlock2);
+ } else {
+ // split horizontally
+ // block right of the inserted entry
+ newBlock1 = {
+ x: block.x + width,
+ y: block.y,
+ width: block.width - width,
+ height: height
+ };
+
+ // block below the inserted entry
+ newBlock2 = {
+ x: block.x,
+ y: block.y + height,
+ width: block.width,
+ height: block.height - height
+ };
+ this.updateBlocks_(index, newBlock1, newBlock2);
+ }
+};
+
+
+/**
+ * Remove the old block and insert new blocks at the same array position.
+ * The new blocks are inserted at the same position, so that splitted
+ * blocks (that are potentially smaller) are filled first.
+ * @private
+ * @param {number} index The index of the block to remove.
+ * @param {ol.style.Atlas.Block} newBlock1 The 1st block to add.
+ * @param {ol.style.Atlas.Block} newBlock2 The 2nd block to add.
+ */
+ol.style.Atlas.prototype.updateBlocks_ =
+ function(index, newBlock1, newBlock2) {
+ var args = [index, 1];
+ if (newBlock1.width > 0 && newBlock1.height > 0) {
+ args.push(newBlock1);
+ }
+ if (newBlock2.width > 0 && newBlock2.height > 0) {
+ args.push(newBlock2);
+ }
+ this.emptyBlocks_.splice.apply(this.emptyBlocks_, args);
+};
+
+
+/**
+ * @typedef {{x: number, y: number, width: number, height: number}}
+ */
+ol.style.Atlas.Block;
diff --git a/src/ol/style/circlestyle.js b/src/ol/style/circlestyle.js
index 8901d8362b3..96632d06aaa 100644
--- a/src/ol/style/circlestyle.js
+++ b/src/ol/style/circlestyle.js
@@ -1,10 +1,12 @@
goog.provide('ol.style.Circle');
+goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('ol.color');
goog.require('ol.has');
goog.require('ol.render.canvas');
+goog.require('ol.structs.IHasChecksum');
goog.require('ol.style.Fill');
goog.require('ol.style.Image');
goog.require('ol.style.ImageState');
@@ -19,18 +21,24 @@ goog.require('ol.style.Stroke');
* @constructor
* @param {olx.style.CircleOptions=} opt_options Options.
* @extends {ol.style.Image}
+ * @implements {ol.structs.IHasChecksum}
* @api
*/
ol.style.Circle = function(opt_options) {
var options = goog.isDef(opt_options) ? opt_options : {};
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.checksums_ = null;
+
/**
* @private
* @type {HTMLCanvasElement}
*/
- this.canvas_ = /** @type {HTMLCanvasElement} */
- (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ this.canvas_ = null;
/**
* @private
@@ -46,9 +54,9 @@ ol.style.Circle = function(opt_options) {
/**
* @private
- * @type {Array.}
+ * @type {ol.style.Stroke}
*/
- this.origin_ = [0, 0];
+ this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null;
/**
* @private
@@ -58,23 +66,41 @@ ol.style.Circle = function(opt_options) {
/**
* @private
- * @type {ol.style.Stroke}
+ * @type {Array.}
*/
- this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null;
+ this.origin_ = [0, 0];
- var size = this.render_();
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.hitDetectionOrigin_ = [0, 0];
/**
* @private
* @type {Array.}
*/
- this.anchor_ = [size / 2, size / 2];
+ this.anchor_ = null;
/**
* @private
* @type {ol.Size}
*/
- this.size_ = [size, size];
+ this.size_ = null;
+
+ /**
+ * @private
+ * @type {ol.Size}
+ */
+ this.imageSize_ = null;
+
+ /**
+ * @private
+ * @type {ol.Size}
+ */
+ this.hitDetectionImageSize_ = null;
+
+ this.render_(options.atlasManager);
/**
* @type {boolean}
@@ -138,6 +164,22 @@ ol.style.Circle.prototype.getImageState = function() {
};
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getImageSize = function() {
+ return this.imageSize_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getHitDetectionImageSize = function() {
+ return this.hitDetectionImageSize_;
+};
+
+
/**
* @inheritDoc
* @api
@@ -147,6 +189,14 @@ ol.style.Circle.prototype.getOrigin = function() {
};
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getHitDetectionOrigin = function() {
+ return this.hitDetectionOrigin_;
+};
+
+
/**
* @return {number} Radius.
* @api
@@ -192,80 +242,217 @@ ol.style.Circle.prototype.load = goog.nullFunction;
ol.style.Circle.prototype.unlistenImageChange = goog.nullFunction;
+/**
+ * @typedef {{strokeStyle: (string|undefined), strokeWidth: number,
+ * size: number, lineDash: Array.}}
+ */
+ol.style.Circle.RenderOptions;
+
+
/**
* @private
- * @return {number} Size.
+ * @param {ol.style.AtlasManager|undefined} atlasManager
*/
-ol.style.Circle.prototype.render_ = function() {
- var canvas = this.canvas_;
- var strokeStyle, strokeWidth, lineDash;
+ol.style.Circle.prototype.render_ = function(atlasManager) {
+ var imageSize;
+ var lineDash = null;
+ var strokeStyle;
+ var strokeWidth = 0;
- if (goog.isNull(this.stroke_)) {
- strokeWidth = 0;
- } else {
+ if (!goog.isNull(this.stroke_)) {
strokeStyle = ol.color.asString(this.stroke_.getColor());
strokeWidth = this.stroke_.getWidth();
if (!goog.isDef(strokeWidth)) {
strokeWidth = ol.render.canvas.defaultLineWidth;
}
+ lineDash = this.stroke_.getLineDash();
+ if (!ol.has.CANVAS_LINE_DASH) {
+ lineDash = null;
+ }
}
+
var size = 2 * (this.radius_ + strokeWidth) + 1;
- // draw the circle on the canvas
+ /** @type {ol.style.Circle.RenderOptions} */
+ var renderOptions = {
+ strokeStyle: strokeStyle,
+ strokeWidth: strokeWidth,
+ size: size,
+ lineDash: lineDash
+ };
+
+ if (!goog.isDef(atlasManager)) {
+ // no atlas manager is used, create a new canvas
+ this.canvas_ = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ this.canvas_.height = size;
+ this.canvas_.width = size;
- canvas.height = size;
- canvas.width = size;
+ // canvas.width and height are rounded to the closest integer
+ size = this.canvas_.width;
+ imageSize = size;
- // canvas.width and height are rounded to the closest integer
- size = canvas.width;
+ // draw the circle on the canvas
+ var context = /** @type {CanvasRenderingContext2D} */
+ (this.canvas_.getContext('2d'));
+ this.draw_(renderOptions, context, 0, 0);
- var context = /** @type {CanvasRenderingContext2D} */
- (canvas.getContext('2d'));
- context.arc(size / 2, size / 2, this.radius_, 0, 2 * Math.PI, true);
+ this.createHitDetectionCanvas_(renderOptions);
+ } else {
+ // an atlas manager is used, add the symbol to an atlas
+ size = Math.round(size);
+
+ var hasCustomHitDetectionImage = goog.isNull(this.fill_);
+ var renderHitDetectionCallback;
+ if (hasCustomHitDetectionImage) {
+ // render the hit-detection image into a separate atlas image
+ renderHitDetectionCallback =
+ goog.bind(this.drawHitDetectionCanvas_, this, renderOptions);
+ }
+
+ var id = this.getChecksum();
+ var info = atlasManager.add(
+ id, size, size, goog.bind(this.draw_, this, renderOptions),
+ renderHitDetectionCallback);
+ goog.asserts.assert(info !== null, 'circle radius is too large');
+
+ this.canvas_ = info.image;
+ this.origin_ = [info.offsetX, info.offsetY];
+ imageSize = info.image.width;
+
+ if (hasCustomHitDetectionImage) {
+ this.hitDetectionCanvas_ = info.hitImage;
+ this.hitDetectionOrigin_ = [info.hitOffsetX, info.hitOffsetY];
+ this.hitDetectionImageSize_ =
+ [info.hitImage.width, info.hitImage.height];
+ } else {
+ this.hitDetectionCanvas_ = this.canvas_;
+ this.hitDetectionOrigin_ = this.origin_;
+ this.hitDetectionImageSize_ = [imageSize, imageSize];
+ }
+ }
+
+ this.anchor_ = [size / 2, size / 2];
+ this.size_ = [size, size];
+ this.imageSize_ = [imageSize, imageSize];
+};
+
+
+/**
+ * @private
+ * @param {ol.style.Circle.RenderOptions} renderOptions
+ * @param {CanvasRenderingContext2D} context
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.Circle.prototype.draw_ = function(renderOptions, context, x, y) {
+ // reset transform
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // then move to (x, y)
+ context.translate(x, y);
+
+ context.beginPath();
+ context.arc(
+ renderOptions.size / 2, renderOptions.size / 2,
+ this.radius_, 0, 2 * Math.PI, true);
if (!goog.isNull(this.fill_)) {
context.fillStyle = ol.color.asString(this.fill_.getColor());
context.fill();
}
if (!goog.isNull(this.stroke_)) {
- context.strokeStyle = strokeStyle;
- lineDash = this.stroke_.getLineDash();
- if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) {
- context.setLineDash(lineDash);
+ context.strokeStyle = renderOptions.strokeStyle;
+ context.lineWidth = renderOptions.strokeWidth;
+ if (!goog.isNull(renderOptions.lineDash)) {
+ context.setLineDash(renderOptions.lineDash);
}
- context.lineWidth = strokeWidth;
context.stroke();
}
+ context.closePath();
+};
- // deal with the hit detection canvas
+/**
+ * @private
+ * @param {ol.style.Circle.RenderOptions} renderOptions
+ */
+ol.style.Circle.prototype.createHitDetectionCanvas_ = function(renderOptions) {
+ this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
if (!goog.isNull(this.fill_)) {
- this.hitDetectionCanvas_ = canvas;
- } else {
- this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */
- (goog.dom.createElement(goog.dom.TagName.CANVAS));
- canvas = this.hitDetectionCanvas_;
+ this.hitDetectionCanvas_ = this.canvas_;
+ return;
+ }
- canvas.height = size;
- canvas.width = size;
+ // if no fill style is set, create an extra hit-detection image with a
+ // default fill style
+ this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ var canvas = this.hitDetectionCanvas_;
- context = /** @type {CanvasRenderingContext2D} */
- (canvas.getContext('2d'));
- context.arc(size / 2, size / 2, this.radius_, 0, 2 * Math.PI, true);
+ canvas.height = renderOptions.size;
+ canvas.width = renderOptions.size;
- context.fillStyle = ol.render.canvas.defaultFillStyle;
- context.fill();
- if (!goog.isNull(this.stroke_)) {
- context.strokeStyle = strokeStyle;
- lineDash = this.stroke_.getLineDash();
- if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) {
- context.setLineDash(lineDash);
- }
- context.lineWidth = strokeWidth;
- context.stroke();
+ var context = /** @type {CanvasRenderingContext2D} */
+ (canvas.getContext('2d'));
+ this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
+};
+
+
+/**
+ * @private
+ * @param {ol.style.Circle.RenderOptions} renderOptions
+ * @param {CanvasRenderingContext2D} context
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.Circle.prototype.drawHitDetectionCanvas_ =
+ function(renderOptions, context, x, y) {
+ // reset transform
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // then move to (x, y)
+ context.translate(x, y);
+
+ context.beginPath();
+ context.arc(
+ renderOptions.size / 2, renderOptions.size / 2,
+ this.radius_, 0, 2 * Math.PI, true);
+
+ context.fillStyle = ol.render.canvas.defaultFillStyle;
+ context.fill();
+ if (!goog.isNull(this.stroke_)) {
+ context.strokeStyle = renderOptions.strokeStyle;
+ context.lineWidth = renderOptions.strokeWidth;
+ if (!goog.isNull(renderOptions.lineDash)) {
+ context.setLineDash(renderOptions.lineDash);
}
+ context.stroke();
+ }
+ context.closePath();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Circle.prototype.getChecksum = function() {
+ var strokeChecksum = !goog.isNull(this.stroke_) ?
+ this.stroke_.getChecksum() : '-';
+ var fillChecksum = !goog.isNull(this.fill_) ?
+ this.fill_.getChecksum() : '-';
+
+ var recalculate = goog.isNull(this.checksums_) ||
+ (strokeChecksum != this.checksums_[1] ||
+ fillChecksum != this.checksums_[2] ||
+ this.radius_ != this.checksums_[3]);
+
+ if (recalculate) {
+ var checksum = 'c' + strokeChecksum + fillChecksum +
+ (goog.isDef(this.radius_) ? this.radius_.toString() : '-');
+ this.checksums_ = [checksum, strokeChecksum, fillChecksum, this.radius_];
}
- return size;
+ return this.checksums_[0];
};
diff --git a/src/ol/style/fillstyle.js b/src/ol/style/fillstyle.js
index 6714445868a..4fd62d77e79 100644
--- a/src/ol/style/fillstyle.js
+++ b/src/ol/style/fillstyle.js
@@ -1,5 +1,8 @@
goog.provide('ol.style.Fill');
+goog.require('ol.color');
+goog.require('ol.structs.IHasChecksum');
+
/**
@@ -8,6 +11,7 @@ goog.provide('ol.style.Fill');
*
* @constructor
* @param {olx.style.FillOptions=} opt_options Options.
+ * @implements {ol.structs.IHasChecksum}
* @api
*/
ol.style.Fill = function(opt_options) {
@@ -19,6 +23,12 @@ ol.style.Fill = function(opt_options) {
* @type {ol.Color|string}
*/
this.color_ = goog.isDef(options.color) ? options.color : null;
+
+ /**
+ * @private
+ * @type {string|undefined}
+ */
+ this.checksum_ = undefined;
};
@@ -39,4 +49,18 @@ ol.style.Fill.prototype.getColor = function() {
*/
ol.style.Fill.prototype.setColor = function(color) {
this.color_ = color;
+ this.checksum_ = undefined;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Fill.prototype.getChecksum = function() {
+ if (!goog.isDef(this.checksum_)) {
+ this.checksum_ = 'f' + (!goog.isNull(this.color_) ?
+ ol.color.asString(this.color_) : '-');
+ }
+
+ return this.checksum_;
};
diff --git a/src/ol/style/iconstyle.js b/src/ol/style/iconstyle.js
index c82881420a4..9913c6412a1 100644
--- a/src/ol/style/iconstyle.js
+++ b/src/ol/style/iconstyle.js
@@ -246,6 +246,14 @@ ol.style.Icon.prototype.getImageSize = function() {
};
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.getHitDetectionImageSize = function() {
+ return this.getImageSize();
+};
+
+
/**
* @inheritDoc
*/
@@ -293,6 +301,14 @@ ol.style.Icon.prototype.getOrigin = function() {
};
+/**
+ * @inheritDoc
+ */
+ol.style.Icon.prototype.getHitDetectionOrigin = function() {
+ return this.getOrigin();
+};
+
+
/**
* @return {string|undefined} Image src.
* @api
diff --git a/src/ol/style/imagestyle.js b/src/ol/style/imagestyle.js
index 35c3a3ac16c..be7c2707884 100644
--- a/src/ol/style/imagestyle.js
+++ b/src/ol/style/imagestyle.js
@@ -126,6 +126,13 @@ ol.style.Image.prototype.getAnchor = goog.abstractMethod;
ol.style.Image.prototype.getImage = goog.abstractMethod;
+/**
+ * @param {number} pixelRatio Pixel ratio.
+ * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
+ */
+ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod;
+
+
/**
* @return {ol.style.ImageState} Image state.
*/
@@ -133,10 +140,15 @@ ol.style.Image.prototype.getImageState = goog.abstractMethod;
/**
- * @param {number} pixelRatio Pixel ratio.
- * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
+ * @return {ol.Size} Image size.
*/
-ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod;
+ol.style.Image.prototype.getImageSize = goog.abstractMethod;
+
+
+/**
+ * @return {ol.Size} Size of the hit-detection image.
+ */
+ol.style.Image.prototype.getHitDetectionImageSize = goog.abstractMethod;
/**
@@ -146,6 +158,13 @@ ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod;
ol.style.Image.prototype.getOrigin = goog.abstractMethod;
+/**
+ * @function
+ * @return {Array.} Origin for the hit-detection image.
+ */
+ol.style.Image.prototype.getHitDetectionOrigin = goog.abstractMethod;
+
+
/**
* @function
* @return {ol.Size} Size.
diff --git a/src/ol/style/regularshapestyle.js b/src/ol/style/regularshapestyle.js
index 579e71f6f7e..f7744cc3ffe 100644
--- a/src/ol/style/regularshapestyle.js
+++ b/src/ol/style/regularshapestyle.js
@@ -22,18 +22,24 @@ goog.require('ol.style.Stroke');
* @constructor
* @param {olx.style.RegularShapeOptions=} opt_options Options.
* @extends {ol.style.Image}
+ * @implements {ol.structs.IHasChecksum}
* @api
*/
ol.style.RegularShape = function(opt_options) {
var options = goog.isDef(opt_options) ? opt_options : {};
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.checksums_ = null;
+
/**
* @private
* @type {HTMLCanvasElement}
*/
- this.canvas_ = /** @type {HTMLCanvasElement} */
- (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ this.canvas_ = null;
/**
* @private
@@ -53,6 +59,12 @@ ol.style.RegularShape = function(opt_options) {
*/
this.origin_ = [0, 0];
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.hitDetectionOrigin_ = [0, 0];
+
/**
* @private
* @type {number}
@@ -88,19 +100,31 @@ ol.style.RegularShape = function(opt_options) {
*/
this.stroke_ = goog.isDef(options.stroke) ? options.stroke : null;
- var size = this.render_();
-
/**
* @private
* @type {Array.}
*/
- this.anchor_ = [size / 2, size / 2];
+ this.anchor_ = null;
/**
* @private
* @type {ol.Size}
*/
- this.size_ = [size, size];
+ this.size_ = null;
+
+ /**
+ * @private
+ * @type {ol.Size}
+ */
+ this.imageSize_ = null;
+
+ /**
+ * @private
+ * @type {ol.Size}
+ */
+ this.hitDetectionImageSize_ = null;
+
+ this.render_(options.atlasManager);
/**
* @type {boolean}
@@ -155,6 +179,22 @@ ol.style.RegularShape.prototype.getImage = function(pixelRatio) {
};
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getImageSize = function() {
+ return this.imageSize_;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getHitDetectionImageSize = function() {
+ return this.hitDetectionImageSize_;
+};
+
+
/**
* @inheritDoc
*/
@@ -172,6 +212,14 @@ ol.style.RegularShape.prototype.getOrigin = function() {
};
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getHitDetectionOrigin = function() {
+ return this.hitDetectionOrigin_;
+};
+
+
/**
* @return {number} Radius.
* @api
@@ -226,37 +274,117 @@ ol.style.RegularShape.prototype.load = goog.nullFunction;
ol.style.RegularShape.prototype.unlistenImageChange = goog.nullFunction;
+/**
+ * @typedef {{strokeStyle: (string|undefined), strokeWidth: number,
+ * size: number, lineDash: Array.}}
+ */
+ol.style.RegularShape.RenderOptions;
+
+
/**
* @private
- * @return {number} Size.
+ * @param {ol.style.AtlasManager|undefined} atlasManager
*/
-ol.style.RegularShape.prototype.render_ = function() {
- var canvas = this.canvas_;
- var strokeStyle, strokeWidth, lineDash;
+ol.style.RegularShape.prototype.render_ = function(atlasManager) {
+ var imageSize;
+ var lineDash = null;
+ var strokeStyle;
+ var strokeWidth = 0;
- if (goog.isNull(this.stroke_)) {
- strokeWidth = 0;
- } else {
+ if (!goog.isNull(this.stroke_)) {
strokeStyle = ol.color.asString(this.stroke_.getColor());
strokeWidth = this.stroke_.getWidth();
if (!goog.isDef(strokeWidth)) {
strokeWidth = ol.render.canvas.defaultLineWidth;
}
+ lineDash = this.stroke_.getLineDash();
+ if (!ol.has.CANVAS_LINE_DASH) {
+ lineDash = null;
+ }
}
var size = 2 * (this.radius_ + strokeWidth) + 1;
- // draw the regular shape on the canvas
+ /** @type {ol.style.RegularShape.RenderOptions} */
+ var renderOptions = {
+ strokeStyle: strokeStyle,
+ strokeWidth: strokeWidth,
+ size: size,
+ lineDash: lineDash
+ };
+
+ if (!goog.isDef(atlasManager)) {
+ // no atlas manager is used, create a new canvas
+ this.canvas_ = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
- canvas.height = size;
- canvas.width = size;
+ this.canvas_.height = size;
+ this.canvas_.width = size;
- // canvas.width and height are rounded to the closest integer
- size = canvas.width;
+ // canvas.width and height are rounded to the closest integer
+ size = this.canvas_.width;
+ imageSize = size;
- var context = /** @type {CanvasRenderingContext2D} */
- (canvas.getContext('2d'));
+ var context = /** @type {CanvasRenderingContext2D} */
+ (this.canvas_.getContext('2d'));
+ this.draw_(renderOptions, context, 0, 0);
+
+ this.createHitDetectionCanvas_(renderOptions);
+ } else {
+ // an atlas manager is used, add the symbol to an atlas
+ size = Math.round(size);
+
+ var hasCustomHitDetectionImage = goog.isNull(this.fill_);
+ var renderHitDetectionCallback;
+ if (hasCustomHitDetectionImage) {
+ // render the hit-detection image into a separate atlas image
+ renderHitDetectionCallback =
+ goog.bind(this.drawHitDetectionCanvas_, this, renderOptions);
+ }
+
+ var id = this.getChecksum();
+ var info = atlasManager.add(
+ id, size, size, goog.bind(this.draw_, this, renderOptions),
+ renderHitDetectionCallback);
+ goog.asserts.assert(!goog.isNull(info), 'shape size is too large');
+
+ this.canvas_ = info.image;
+ this.origin_ = [info.offsetX, info.offsetY];
+ imageSize = info.image.width;
+
+ if (hasCustomHitDetectionImage) {
+ this.hitDetectionCanvas_ = info.hitImage;
+ this.hitDetectionOrigin_ = [info.hitOffsetX, info.hitOffsetY];
+ this.hitDetectionImageSize_ =
+ [info.hitImage.width, info.hitImage.height];
+ } else {
+ this.hitDetectionCanvas_ = this.canvas_;
+ this.hitDetectionOrigin_ = this.origin_;
+ this.hitDetectionImageSize_ = [imageSize, imageSize];
+ }
+ }
+
+ this.anchor_ = [size / 2, size / 2];
+ this.size_ = [size, size];
+ this.imageSize_ = [imageSize, imageSize];
+};
+
+
+/**
+ * @private
+ * @param {ol.style.Circle.RenderOptions} renderOptions
+ * @param {CanvasRenderingContext2D} context
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.RegularShape.prototype.draw_ = function(renderOptions, context, x, y) {
var i, angle0, radiusC;
+ // reset transform
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // then move to (x, y)
+ context.translate(x, y);
+
context.beginPath();
if (this.radius2_ !== this.radius_) {
this.points_ = 2 * this.points_;
@@ -264,8 +392,8 @@ ol.style.RegularShape.prototype.render_ = function() {
for (i = 0; i <= this.points_; i++) {
angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
- context.lineTo(size / 2 + radiusC * Math.cos(angle0),
- size / 2 + radiusC * Math.sin(angle0));
+ context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
+ renderOptions.size / 2 + radiusC * Math.sin(angle0));
}
if (!goog.isNull(this.fill_)) {
@@ -273,52 +401,111 @@ ol.style.RegularShape.prototype.render_ = function() {
context.fill();
}
if (!goog.isNull(this.stroke_)) {
- context.strokeStyle = strokeStyle;
- lineDash = this.stroke_.getLineDash();
- if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) {
- context.setLineDash(lineDash);
+ context.strokeStyle = renderOptions.strokeStyle;
+ context.lineWidth = renderOptions.strokeWidth;
+ if (!goog.isNull(renderOptions.lineDash)) {
+ context.setLineDash(renderOptions.lineDash);
}
- context.lineWidth = strokeWidth;
context.stroke();
}
+ context.closePath();
+};
- // deal with the hit detection canvas
+/**
+ * @private
+ * @param {ol.style.RegularShape.RenderOptions} renderOptions
+ */
+ol.style.RegularShape.prototype.createHitDetectionCanvas_ =
+ function(renderOptions) {
+ this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
if (!goog.isNull(this.fill_)) {
- this.hitDetectionCanvas_ = canvas;
- } else {
- this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */
- (goog.dom.createElement(goog.dom.TagName.CANVAS));
- canvas = this.hitDetectionCanvas_;
+ this.hitDetectionCanvas_ = this.canvas_;
+ return;
+ }
- canvas.height = size;
- canvas.width = size;
+ // if no fill style is set, create an extra hit-detection image with a
+ // default fill style
+ this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */
+ (goog.dom.createElement(goog.dom.TagName.CANVAS));
+ var canvas = this.hitDetectionCanvas_;
- context = /** @type {CanvasRenderingContext2D} */
- (canvas.getContext('2d'));
- context.beginPath();
- if (this.radius2_ !== this.radius_) {
- this.points_ = 2 * this.points_;
- }
- for (i = 0; i <= this.points_; i++) {
- angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
- radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
- context.lineTo(size / 2 + radiusC * Math.cos(angle0),
- size / 2 + radiusC * Math.sin(angle0));
- }
+ canvas.height = renderOptions.size;
+ canvas.width = renderOptions.size;
- context.fillStyle = ol.render.canvas.defaultFillStyle;
- context.fill();
- if (!goog.isNull(this.stroke_)) {
- context.strokeStyle = strokeStyle;
- lineDash = this.stroke_.getLineDash();
- if (ol.has.CANVAS_LINE_DASH && !goog.isNull(lineDash)) {
- context.setLineDash(lineDash);
- }
- context.lineWidth = strokeWidth;
- context.stroke();
+ var context = /** @type {CanvasRenderingContext2D} */
+ (canvas.getContext('2d'));
+ this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
+};
+
+
+/**
+ * @private
+ * @param {ol.style.RegularShape.RenderOptions} renderOptions
+ * @param {CanvasRenderingContext2D} context
+ * @param {number} x The origin for the symbol (x).
+ * @param {number} y The origin for the symbol (y).
+ */
+ol.style.RegularShape.prototype.drawHitDetectionCanvas_ =
+ function(renderOptions, context, x, y) {
+ // reset transform
+ context.setTransform(1, 0, 0, 1, 0, 0);
+
+ // then move to (x, y)
+ context.translate(x, y);
+
+ context.beginPath();
+ if (this.radius2_ !== this.radius_) {
+ this.points_ = 2 * this.points_;
+ }
+ var i, radiusC, angle0;
+ for (i = 0; i <= this.points_; i++) {
+ angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
+ radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
+ context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
+ renderOptions.size / 2 + radiusC * Math.sin(angle0));
+ }
+
+ context.fillStyle = ol.render.canvas.defaultFillStyle;
+ context.fill();
+ if (!goog.isNull(this.stroke_)) {
+ context.strokeStyle = renderOptions.strokeStyle;
+ context.lineWidth = renderOptions.strokeWidth;
+ if (!goog.isNull(renderOptions.lineDash)) {
+ context.setLineDash(renderOptions.lineDash);
}
+ context.stroke();
+ }
+ context.closePath();
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.RegularShape.prototype.getChecksum = function() {
+ var strokeChecksum = !goog.isNull(this.stroke_) ?
+ this.stroke_.getChecksum() : '-';
+ var fillChecksum = !goog.isNull(this.fill_) ?
+ this.fill_.getChecksum() : '-';
+
+ var recalculate = goog.isNull(this.checksums_) ||
+ (strokeChecksum != this.checksums_[1] ||
+ fillChecksum != this.checksums_[2] ||
+ this.radius_ != this.checksums_[3] ||
+ this.radius2_ != this.checksums_[4] ||
+ this.angle_ != this.checksums_[5] ||
+ this.points_ != this.checksums_[6]);
+
+ if (recalculate) {
+ var checksum = 'r' + strokeChecksum + fillChecksum +
+ (goog.isDef(this.radius_) ? this.radius_.toString() : '-') +
+ (goog.isDef(this.radius2_) ? this.radius2_.toString() : '-') +
+ (goog.isDef(this.angle_) ? this.angle_.toString() : '-') +
+ (goog.isDef(this.points_) ? this.points_.toString() : '-');
+ this.checksums_ = [checksum, strokeChecksum, fillChecksum,
+ this.radius_, this.radius2_, this.angle_, this.points_];
}
- return size;
+ return this.checksums_[0];
};
diff --git a/src/ol/style/strokestyle.js b/src/ol/style/strokestyle.js
index 15e99bf2603..a679df28763 100644
--- a/src/ol/style/strokestyle.js
+++ b/src/ol/style/strokestyle.js
@@ -1,5 +1,10 @@
goog.provide('ol.style.Stroke');
+goog.require('goog.crypt');
+goog.require('goog.crypt.Md5');
+goog.require('ol.color');
+goog.require('ol.structs.IHasChecksum');
+
/**
@@ -11,6 +16,7 @@ goog.provide('ol.style.Stroke');
*
* @constructor
* @param {olx.style.StrokeOptions=} opt_options Options.
+ * @implements {ol.structs.IHasChecksum}
* @api
*/
ol.style.Stroke = function(opt_options) {
@@ -52,6 +58,12 @@ ol.style.Stroke = function(opt_options) {
* @type {number|undefined}
*/
this.width_ = options.width;
+
+ /**
+ * @private
+ * @type {string|undefined}
+ */
+ this.checksum_ = undefined;
};
@@ -117,6 +129,7 @@ ol.style.Stroke.prototype.getWidth = function() {
*/
ol.style.Stroke.prototype.setColor = function(color) {
this.color_ = color;
+ this.checksum_ = undefined;
};
@@ -128,6 +141,7 @@ ol.style.Stroke.prototype.setColor = function(color) {
*/
ol.style.Stroke.prototype.setLineCap = function(lineCap) {
this.lineCap_ = lineCap;
+ this.checksum_ = undefined;
};
@@ -139,6 +153,7 @@ ol.style.Stroke.prototype.setLineCap = function(lineCap) {
*/
ol.style.Stroke.prototype.setLineDash = function(lineDash) {
this.lineDash_ = lineDash;
+ this.checksum_ = undefined;
};
@@ -150,6 +165,7 @@ ol.style.Stroke.prototype.setLineDash = function(lineDash) {
*/
ol.style.Stroke.prototype.setLineJoin = function(lineJoin) {
this.lineJoin_ = lineJoin;
+ this.checksum_ = undefined;
};
@@ -161,6 +177,7 @@ ol.style.Stroke.prototype.setLineJoin = function(lineJoin) {
*/
ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) {
this.miterLimit_ = miterLimit;
+ this.checksum_ = undefined;
};
@@ -172,4 +189,33 @@ ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) {
*/
ol.style.Stroke.prototype.setWidth = function(width) {
this.width_ = width;
+ this.checksum_ = undefined;
+};
+
+
+/**
+ * @inheritDoc
+ */
+ol.style.Stroke.prototype.getChecksum = function() {
+ if (!goog.isDef(this.checksum_)) {
+ var raw = 's' +
+ (!goog.isNull(this.color_) ?
+ ol.color.asString(this.color_) : '-') + ',' +
+ (goog.isDef(this.lineCap_) ?
+ this.lineCap_.toString() : '-') + ',' +
+ (!goog.isNull(this.lineDash_) ?
+ this.lineDash_.toString() : '-') + ',' +
+ (goog.isDef(this.lineJoin_) ?
+ this.lineJoin_ : '-') + ',' +
+ (goog.isDef(this.miterLimit_) ?
+ this.miterLimit_.toString() : '-') + ',' +
+ (goog.isDef(this.width_) ?
+ this.width_.toString() : '-');
+
+ var md5 = new goog.crypt.Md5();
+ md5.update(raw);
+ this.checksum_ = goog.crypt.byteArrayToString(md5.digest());
+ }
+
+ return this.checksum_;
};
diff --git a/src/ol/webgl/buffer.js b/src/ol/webgl/buffer.js
new file mode 100644
index 00000000000..a39194ccf6b
--- /dev/null
+++ b/src/ol/webgl/buffer.js
@@ -0,0 +1,56 @@
+goog.provide('ol.webgl.Buffer');
+
+goog.require('goog.array');
+goog.require('goog.webgl');
+goog.require('ol');
+
+
+/**
+ * @enum {number}
+ */
+ol.webgl.BufferUsage = {
+ STATIC_DRAW: goog.webgl.STATIC_DRAW,
+ STREAM_DRAW: goog.webgl.STREAM_DRAW,
+ DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW
+};
+
+
+
+/**
+ * @constructor
+ * @param {Array.=} opt_arr Array.
+ * @param {number=} opt_usage Usage.
+ * @struct
+ */
+ol.webgl.Buffer = function(opt_arr, opt_usage) {
+
+ /**
+ * @private
+ * @type {Array.}
+ */
+ this.arr_ = goog.isDef(opt_arr) ? opt_arr : [];
+
+ /**
+ * @private
+ * @type {number}
+ */
+ this.usage_ = goog.isDef(opt_usage) ?
+ opt_usage : ol.webgl.BufferUsage.STATIC_DRAW;
+
+};
+
+
+/**
+ * @return {Array.} Array.
+ */
+ol.webgl.Buffer.prototype.getArray = function() {
+ return this.arr_;
+};
+
+
+/**
+ * @return {number} Usage.
+ */
+ol.webgl.Buffer.prototype.getUsage = function() {
+ return this.usage_;
+};
diff --git a/src/ol/webgl/context.js b/src/ol/webgl/context.js
index 307837dc669..6b91f6c2a7d 100644
--- a/src/ol/webgl/context.js
+++ b/src/ol/webgl/context.js
@@ -1,18 +1,18 @@
goog.provide('ol.webgl.Context');
+goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.log');
goog.require('goog.object');
-goog.require('ol.structs.Buffer');
-goog.require('ol.structs.IntegerSet');
+goog.require('ol');
+goog.require('ol.webgl.Buffer');
goog.require('ol.webgl.WebGLContextEventType');
/**
- * @typedef {{buf: ol.structs.Buffer,
- * buffer: WebGLBuffer,
- * dirtySet: ol.structs.IntegerSet}}
+ * @typedef {{buf: ol.webgl.Buffer,
+ * buffer: WebGLBuffer}}
*/
ol.webgl.BufferCacheEntry;
@@ -66,6 +66,18 @@ ol.webgl.Context = function(canvas, gl) {
*/
this.currentProgram_ = null;
+ /**
+ * @type {boolean}
+ */
+ this.hasOESElementIndexUint = goog.array.contains(
+ ol.WEBGL_EXTENSIONS, 'OES_element_index_uint');
+
+ // use the OES_element_index_uint extension if available
+ if (this.hasOESElementIndexUint) {
+ var ext = gl.getExtension('OES_element_index_uint');
+ goog.asserts.assert(!goog.isNull(ext));
+ }
+
goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.LOST,
this.handleWebGLContextLost, false, this);
goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.RESTORED,
@@ -75,8 +87,11 @@ ol.webgl.Context = function(canvas, gl) {
/**
+ * Just bind the buffer if it's in the cache. Otherwise create
+ * the WebGL buffer, bind it, populate it, and add an entry to
+ * the cache.
* @param {number} target Target.
- * @param {ol.structs.Buffer} buf Buffer.
+ * @param {ol.webgl.Buffer} buf Buffer.
*/
ol.webgl.Context.prototype.bindBuffer = function(target, buf) {
var gl = this.getGL();
@@ -85,45 +100,37 @@ ol.webgl.Context.prototype.bindBuffer = function(target, buf) {
if (bufferKey in this.bufferCache_) {
var bufferCacheEntry = this.bufferCache_[bufferKey];
gl.bindBuffer(target, bufferCacheEntry.buffer);
- bufferCacheEntry.dirtySet.forEachRange(function(start, stop) {
- // FIXME check if slice is really efficient here
- var slice = arr.slice(start, stop);
- gl.bufferSubData(
- target,
- start,
- target == goog.webgl.ARRAY_BUFFER ?
- new Float32Array(slice) :
- new Uint16Array(slice));
- });
- bufferCacheEntry.dirtySet.clear();
} else {
var buffer = gl.createBuffer();
gl.bindBuffer(target, buffer);
- gl.bufferData(
- target,
- target == goog.webgl.ARRAY_BUFFER ?
- new Float32Array(arr) : new Uint16Array(arr),
- buf.getUsage());
- var dirtySet = new ol.structs.IntegerSet();
- buf.addDirtySet(dirtySet);
+ goog.asserts.assert(target == goog.webgl.ARRAY_BUFFER ||
+ target == goog.webgl.ELEMENT_ARRAY_BUFFER);
+ var /** @type {ArrayBufferView} */ arrayBuffer;
+ if (target == goog.webgl.ARRAY_BUFFER) {
+ arrayBuffer = new Float32Array(arr);
+ } else if (target == goog.webgl.ELEMENT_ARRAY_BUFFER) {
+ arrayBuffer = this.hasOESElementIndexUint ?
+ new Uint32Array(arr) : new Uint16Array(arr);
+ } else {
+ goog.asserts.fail();
+ }
+ gl.bufferData(target, arrayBuffer, buf.getUsage());
this.bufferCache_[bufferKey] = {
buf: buf,
- buffer: buffer,
- dirtySet: dirtySet
+ buffer: buffer
};
}
};
/**
- * @param {ol.structs.Buffer} buf Buffer.
+ * @param {ol.webgl.Buffer} buf Buffer.
*/
ol.webgl.Context.prototype.deleteBuffer = function(buf) {
var gl = this.getGL();
var bufferKey = goog.getUid(buf);
goog.asserts.assert(bufferKey in this.bufferCache_);
var bufferCacheEntry = this.bufferCache_[bufferKey];
- bufferCacheEntry.buf.removeDirtySet(bufferCacheEntry.dirtySet);
if (!gl.isContextLost()) {
gl.deleteBuffer(bufferCacheEntry.buffer);
}
@@ -135,9 +142,6 @@ ol.webgl.Context.prototype.deleteBuffer = function(buf) {
* @inheritDoc
*/
ol.webgl.Context.prototype.disposeInternal = function() {
- goog.object.forEach(this.bufferCache_, function(bufferCacheEntry) {
- bufferCacheEntry.buf.removeDirtySet(bufferCacheEntry.dirtySet);
- });
var gl = this.getGL();
if (!gl.isContextLost()) {
goog.object.forEach(this.bufferCache_, function(bufferCacheEntry) {
@@ -171,6 +175,8 @@ ol.webgl.Context.prototype.getGL = function() {
/**
+ * Get shader from the cache if it's in the cache. Otherwise, create
+ * the WebGL shader, compile it, and add entry to cache.
* @param {ol.webgl.Shader} shaderObject Shader object.
* @return {WebGLShader} Shader.
*/
@@ -199,6 +205,9 @@ ol.webgl.Context.prototype.getShader = function(shaderObject) {
/**
+ * Get the program from the cache if it's in the cache. Otherwise create
+ * the WebGL program, attach the shaders to it, and add an entry to the
+ * cache.
* @param {ol.webgl.shader.Fragment} fragmentShaderObject Fragment shader.
* @param {ol.webgl.shader.Vertex} vertexShaderObject Vertex shader.
* @return {WebGLProgram} Program.
@@ -249,6 +258,9 @@ ol.webgl.Context.prototype.handleWebGLContextRestored = function() {
/**
+ * Just return false if that program is used already. Other use
+ * that program (call `gl.useProgram`) and make it the "current
+ * program".
* @param {WebGLProgram} program Program.
* @return {boolean} Changed.
* @api
diff --git a/test/spec/ol/render/webglreplay.test.js b/test/spec/ol/render/webglreplay.test.js
new file mode 100644
index 00000000000..8e600186e73
--- /dev/null
+++ b/test/spec/ol/render/webglreplay.test.js
@@ -0,0 +1,158 @@
+goog.provide('ol.test.render.webgl.Replay');
+
+describe('ol.render.webgl.ImageReplay', function() {
+ var replay;
+
+ var createImageStyle = function(image) {
+ var imageStyle = new ol.style.Image({
+ opacity: 0.1,
+ rotateWithView: true,
+ rotation: 1.5,
+ scale: 2.0
+ });
+ imageStyle.getAnchor = function() {
+ return [0.5, 1];
+ };
+ imageStyle.getImage = function() {
+ return image;
+ };
+ imageStyle.getImageSize = function() {
+ return [512, 512];
+ };
+ imageStyle.getOrigin = function() {
+ return [200, 200];
+ };
+ imageStyle.getSize = function() {
+ return [256, 256];
+ };
+ return imageStyle;
+ };
+
+ beforeEach(function() {
+ var tolerance = 0.1;
+ var maxExtent = [-10000, -20000, 10000, 20000];
+ replay = new ol.render.webgl.ImageReplay(tolerance, maxExtent);
+ });
+
+ describe('#setImageStyle', function() {
+
+ var imageStyle1, imageStyle2;
+
+ beforeEach(function() {
+ imageStyle1 = createImageStyle(new Image());
+ imageStyle2 = createImageStyle(new Image());
+ });
+
+ it('set expected states', function() {
+ replay.setImageStyle(imageStyle1);
+ expect(replay.anchorX_).to.be(0.5);
+ expect(replay.anchorY_).to.be(1);
+ expect(replay.height_).to.be(256);
+ expect(replay.imageHeight_).to.be(512);
+ expect(replay.imageWidth_).to.be(512);
+ expect(replay.opacity_).to.be(0.1);
+ expect(replay.originX_).to.be(200);
+ expect(replay.originY_).to.be(200);
+ expect(replay.rotation_).to.be(1.5);
+ expect(replay.rotateWithView_).to.be(true);
+ expect(replay.scale_).to.be(2.0);
+ expect(replay.width_).to.be(256);
+ expect(replay.images_).to.have.length(1);
+ expect(replay.groupIndices_).to.have.length(0);
+
+ replay.setImageStyle(imageStyle1);
+ expect(replay.images_).to.have.length(1);
+ expect(replay.groupIndices_).to.have.length(0);
+
+ replay.setImageStyle(imageStyle2);
+ expect(replay.images_).to.have.length(2);
+ expect(replay.groupIndices_).to.have.length(1);
+ });
+ });
+
+ describe('#drawPointGeometry', function() {
+ beforeEach(function() {
+ var imageStyle = createImageStyle(new Image());
+ replay.setImageStyle(imageStyle);
+ });
+
+ it('sets the buffer data', function() {
+ var point;
+
+ point = new ol.geom.Point([1000, 2000]);
+ replay.drawPointGeometry(point, null);
+ expect(replay.vertices_).to.have.length(32);
+ expect(replay.indices_).to.have.length(6);
+ expect(replay.indices_[0]).to.be(0);
+ expect(replay.indices_[1]).to.be(1);
+ expect(replay.indices_[2]).to.be(2);
+ expect(replay.indices_[3]).to.be(0);
+ expect(replay.indices_[4]).to.be(2);
+ expect(replay.indices_[5]).to.be(3);
+
+ point = new ol.geom.Point([2000, 3000]);
+ replay.drawPointGeometry(point, null);
+ expect(replay.vertices_).to.have.length(64);
+ expect(replay.indices_).to.have.length(12);
+ expect(replay.indices_[6]).to.be(4);
+ expect(replay.indices_[7]).to.be(5);
+ expect(replay.indices_[8]).to.be(6);
+ expect(replay.indices_[9]).to.be(4);
+ expect(replay.indices_[10]).to.be(6);
+ expect(replay.indices_[11]).to.be(7);
+ });
+ });
+
+ describe('#drawMultiPointGeometry', function() {
+ beforeEach(function() {
+ var imageStyle = createImageStyle(new Image());
+ replay.setImageStyle(imageStyle);
+ });
+
+ it('sets the buffer data', function() {
+ var multiPoint;
+
+ multiPoint = new ol.geom.MultiPoint(
+ [[1000, 2000], [2000, 3000]]);
+ replay.drawMultiPointGeometry(multiPoint, null);
+ expect(replay.vertices_).to.have.length(64);
+ expect(replay.indices_).to.have.length(12);
+ expect(replay.indices_[0]).to.be(0);
+ expect(replay.indices_[1]).to.be(1);
+ expect(replay.indices_[2]).to.be(2);
+ expect(replay.indices_[3]).to.be(0);
+ expect(replay.indices_[4]).to.be(2);
+ expect(replay.indices_[5]).to.be(3);
+ expect(replay.indices_[6]).to.be(4);
+ expect(replay.indices_[7]).to.be(5);
+ expect(replay.indices_[8]).to.be(6);
+ expect(replay.indices_[9]).to.be(4);
+ expect(replay.indices_[10]).to.be(6);
+ expect(replay.indices_[11]).to.be(7);
+
+ multiPoint = new ol.geom.MultiPoint(
+ [[3000, 4000], [4000, 5000]]);
+ replay.drawMultiPointGeometry(multiPoint, null);
+ expect(replay.vertices_).to.have.length(128);
+ expect(replay.indices_).to.have.length(24);
+ expect(replay.indices_[12]).to.be(8);
+ expect(replay.indices_[13]).to.be(9);
+ expect(replay.indices_[14]).to.be(10);
+ expect(replay.indices_[15]).to.be(8);
+ expect(replay.indices_[16]).to.be(10);
+ expect(replay.indices_[17]).to.be(11);
+ expect(replay.indices_[18]).to.be(12);
+ expect(replay.indices_[19]).to.be(13);
+ expect(replay.indices_[20]).to.be(14);
+ expect(replay.indices_[21]).to.be(12);
+ expect(replay.indices_[22]).to.be(14);
+ expect(replay.indices_[23]).to.be(15);
+ });
+ });
+});
+
+goog.require('ol.extent');
+goog.require('ol.geom.MultiPoint');
+goog.require('ol.geom.Point');
+goog.require('ol.render.webgl.ImageReplay');
+goog.require('ol.style.Image');
diff --git a/test/spec/ol/structs/buffer.test.js b/test/spec/ol/structs/buffer.test.js
deleted file mode 100644
index 94d24cec860..00000000000
--- a/test/spec/ol/structs/buffer.test.js
+++ /dev/null
@@ -1,297 +0,0 @@
-goog.provide('ol.test.structs.Buffer');
-
-
-describe('ol.structs.Buffer', function() {
-
- describe('constructor', function() {
-
- describe('without an argument', function() {
-
- var b;
- beforeEach(function() {
- b = new ol.structs.Buffer();
- });
-
- it('constructs an empty instance', function() {
- expect(b.getArray()).to.be.empty();
- expect(b.getCount()).to.be(0);
- });
-
- });
-
- describe('with a single array argument', function() {
-
- var b;
- beforeEach(function() {
- b = new ol.structs.Buffer([0, 1, 2, 3]);
- });
-
- it('constructs a populated instance', function() {
- expect(b.getArray()).to.eql([0, 1, 2, 3]);
- });
-
- });
-
- });
-
- describe('with an empty instance', function() {
-
- var b;
- beforeEach(function() {
- b = new ol.structs.Buffer();
- });
-
- describe('forEachRange', function() {
-
- it('does not call the callback', function() {
- var callback = sinon.spy();
- b.forEachRange(callback);
- expect(callback).not.to.be.called();
- });
-
- });
-
- describe('getArray', function() {
-
- it('returns an empty array', function() {
- expect(b.getArray()).to.be.empty();
- });
-
- });
-
- describe('getCount', function() {
-
- it('returns 0', function() {
- expect(b.getCount()).to.be(0);
- });
-
- });
-
- });
-
- describe('with an empty instance with spare capacity', function() {
-
- var b;
- beforeEach(function() {
- b = new ol.structs.Buffer(new Array(4), 0);
- });
-
- describe('add', function() {
-
- it('allows elements to be added', function() {
- expect(b.add([0, 1, 2, 3])).to.be(0);
- expect(b.getArray()).to.eql([0, 1, 2, 3]);
- });
-
- });
-
- describe('forEachRange', function() {
-
- it('does not call the callback', function() {
- var callback = sinon.spy();
- b.forEachRange(callback);
- expect(callback).not.to.be.called();
- });
-
- });
-
- describe('getCount', function() {
-
- it('returns 0', function() {
- expect(b.getCount()).to.be(0);
- });
-
- });
-
- });
-
- describe('with an instance with no spare capacity', function() {
-
- var b;
- beforeEach(function() {
- b = new ol.structs.Buffer([0, 1, 2, 3]);
- });
-
- describe('add', function() {
-
- it('throws an exception', function() {
- expect(function() {
- b.add([4, 5]);
- }).to.throwException();
- });
-
- });
-
- describe('forEachRange', function() {
-
- it('calls the callback', function() {
- var callback = sinon.spy();
- b.forEachRange(callback);
- expect(callback.calledOnce).to.be(true);
- expect(callback.args[0]).to.eql([0, 4]);
- });
-
- });
-
- describe('getCount', function() {
-
- it('returns the expected value', function() {
- expect(b.getCount()).to.be(4);
- });
-
- });
-
- describe('remove', function() {
-
- it('allows items to be removes', function() {
- expect(function() {
- b.remove(4, 2);
- }).to.not.throwException();
- });
-
- });
-
- describe('set', function() {
-
- it('updates the items', function() {
- b.set([5, 6], 2);
- expect(b.getArray()).to.eql([0, 1, 5, 6]);
- });
-
- it('marks the set items as dirty', function() {
- var dirtySet = new ol.structs.IntegerSet();
- b.addDirtySet(dirtySet);
- expect(dirtySet.isEmpty()).to.be(true);
- b.set([5, 6], 2);
- expect(dirtySet.isEmpty()).to.be(false);
- expect(dirtySet.getArray()).to.eql([2, 4]);
- });
-
- });
-
- });
-
- describe('with an instance with spare capacity', function() {
-
- var b;
- beforeEach(function() {
- var arr = [0, 1, 2, 3];
- arr.length = 8;
- b = new ol.structs.Buffer(arr, 4);
- });
-
- describe('add', function() {
-
- it('allows more items to be added', function() {
- expect(b.add([4, 5, 6, 7])).to.be(4);
- expect(b.getArray()).to.eql([0, 1, 2, 3, 4, 5, 6, 7]);
- });
-
- });
-
- describe('forEachRange', function() {
-
- it('calls the callback with the expected values', function() {
- var callback = sinon.spy();
- b.forEachRange(callback);
- expect(callback.calledOnce).to.be(true);
- expect(callback.args[0]).to.eql([0, 4]);
- });
-
- });
-
- describe('getCount', function() {
-
- it('returns the expected value', function() {
- expect(b.getCount()).to.be(4);
- });
-
- });
-
- describe('getFreeSet', function() {
-
- it('returns the expected set', function() {
- var freeSet = b.getFreeSet();
- expect(freeSet.isEmpty()).to.be(false);
- expect(freeSet.getArray()).to.eql([4, 8]);
- });
-
- });
-
- });
-
- describe('with a populated instance', function() {
-
- var b;
- beforeEach(function() {
- b = new ol.structs.Buffer([1234567.1234567, -7654321.7654321]);
- });
-
- describe('getSplit32', function() {
-
- it('returns the expected value', function() {
- var split32 = b.getSplit32();
- expect(split32).to.be.a(Float32Array);
- expect(split32).to.have.length(4);
- expect(split32[0]).to.roughlyEqual(1179648.0, 1e1);
- expect(split32[1]).to.roughlyEqual(54919.12345670001, 1e-2);
- expect(split32[2]).to.roughlyEqual(-7602176.0, 1e1);
- expect(split32[3]).to.roughlyEqual(-52145.76543209981, 1e-2);
- });
-
- it('tracks updates', function() {
- b.getSplit32();
- b.getArray()[0] = 0;
- b.markDirty(1, 0);
- var split32 = b.getSplit32();
- expect(split32).to.be.a(Float32Array);
- expect(split32).to.have.length(4);
- expect(split32[0]).to.be(0);
- expect(split32[1]).to.be(0);
- expect(split32[2]).to.roughlyEqual(-7602176.0, 1e1);
- expect(split32[3]).to.roughlyEqual(-52145.76543209981, 1e-2);
- });
-
- });
- });
-
- describe('usage tests', function() {
-
- it('allows multiple adds and removes', function() {
- var b = new ol.structs.Buffer(new Array(8), 0);
- expect(b.add([0, 1])).to.be(0);
- expect(b.getArray()).to.arreqlNaN([0, 1, NaN, NaN, NaN, NaN, NaN, NaN]);
- expect(b.getCount()).to.be(2);
- expect(b.add([2, 3, 4, 5])).to.be(2);
- expect(b.getArray()).to.arreqlNaN([0, 1, 2, 3, 4, 5, NaN, NaN]);
- expect(b.getCount()).to.be(6);
- expect(b.add([6, 7])).to.be(6);
- expect(b.getArray()).to.eql([0, 1, 2, 3, 4, 5, 6, 7]);
- expect(b.getCount()).to.be(8);
- b.remove(2, 2);
- expect(b.getArray()).to.arreqlNaN([0, 1, NaN, NaN, 4, 5, 6, 7]);
- expect(b.getCount()).to.be(6);
- expect(b.add([8, 9])).to.be(2);
- expect(b.getArray()).to.eql([0, 1, 8, 9, 4, 5, 6, 7]);
- expect(b.getCount()).to.be(8);
- b.remove(1, 1);
- expect(b.getArray()).to.arreqlNaN([0, NaN, 8, 9, 4, 5, 6, 7]);
- expect(b.getCount()).to.be(7);
- b.remove(4, 4);
- expect(b.getArray()).to.arreqlNaN([0, NaN, 8, 9, NaN, NaN, NaN, NaN]);
- expect(b.getCount()).to.be(3);
- expect(b.add([10, 11, 12])).to.be(4);
- expect(b.getArray()).to.arreqlNaN([0, NaN, 8, 9, 10, 11, 12, NaN]);
- expect(b.getCount()).to.be(6);
- expect(b.add([13])).to.be(1);
- expect(b.getArray()).to.arreqlNaN([0, 13, 8, 9, 10, 11, 12, NaN]);
- expect(b.getCount()).to.be(7);
- });
-
- });
-
-});
-
-
-goog.require('ol.structs.Buffer');
-goog.require('ol.structs.IntegerSet');
diff --git a/test/spec/ol/structs/integerset.test.js b/test/spec/ol/structs/integerset.test.js
deleted file mode 100644
index 2fb7af18fac..00000000000
--- a/test/spec/ol/structs/integerset.test.js
+++ /dev/null
@@ -1,622 +0,0 @@
-goog.provide('ol.test.structs.IntegerSet');
-
-
-describe('ol.structs.IntegerSet', function() {
-
- describe('constructor', function() {
-
- describe('without an argument', function() {
-
- it('constructs an empty instance', function() {
- var is = new ol.structs.IntegerSet();
- expect(is).to.be.an(ol.structs.IntegerSet);
- expect(is.getArray()).to.be.empty();
- });
-
- });
-
- describe('with an argument', function() {
-
- it('constructs with a valid array', function() {
- var is = new ol.structs.IntegerSet([0, 2, 4, 6]);
- expect(is).to.be.an(ol.structs.IntegerSet);
- expect(is.getArray()).to.eql([0, 2, 4, 6]);
- });
-
- it('throws an exception with an odd number of elements', function() {
- expect(function() {
- var is = new ol.structs.IntegerSet([0, 2, 4]);
- is = is; // suppress gjslint warning about unused variable
- }).to.throwException();
- });
-
- it('throws an exception with out-of-order elements', function() {
- expect(function() {
- var is = new ol.structs.IntegerSet([0, 2, 2, 4]);
- is = is; // suppress gjslint warning about unused variable
- }).to.throwException();
- });
-
- });
-
- });
-
- describe('with an empty instance', function() {
-
- var is;
- beforeEach(function() {
- is = new ol.structs.IntegerSet();
- });
-
- describe('addRange', function() {
-
- it('creates a new element', function() {
- is.addRange(0, 2);
- expect(is.getArray()).to.eql([0, 2]);
- });
-
- });
-
- describe('findRange', function() {
-
- it('returns -1', function() {
- expect(is.findRange(2)).to.be(-1);
- });
-
- });
-
- describe('forEachRange', function() {
-
- it('does not call the callback', function() {
- var callback = sinon.spy();
- is.forEachRange(callback);
- expect(callback).to.not.be.called();
- });
-
- });
-
- describe('forEachRangeInverted', function() {
-
- it('does call the callback', function() {
- var callback = sinon.spy();
- is.forEachRangeInverted(0, 8, callback);
- expect(callback.calledOnce).to.be(true);
- expect(callback.args[0]).to.eql([0, 8]);
- });
-
- });
-
- describe('getFirst', function() {
-
- it('returns -1', function() {
- expect(is.getFirst()).to.be(-1);
- });
-
- });
-
- describe('getLast', function() {
-
- it('returns -1', function() {
- expect(is.getLast()).to.be(-1);
- });
-
- });
-
- describe('getSize', function() {
-
- it('returns 0', function() {
- expect(is.getSize()).to.be(0);
- });
-
- });
-
- describe('intersectsRange', function() {
-
- it('returns false', function() {
- expect(is.intersectsRange(0, 0)).to.be(false);
- });
-
- });
-
- describe('isEmpty', function() {
-
- it('returns true', function() {
- expect(is.isEmpty()).to.be(true);
- });
-
- });
-
- describe('toString', function() {
-
- it('returns an empty string', function() {
- expect(is.toString()).to.be.empty();
- });
-
- });
-
- });
-
- describe('with a populated instance', function() {
-
- var is;
- beforeEach(function() {
- is = new ol.structs.IntegerSet([4, 6, 8, 10, 12, 14]);
- });
-
- describe('addRange', function() {
-
- it('inserts before the first element', function() {
- is.addRange(0, 2);
- expect(is.getArray()).to.eql([0, 2, 4, 6, 8, 10, 12, 14]);
- });
-
- it('extends the first element to the left', function() {
- is.addRange(0, 4);
- expect(is.getArray()).to.eql([0, 6, 8, 10, 12, 14]);
- });
-
- it('extends the first element to the right', function() {
- is.addRange(6, 7);
- expect(is.getArray()).to.eql([4, 7, 8, 10, 12, 14]);
- });
-
- it('merges the first two elements', function() {
- is.addRange(6, 8);
- expect(is.getArray()).to.eql([4, 10, 12, 14]);
- });
-
- it('extends middle elements to the left', function() {
- is.addRange(7, 8);
- expect(is.getArray()).to.eql([4, 6, 7, 10, 12, 14]);
- });
-
- it('extends middle elements to the right', function() {
- is.addRange(10, 11);
- expect(is.getArray()).to.eql([4, 6, 8, 11, 12, 14]);
- });
-
- it('merges the last two elements', function() {
- is.addRange(10, 12);
- expect(is.getArray()).to.eql([4, 6, 8, 14]);
- });
-
- it('extends the last element to the left', function() {
- is.addRange(11, 12);
- expect(is.getArray()).to.eql([4, 6, 8, 10, 11, 14]);
- });
-
- it('extends the last element to the right', function() {
- is.addRange(14, 15);
- expect(is.getArray()).to.eql([4, 6, 8, 10, 12, 15]);
- });
-
- it('inserts after the last element', function() {
- is.addRange(16, 18);
- expect(is.getArray()).to.eql([4, 6, 8, 10, 12, 14, 16, 18]);
- });
-
- });
-
- describe('clear', function() {
-
- it('clears the instance', function() {
- is.clear();
- expect(is.getArray()).to.be.empty();
- });
-
- });
-
- describe('findRange', function() {
-
- it('throws an exception when passed a negative size', function() {
- expect(function() {
- is.findRange(-1);
- }).to.throwException();
- });
-
- it('throws an exception when passed a zero size', function() {
- expect(function() {
- is.findRange(0);
- }).to.throwException();
- });
-
- it('finds the first range of size 1', function() {
- expect(is.findRange(1)).to.be(4);
- });
-
- it('finds the first range of size 2', function() {
- expect(is.findRange(2)).to.be(4);
- });
-
- it('returns -1 when no range can be found', function() {
- expect(is.findRange(3)).to.be(-1);
- });
-
- });
-
- describe('forEachRange', function() {
-
- it('calls the callback', function() {
- var callback = sinon.spy();
- is.forEachRange(callback);
- expect(callback).to.be.called();
- expect(callback.calledThrice).to.be(true);
- expect(callback.args[0]).to.eql([4, 6]);
- expect(callback.args[1]).to.eql([8, 10]);
- expect(callback.args[2]).to.eql([12, 14]);
- });
-
- });
-
- describe('forEachRangeInverted', function() {
-
- it('does call the callback', function() {
- var callback = sinon.spy();
- is.forEachRangeInverted(0, 16, callback);
- expect(callback.callCount).to.be(4);
- expect(callback.args[0]).to.eql([0, 4]);
- expect(callback.args[1]).to.eql([6, 8]);
- expect(callback.args[2]).to.eql([10, 12]);
- expect(callback.args[3]).to.eql([14, 16]);
- });
-
- });
-
-
- describe('getFirst', function() {
-
- it('returns the expected value', function() {
- expect(is.getFirst()).to.be(4);
- });
-
- });
-
- describe('getLast', function() {
-
- it('returns the expected value', function() {
- expect(is.getLast()).to.be(14);
- });
-
- });
-
- describe('getSize', function() {
-
- it('returns the expected value', function() {
- expect(is.getSize()).to.be(6);
- });
-
- });
-
- describe('intersectsRange', function() {
-
- it('returns the expected value for small ranges', function() {
- expect(is.intersectsRange(1, 3)).to.be(false);
- expect(is.intersectsRange(2, 4)).to.be(false);
- expect(is.intersectsRange(3, 5)).to.be(true);
- expect(is.intersectsRange(4, 6)).to.be(true);
- expect(is.intersectsRange(5, 7)).to.be(true);
- expect(is.intersectsRange(6, 8)).to.be(false);
- expect(is.intersectsRange(7, 9)).to.be(true);
- expect(is.intersectsRange(8, 10)).to.be(true);
- expect(is.intersectsRange(9, 11)).to.be(true);
- expect(is.intersectsRange(10, 12)).to.be(false);
- expect(is.intersectsRange(11, 13)).to.be(true);
- expect(is.intersectsRange(12, 14)).to.be(true);
- expect(is.intersectsRange(13, 15)).to.be(true);
- expect(is.intersectsRange(14, 16)).to.be(false);
- expect(is.intersectsRange(15, 17)).to.be(false);
- });
-
- it('returns the expected value for large ranges', function() {
- expect(is.intersectsRange(-3, 1)).to.be(false);
- expect(is.intersectsRange(1, 5)).to.be(true);
- expect(is.intersectsRange(1, 9)).to.be(true);
- expect(is.intersectsRange(1, 13)).to.be(true);
- expect(is.intersectsRange(1, 17)).to.be(true);
- expect(is.intersectsRange(5, 9)).to.be(true);
- expect(is.intersectsRange(5, 13)).to.be(true);
- expect(is.intersectsRange(5, 17)).to.be(true);
- expect(is.intersectsRange(9, 13)).to.be(true);
- expect(is.intersectsRange(9, 17)).to.be(true);
- expect(is.intersectsRange(13, 17)).to.be(true);
- expect(is.intersectsRange(17, 21)).to.be(false);
- });
-
- });
-
- describe('isEmpty', function() {
-
- it('returns false', function() {
- expect(is.isEmpty()).to.be(false);
- });
-
- });
-
- describe('removeRange', function() {
-
- it('removes the first part of the first element', function() {
- is.removeRange(4, 5);
- expect(is.getArray()).to.eql([5, 6, 8, 10, 12, 14]);
- });
-
- it('removes the last part of the first element', function() {
- is.removeRange(5, 6);
- expect(is.getArray()).to.eql([4, 5, 8, 10, 12, 14]);
- });
-
- it('removes the first element', function() {
- is.removeRange(4, 6);
- expect(is.getArray()).to.eql([8, 10, 12, 14]);
- });
-
- it('removes the first part of a middle element', function() {
- is.removeRange(8, 9);
- expect(is.getArray()).to.eql([4, 6, 9, 10, 12, 14]);
- });
-
- it('removes the last part of a middle element', function() {
- is.removeRange(9, 10);
- expect(is.getArray()).to.eql([4, 6, 8, 9, 12, 14]);
- });
-
- it('removes a middle element', function() {
- is.removeRange(8, 10);
- expect(is.getArray()).to.eql([4, 6, 12, 14]);
- });
-
- it('removes the first part of the last element', function() {
- is.removeRange(12, 13);
- expect(is.getArray()).to.eql([4, 6, 8, 10, 13, 14]);
- });
-
- it('removes the last part of the last element', function() {
- is.removeRange(13, 14);
- expect(is.getArray()).to.eql([4, 6, 8, 10, 12, 13]);
- });
-
- it('removes the last element', function() {
- is.removeRange(12, 14);
- expect(is.getArray()).to.eql([4, 6, 8, 10]);
- });
-
- it('can remove multiple ranges near the start', function() {
- is.removeRange(3, 11);
- expect(is.getArray()).to.eql([12, 14]);
- });
-
- it('can remove multiple ranges near the start', function() {
- is.removeRange(7, 15);
- expect(is.getArray()).to.eql([4, 6]);
- });
-
- it('throws an exception when passed an invalid range', function() {
- expect(function() {
- is.removeRange(2, 0);
- }).to.throwException();
- });
-
- });
-
- describe('toString', function() {
-
- it('returns the expected value', function() {
- expect(is.toString()).to.be('4-6, 8-10, 12-14');
- });
- });
-
- });
-
- describe('with fragmentation', function() {
-
- var is;
- beforeEach(function() {
- is = new ol.structs.IntegerSet([0, 1, 2, 4, 5, 8, 9, 12, 13, 15, 16, 17]);
- });
-
- describe('findRange', function() {
-
- it('finds the first range of size 1', function() {
- expect(is.findRange(1)).to.be(0);
- });
-
- it('finds the first range of size 2', function() {
- expect(is.findRange(2)).to.be(2);
- });
-
- it('finds the first range of size 3', function() {
- expect(is.findRange(3)).to.be(5);
- });
-
- it('returns -1 when no range can be found', function() {
- expect(is.findRange(4)).to.be(-1);
- });
-
- });
-
- describe('getFirst', function() {
-
- it('returns the expected value', function() {
- expect(is.getFirst()).to.be(0);
- });
-
- });
-
- describe('getLast', function() {
-
- it('returns the expected value', function() {
- expect(is.getLast()).to.be(17);
- });
-
- });
-
- describe('getSize', function() {
-
- it('returns the expected value', function() {
- expect(is.getSize()).to.be(12);
- });
-
- });
-
- describe('removeRange', function() {
-
- it('removing an empty range has no effect', function() {
- is.removeRange(0, 0);
- expect(is.getArray()).to.eql(
- [0, 1, 2, 4, 5, 8, 9, 12, 13, 15, 16, 17]);
- });
-
- it('can remove elements from the middle of range', function() {
- is.removeRange(6, 7);
- expect(is.getArray()).to.eql(
- [0, 1, 2, 4, 5, 6, 7, 8, 9, 12, 13, 15, 16, 17]);
- });
-
- it('can remove multiple ranges', function() {
- is.removeRange(2, 12);
- expect(is.getArray()).to.eql([0, 1, 13, 15, 16, 17]);
- });
-
- it('can remove multiple ranges and reduce others', function() {
- is.removeRange(0, 10);
- expect(is.getArray()).to.eql([10, 12, 13, 15, 16, 17]);
- });
-
- it('can remove all ranges', function() {
- is.removeRange(0, 18);
- expect(is.getArray()).to.eql([]);
- });
-
- });
-
- describe('toString', function() {
-
- it('returns the expected value', function() {
- expect(is.toString()).to.be('0-1, 2-4, 5-8, 9-12, 13-15, 16-17');
- });
-
- });
-
- });
-
- describe('compared to a slow reference implementation', function() {
-
- var SimpleIntegerSet = function() {
- this.integers_ = {};
- };
-
- SimpleIntegerSet.prototype.addRange = function(addStart, addStop) {
- var i;
- for (i = addStart; i < addStop; ++i) {
- this.integers_[i.toString()] = true;
- }
- };
-
- SimpleIntegerSet.prototype.clear = function() {
- this.integers_ = {};
- };
-
- SimpleIntegerSet.prototype.getArray = function() {
- var integers = goog.array.map(
- goog.object.getKeys(this.integers_), Number);
- goog.array.sort(integers);
- var arr = [];
- var start = -1, stop;
- var i;
- for (i = 0; i < integers.length; ++i) {
- if (start == -1) {
- start = stop = integers[i];
- } else if (integers[i] == stop + 1) {
- ++stop;
- } else {
- arr.push(start, stop + 1);
- start = stop = integers[i];
- }
- }
- if (start != -1) {
- arr.push(start, stop + 1);
- }
- return arr;
- };
-
- SimpleIntegerSet.prototype.removeRange = function(removeStart, removeStop) {
- var i;
- for (i = removeStart; i < removeStop; ++i) {
- delete this.integers_[i.toString()];
- }
- };
-
- var is, sis;
- beforeEach(function() {
- is = new ol.structs.IntegerSet();
- sis = new SimpleIntegerSet();
- });
-
- it('behaves identically with random adds', function() {
- var addStart, addStop, i;
- for (i = 0; i < 64; ++i) {
- addStart = goog.math.randomInt(128);
- addStop = addStart + goog.math.randomInt(16);
- is.addRange(addStart, addStop);
- sis.addRange(addStart, addStop);
- expect(is.getArray()).to.eql(sis.getArray());
- }
- });
-
- it('behaves identically with random removes', function() {
- is.addRange(0, 128);
- sis.addRange(0, 128);
- var i, removeStart, removeStop;
- for (i = 0; i < 64; ++i) {
- removeStart = goog.math.randomInt(128);
- removeStop = removeStart + goog.math.randomInt(16);
- is.removeRange(removeStart, removeStop);
- sis.removeRange(removeStart, removeStop);
- expect(is.getArray()).to.eql(sis.getArray());
- }
- });
-
- it('behaves identically with random adds and removes', function() {
- var i, start, stop;
- for (i = 0; i < 64; ++i) {
- start = goog.math.randomInt(128);
- stop = start + goog.math.randomInt(16);
- if (Math.random() < 0.5) {
- is.addRange(start, stop);
- sis.addRange(start, stop);
- } else {
- is.removeRange(start, stop);
- sis.removeRange(start, stop);
- }
- expect(is.getArray()).to.eql(sis.getArray());
- }
- });
-
- it('behaves identically with random adds, removes, and clears', function() {
- var i, p, start, stop;
- for (i = 0; i < 64; ++i) {
- start = goog.math.randomInt(128);
- stop = start + goog.math.randomInt(16);
- p = Math.random();
- if (p < 0.45) {
- is.addRange(start, stop);
- sis.addRange(start, stop);
- } else if (p < 0.9) {
- is.removeRange(start, stop);
- sis.removeRange(start, stop);
- } else {
- is.clear();
- sis.clear();
- }
- expect(is.getArray()).to.eql(sis.getArray());
- }
- });
-
- });
-
-});
-
-
-goog.require('goog.array');
-goog.require('goog.math');
-goog.require('goog.object');
-goog.require('ol.structs.IntegerSet');
diff --git a/test/spec/ol/style/atlasmanager.test.js b/test/spec/ol/style/atlasmanager.test.js
new file mode 100644
index 00000000000..66f5707fd13
--- /dev/null
+++ b/test/spec/ol/style/atlasmanager.test.js
@@ -0,0 +1,273 @@
+goog.provide('ol.test.style.AtlasManager');
+
+
+describe('ol.style.Atlas', function() {
+
+ var defaultRender = function(context, x, y) {
+ };
+
+ describe('#constructor', function() {
+
+ it('inits the atlas', function() {
+ var atlas = new ol.style.Atlas(256, 1);
+ expect(atlas.emptyBlocks_).to.eql(
+ [{x: 0, y: 0, width: 256, height: 256}]);
+ });
+ });
+
+ describe('#add (squares with same size)', function() {
+
+ it('adds one entry', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+ var info = atlas.add('1', 32, 32, defaultRender);
+
+ expect(info).to.eql(
+ {offsetX: 1, offsetY: 1, image: atlas.canvas_});
+
+ expect(atlas.get('1')).to.eql(info);
+ });
+
+ it('adds two entries', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ atlas.add('1', 32, 32, defaultRender);
+ var info = atlas.add('2', 32, 32, defaultRender);
+
+ expect(info).to.eql(
+ {offsetX: 34, offsetY: 1, image: atlas.canvas_});
+
+ expect(atlas.get('2')).to.eql(info);
+ });
+
+ it('adds three entries', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ atlas.add('1', 32, 32, defaultRender);
+ atlas.add('2', 32, 32, defaultRender);
+ var info = atlas.add('3', 32, 32, defaultRender);
+
+ expect(info).to.eql(
+ {offsetX: 67, offsetY: 1, image: atlas.canvas_});
+
+ expect(atlas.get('3')).to.eql(info);
+ });
+
+ it('adds four entries (new row)', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ atlas.add('1', 32, 32, defaultRender);
+ atlas.add('2', 32, 32, defaultRender);
+ atlas.add('3', 32, 32, defaultRender);
+ var info = atlas.add('4', 32, 32, defaultRender);
+
+ expect(info).to.eql(
+ {offsetX: 1, offsetY: 34, image: atlas.canvas_});
+
+ expect(atlas.get('4')).to.eql(info);
+ });
+
+ it('returns null when an entry is too big', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ atlas.add('1', 32, 32, defaultRender);
+ atlas.add('2', 32, 32, defaultRender);
+ atlas.add('3', 32, 32, defaultRender);
+ var info = atlas.add(4, 100, 100, defaultRender);
+
+ expect(info).to.eql(null);
+ });
+
+ it('fills up the whole atlas', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ for (var i = 1; i <= 16; i++) {
+ expect(atlas.add(i.toString(), 28, 28, defaultRender)).to.be.ok();
+ }
+
+ // there is no more space for items of this size, the next one will fail
+ expect(atlas.add('17', 28, 28, defaultRender)).to.eql(null);
+ });
+ });
+
+ describe('#add (rectangles with different sizes)', function() {
+
+ it('adds a bunch of rectangles', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ expect(atlas.add('1', 64, 32, defaultRender)).to.eql(
+ {offsetX: 1, offsetY: 1, image: atlas.canvas_});
+
+ expect(atlas.add('2', 64, 32, defaultRender)).to.eql(
+ {offsetX: 1, offsetY: 34, image: atlas.canvas_});
+
+ expect(atlas.add('3', 64, 32, defaultRender)).to.eql(
+ {offsetX: 1, offsetY: 67, image: atlas.canvas_});
+
+ // this one can not be added anymore
+ expect(atlas.add('4', 64, 32, defaultRender)).to.eql(null);
+
+ // but there is still room for smaller ones
+ expect(atlas.add('5', 40, 32, defaultRender)).to.eql(
+ {offsetX: 66, offsetY: 1, image: atlas.canvas_});
+
+ expect(atlas.add('6', 40, 32, defaultRender)).to.eql(
+ {offsetX: 66, offsetY: 34, image: atlas.canvas_});
+ });
+
+ it('fills up the whole atlas (rectangles in portrait format)', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ for (var i = 1; i <= 32; i++) {
+ expect(atlas.add(i.toString(), 28, 14, defaultRender)).to.be.ok();
+ }
+
+ // there is no more space for items of this size, the next one will fail
+ expect(atlas.add('33', 28, 14, defaultRender)).to.eql(null);
+ });
+
+ it('fills up the whole atlas (rectangles in landscape format)', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ for (var i = 1; i <= 32; i++) {
+ expect(atlas.add(i.toString(), 14, 28, defaultRender)).to.be.ok();
+ }
+
+ // there is no more space for items of this size, the next one will fail
+ expect(atlas.add('33', 14, 28, defaultRender)).to.eql(null);
+ });
+ });
+
+ describe('#add (rendering)', function() {
+
+ it('calls the render callback with the right values', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+ var rendererCallback = sinon.spy();
+ atlas.add('1', 32, 32, rendererCallback);
+
+ expect(rendererCallback.calledOnce).to.be.ok();
+ expect(rendererCallback.calledWith(atlas.context_, 1, 1)).to.be.ok();
+
+ rendererCallback = sinon.spy();
+ atlas.add('2', 32, 32, rendererCallback);
+
+ expect(rendererCallback.calledOnce).to.be.ok();
+ expect(rendererCallback.calledWith(atlas.context_, 34, 1)).to.be.ok();
+ });
+
+ it('is possible to actually draw on the canvas', function() {
+ var atlas = new ol.style.Atlas(128, 1);
+
+ var rendererCallback = function(context, x, y) {
+ context.fillStyle = '#FFA500';
+ context.fillRect(x, y, 32, 32);
+ };
+
+ expect(atlas.add('1', 32, 32, rendererCallback)).to.be.ok();
+ expect(atlas.add('2', 32, 32, rendererCallback)).to.be.ok();
+ // no error, ok
+ });
+ });
+});
+
+
+describe('ol.style.AtlasManager', function() {
+
+ var defaultRender = function(context, x, y) {
+ };
+
+ describe('#constructor', function() {
+
+ it('inits the atlas manager', function() {
+ var manager = new ol.style.AtlasManager();
+ expect(manager.atlases_).to.not.be.empty();
+ });
+ });
+
+ describe('#add', function() {
+
+ it('adds one entry', function() {
+ var manager = new ol.style.AtlasManager({initialSize: 128});
+ var info = manager.add('1', 32, 32, defaultRender);
+
+ expect(info).to.eql({
+ offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_,
+ hitOffsetX: undefined, hitOffsetY: undefined, hitImage: undefined});
+
+ expect(manager.getInfo('1')).to.eql(info);
+ });
+
+ it('adds one entry (also to the hit detection atlas)', function() {
+ var manager = new ol.style.AtlasManager({initialSize: 128});
+ var info = manager.add('1', 32, 32, defaultRender, defaultRender);
+
+ expect(info).to.eql({
+ offsetX: 1, offsetY: 1, image: manager.atlases_[0].canvas_,
+ hitOffsetX: 1, hitOffsetY: 1,
+ hitImage: manager.hitAtlases_[0].canvas_});
+
+ expect(manager.getInfo('1')).to.eql(info);
+ });
+
+ it('creates a new atlas if needed', function() {
+ var manager = new ol.style.AtlasManager({initialSize: 128});
+ expect(manager.add('1', 100, 100, defaultRender, defaultRender))
+ .to.be.ok();
+ var info = manager.add('2', 100, 100, defaultRender, defaultRender);
+ expect(info).to.be.ok();
+ expect(info.image.width).to.eql(256);
+ expect(manager.atlases_).to.have.length(2);
+ expect(info.hitImage.width).to.eql(256);
+ expect(manager.hitAtlases_).to.have.length(2);
+ });
+
+ it('creates new atlases until one is large enough', function() {
+ var manager = new ol.style.AtlasManager({initialSize: 128});
+ expect(manager.add('1', 100, 100, defaultRender, defaultRender))
+ .to.be.ok();
+ expect(manager.atlases_).to.have.length(1);
+ expect(manager.hitAtlases_).to.have.length(1);
+ var info = manager.add('2', 500, 500, defaultRender, defaultRender);
+ expect(info).to.be.ok();
+ expect(info.image.width).to.eql(512);
+ expect(manager.atlases_).to.have.length(3);
+ expect(info.hitImage.width).to.eql(512);
+ expect(manager.hitAtlases_).to.have.length(3);
+ });
+
+ it('checks all existing atlases and create a new if needed', function() {
+ var manager = new ol.style.AtlasManager({initialSize: 128});
+ expect(manager.add('1', 100, 100, defaultRender, defaultRender))
+ .to.be.ok();
+ expect(manager.add('2', 100, 100, defaultRender, defaultRender))
+ .to.be.ok();
+ expect(manager.atlases_).to.have.length(2);
+ expect(manager.hitAtlases_).to.have.length(2);
+ var info = manager.add(3, 500, 500, defaultRender, defaultRender);
+ expect(info).to.be.ok();
+ expect(info.image.width).to.eql(512);
+ expect(manager.atlases_).to.have.length(3);
+ expect(info.hitImage.width).to.eql(512);
+ expect(manager.hitAtlases_).to.have.length(3);
+ });
+
+ it('returns null if the size exceeds the maximum size', function() {
+ var manager = new ol.style.AtlasManager(
+ {initialSize: 128, maxSize: 2048});
+ expect(manager.add('1', 100, 100, defaultRender, defaultRender))
+ .to.be.ok();
+ expect(manager.add('2', 2048, 2048, defaultRender, defaultRender))
+ .to.eql(null);
+ });
+ });
+
+ describe('#getInfo', function() {
+
+ it('returns null if no entry for the given id', function() {
+ var manager = new ol.style.AtlasManager({initialSize: 128});
+ expect(manager.getInfo('123456')).to.eql(null);
+ });
+ });
+});
+
+goog.require('ol.style.Atlas');
+goog.require('ol.style.AtlasManager');
diff --git a/test/spec/ol/style/circlestyle.test.js b/test/spec/ol/style/circlestyle.test.js
new file mode 100644
index 00000000000..4e5d0b985c7
--- /dev/null
+++ b/test/spec/ol/style/circlestyle.test.js
@@ -0,0 +1,240 @@
+goog.provide('ol.test.style.Circle');
+
+
+describe('ol.style.Circle', function() {
+
+ describe('#constructor', function() {
+
+ it('creates a canvas if no atlas is used (no fill-style)', function() {
+ var style = new ol.style.Circle({radius: 10});
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([21, 21]);
+ expect(style.getOrigin()).to.eql([0, 0]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // hit-detection image is created, because no fill style is set
+ expect(style.getImage()).to.not.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([21, 21]);
+ expect(style.getHitDetectionOrigin()).to.eql([0, 0]);
+ });
+
+ it('creates a canvas if no atlas is used (fill-style)', function() {
+ var style = new ol.style.Circle({
+ radius: 10,
+ fill: new ol.style.Fill({
+ color: '#FFFF00'
+ })
+ });
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([21, 21]);
+ expect(style.getOrigin()).to.eql([0, 0]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // no hit-detection image is created, because fill style is set
+ expect(style.getImage()).to.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([21, 21]);
+ expect(style.getHitDetectionOrigin()).to.eql([0, 0]);
+ });
+
+ it('adds itself to an atlas manager (no fill-style)', function() {
+ var atlasManager = new ol.style.AtlasManager({initialSize: 512});
+ var style = new ol.style.Circle({radius: 10, atlasManager: atlasManager});
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([512, 512]);
+ expect(style.getOrigin()).to.eql([1, 1]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // hit-detection image is created, because no fill style is set
+ expect(style.getImage()).to.not.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([512, 512]);
+ expect(style.getHitDetectionOrigin()).to.eql([1, 1]);
+ });
+
+ it('adds itself to an atlas manager (fill-style)', function() {
+ var atlasManager = new ol.style.AtlasManager({initialSize: 512});
+ var style = new ol.style.Circle({
+ radius: 10,
+ atlasManager: atlasManager,
+ fill: new ol.style.Fill({
+ color: '#FFFF00'
+ })
+ });
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([512, 512]);
+ expect(style.getOrigin()).to.eql([1, 1]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // no hit-detection image is created, because fill style is set
+ expect(style.getImage()).to.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([512, 512]);
+ expect(style.getHitDetectionOrigin()).to.eql([1, 1]);
+ });
+ });
+
+ describe('#getChecksum', function() {
+
+ it('calculates the same hash code for default options', function() {
+ var style1 = new ol.style.Circle();
+ var style2 = new ol.style.Circle();
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+ });
+
+ it('calculates not the same hash code (radius)', function() {
+ var style1 = new ol.style.Circle();
+ var style2 = new ol.style.Circle({
+ radius: 5
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('calculates the same hash code (radius)', function() {
+ var style1 = new ol.style.Circle({
+ radius: 5
+ });
+ var style2 = new ol.style.Circle({
+ radius: 5
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+ });
+
+ it('calculates not the same hash code (color)', function() {
+ var style1 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ })
+ });
+ var style2 = new ol.style.Circle({
+ radius: 5,
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('calculates the same hash code (everything set)', function() {
+ var style1 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 2
+ })
+ });
+ var style2 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 2
+ })
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+ });
+
+ it('calculates not the same hash code (stroke width differs)', function() {
+ var style1 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 3
+ })
+ });
+ var style2 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 2
+ })
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('invalidates a cached checksum if values change (fill)', function() {
+ var style1 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ var style2 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+
+ style1.getFill().setColor('red');
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('invalidates a cached checksum if values change (stroke)', function() {
+ var style1 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ var style2 = new ol.style.Circle({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+
+ style1.getStroke().setWidth(4);
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ });
+});
+
+goog.require('ol.style.AtlasManager');
+goog.require('ol.style.Circle');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Stroke');
diff --git a/test/spec/ol/style/regularshapestyle.test.js b/test/spec/ol/style/regularshapestyle.test.js
index c5156700567..f972ae7fc6a 100644
--- a/test/spec/ol/style/regularshapestyle.test.js
+++ b/test/spec/ol/style/regularshapestyle.test.js
@@ -1,36 +1,297 @@
goog.provide('ol.test.style.RegularShape');
describe('ol.style.RegularShape', function() {
- it('can use radius', function() {
- var style = new ol.style.RegularShape({
- radius: 5,
- radius2: 10
+
+ describe('#constructor', function() {
+
+ it('can use radius', function() {
+ var style = new ol.style.RegularShape({
+ radius: 5,
+ radius2: 10
+ });
+ expect(style.getRadius()).to.eql(5);
+ expect(style.getRadius2()).to.eql(10);
});
- expect(style.getRadius()).to.eql(5);
- expect(style.getRadius2()).to.eql(10);
- });
- it('can use radius1 as an alias for radius', function() {
- var style = new ol.style.RegularShape({
- radius1: 5,
- radius2: 10
+
+ it('can use radius1 as an alias for radius', function() {
+ var style = new ol.style.RegularShape({
+ radius1: 5,
+ radius2: 10
+ });
+ expect(style.getRadius()).to.eql(5);
+ expect(style.getRadius2()).to.eql(10);
});
- expect(style.getRadius()).to.eql(5);
- expect(style.getRadius2()).to.eql(10);
- });
- it('will use radius for radius2 if radius2 not defined', function() {
- var style = new ol.style.RegularShape({
- radius: 5
+
+ it('will use radius for radius2 if radius2 not defined', function() {
+ var style = new ol.style.RegularShape({
+ radius: 5
+ });
+ expect(style.getRadius()).to.eql(5);
+ expect(style.getRadius2()).to.eql(5);
+ });
+
+ it('will use radius1 for radius2 if radius2 not defined', function() {
+ var style = new ol.style.RegularShape({
+ radius1: 5
+ });
+ expect(style.getRadius()).to.eql(5);
+ expect(style.getRadius2()).to.eql(5);
+ });
+
+ it('creates a canvas if no atlas is used (no fill-style)', function() {
+ var style = new ol.style.RegularShape({radius: 10});
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([21, 21]);
+ expect(style.getOrigin()).to.eql([0, 0]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // hit-detection image is created, because no fill style is set
+ expect(style.getImage()).to.not.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([21, 21]);
+ expect(style.getHitDetectionOrigin()).to.eql([0, 0]);
+ });
+
+ it('creates a canvas if no atlas is used (fill-style)', function() {
+ var style = new ol.style.RegularShape({
+ radius: 10,
+ fill: new ol.style.Fill({
+ color: '#FFFF00'
+ })
+ });
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([21, 21]);
+ expect(style.getOrigin()).to.eql([0, 0]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // no hit-detection image is created, because fill style is set
+ expect(style.getImage()).to.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([21, 21]);
+ expect(style.getHitDetectionOrigin()).to.eql([0, 0]);
+ });
+
+ it('adds itself to an atlas manager (no fill-style)', function() {
+ var atlasManager = new ol.style.AtlasManager({initialSize: 512});
+ var style = new ol.style.RegularShape(
+ {radius: 10, atlasManager: atlasManager});
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([512, 512]);
+ expect(style.getOrigin()).to.eql([1, 1]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // hit-detection image is created, because no fill style is set
+ expect(style.getImage()).to.not.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([512, 512]);
+ expect(style.getHitDetectionOrigin()).to.eql([1, 1]);
+ });
+
+ it('adds itself to an atlas manager (fill-style)', function() {
+ var atlasManager = new ol.style.AtlasManager({initialSize: 512});
+ var style = new ol.style.RegularShape({
+ radius: 10,
+ atlasManager: atlasManager,
+ fill: new ol.style.Fill({
+ color: '#FFFF00'
+ })
+ });
+ expect(style.getImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getSize()).to.eql([21, 21]);
+ expect(style.getImageSize()).to.eql([512, 512]);
+ expect(style.getOrigin()).to.eql([1, 1]);
+ expect(style.getAnchor()).to.eql([10.5, 10.5]);
+ // no hit-detection image is created, because fill style is set
+ expect(style.getImage()).to.be(style.getHitDetectionImage());
+ expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement);
+ expect(style.getHitDetectionImageSize()).to.eql([512, 512]);
+ expect(style.getHitDetectionOrigin()).to.eql([1, 1]);
});
- expect(style.getRadius()).to.eql(5);
- expect(style.getRadius2()).to.eql(5);
});
- it('will use radius1 for radius2 if radius2 not defined', function() {
- var style = new ol.style.RegularShape({
- radius1: 5
+
+
+ describe('#getChecksum', function() {
+
+ it('calculates not the same hash code (radius)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 4,
+ radius2: 5
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 3,
+ radius2: 5
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('calculates not the same hash code (radius2)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 4,
+ radius2: 5
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 4,
+ radius2: 6
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('calculates the same hash code (radius)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 5
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 5
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+ });
+
+ it('calculates not the same hash code (color)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ })
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 5,
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('calculates the same hash code (everything set)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 5,
+ radius2: 3,
+ angle: 1.41,
+ points: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 2
+ })
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 5,
+ radius2: 3,
+ angle: 1.41,
+ points: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 2
+ })
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
});
- expect(style.getRadius()).to.eql(5);
- expect(style.getRadius2()).to.eql(5);
+
+ it('calculates not the same hash code (stroke width differs)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 5,
+ radius2: 3,
+ angle: 1.41,
+ points: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 3
+ })
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 5,
+ radius2: 3,
+ angle: 1.41,
+ points: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3',
+ lineCap: 'round',
+ lineDash: [5, 15, 25],
+ lineJoin: 'miter',
+ miterLimit: 4,
+ width: 2
+ })
+ });
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('invalidates a cached checksum if values change (fill)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+
+ style1.getFill().setColor('red');
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
+ it('invalidates a cached checksum if values change (stroke)', function() {
+ var style1 = new ol.style.RegularShape({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ var style2 = new ol.style.RegularShape({
+ radius: 5,
+ fill: new ol.style.Fill({
+ color: '#319FD3'
+ }),
+ stroke: new ol.style.Stroke({
+ color: '#319FD3'
+ })
+ });
+ expect(style1.getChecksum()).to.eql(style2.getChecksum());
+
+ style1.getStroke().setWidth(4);
+ expect(style1.getChecksum()).to.not.eql(style2.getChecksum());
+ });
+
});
});
+goog.require('ol.style.AtlasManager');
goog.require('ol.style.RegularShape');
+goog.require('ol.style.Fill');
+goog.require('ol.style.Stroke');
diff --git a/test/spec/ol/webgl/buffer.test.js b/test/spec/ol/webgl/buffer.test.js
new file mode 100644
index 00000000000..7d7153e32eb
--- /dev/null
+++ b/test/spec/ol/webgl/buffer.test.js
@@ -0,0 +1,55 @@
+goog.provide('ol.test.webgl.Buffer');
+
+
+describe('ol.webgl.Buffer', function() {
+
+ describe('constructor', function() {
+
+ describe('without an argument', function() {
+
+ var b;
+ beforeEach(function() {
+ b = new ol.webgl.Buffer();
+ });
+
+ it('constructs an empty instance', function() {
+ expect(b.getArray()).to.be.empty();
+ });
+
+ });
+
+ describe('with a single array argument', function() {
+
+ var b;
+ beforeEach(function() {
+ b = new ol.webgl.Buffer([0, 1, 2, 3]);
+ });
+
+ it('constructs a populated instance', function() {
+ expect(b.getArray()).to.eql([0, 1, 2, 3]);
+ });
+
+ });
+
+ });
+
+ describe('with an empty instance', function() {
+
+ var b;
+ beforeEach(function() {
+ b = new ol.webgl.Buffer();
+ });
+
+ describe('getArray', function() {
+
+ it('returns an empty array', function() {
+ expect(b.getArray()).to.be.empty();
+ });
+
+ });
+
+ });
+
+});
+
+goog.require('ol.webgl.Buffer');