/**
 * Cesium - https://github.com/CesiumGS/cesium
 *
 * Copyright 2011-2020 Cesium Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Columbus View (Pat. Pend.)
 *
 * Portions licensed separately.
 * See https://github.com/CesiumGS/cesium/blob/master/LICENSE.md for full licensing details.
 */
define(['./when-8d13db60', './Check-70bec281', './Math-61ede240', './FeatureDetection-7bd32c34', './createTaskProcessorWorker', './Color-69f1845f', './pbf-9fe59c76'], function (when, Check, _Math, FeatureDetection, createTaskProcessorWorker, Color, pbf) { 'use strict';

    // 用来根据Mapbox style标准构建过滤器和定义过滤方法
    function MvtFilter() {
    }

    /**
     * 根据根据Mapbox style的过滤器对象构造过滤条件
     * @param filter 输入过滤器
     * @returns 返回过滤条件数组
     */
    MvtFilter.parseLayerFilter = function(filter) {
        if (!when.defined(filter) || !(filter instanceof Array)) {
            return null;
        }

        var filterArray = [];
        var condition;
        if (isOperator(filter[0])) {
            condition = parseSingleFilterArray(filter);
            if(when.defined(condition)){
                filterArray.push(condition);
            }
        }
        else{
            for (var fi = 0; fi < filter.length; fi++) {
                if (!(filter[fi] instanceof Array)) {
                    continue;
                }

                if (filter[fi].length !== 3) {
                    for (var fj = 0; fj < filter[fi].length; fj++) {
                        if (filter[fi][fj] instanceof Array && filter[fi][fj].length === 3) {
                            condition = parseSingleFilterArray(filter[fi][fj]);
                            if(when.defined(condition)){
                                filterArray.push(condition);
                            }
                        }
                    }
                } else {
                    condition = parseSingleFilterArray(filter[fi]);
                    if(when.defined(condition)){
                        filterArray.push(condition);
                    }
                }

            }
        }
        return filterArray;
    };

    /**
     * 根据对象的属性进行过滤条件测试
     * @param filterArray 过滤条件数组
     * @param properties 对象属性
     * @returns 通过过滤条件返回true
     */
    MvtFilter.filterTest = function(properties, filterArray) {
        for(var filterIdx = 0, filterCount = filterArray.length; filterIdx < filterCount; filterIdx++){
            var filter = filterArray[filterIdx];
            if(!compareFunctions[filter.filterOperator](properties, filter.filterFieldName, filter.filterCompareValue)){
                return false;
            }
        }
        return true;
    };

    function parseSingleFilterArray(filterArray){
        var filterCompareValue = null;
        var filterFieldName = null;
        var filterOperator = null;
        if (isOperator(filterArray[0])) {
            filterOperator = filterArray[0];
        }
        else{
            return null;
        }

        if(filterArray.length > 1){
            filterFieldName = filterArray[1];
            // TODO: 系统字段暂不处理
            if(filterFieldName[0] === "$" ){
                return null;
            }
        }
        if(filterArray.length > 2){
            filterCompareValue = filterArray[2];
        }
        return {
            filterOperator : filterOperator,
            filterFieldName : filterFieldName,
            filterCompareValue : filterCompareValue
        };
    }

    function isOperator(key) {
        return ["==", "===", ">=", "<=", ">", "<", "!=", "has"].indexOf(key) !== -1;
    }

    function equalFunction(properties, fieldName, testValue){
        return properties[fieldName] == testValue;
    }

    function greaterFunction(properties, fieldName, testValue){
        return properties[fieldName] > testValue;
    }

    function lessFunction(properties, fieldName, testValue){
        return properties[fieldName] < testValue;
    }

    function greaterEqualFunction(properties, fieldName, testValue){
        return properties[fieldName] >= testValue;
    }

    function lessEqualFunction(properties, fieldName, testValue){
        return properties[fieldName] <= testValue;
    }

    function notEqualFunction(properties, fieldName, testValue){
        return properties[fieldName] != testValue;
    }

    function hasFunction(properties, fieldName){
        return when.defined(properties[fieldName]);
    }

    var compareFunctions = {
        "==" : equalFunction,
        "===" : equalFunction,
        ">" : greaterFunction,
        "<" : lessFunction,
        ">=" : greaterEqualFunction,
        "<=" : lessEqualFunction,
        "!=" : notEqualFunction,
        "has" : hasFunction
    };

    function MvtStyle(openlayer, useOffscreen) {
        if(!openlayer){
            throw new Check.DeveloperError('need include ol-debug.js');
        }
        this._useOffscreen = useOffscreen;
        this._openlayer = openlayer;
    }

    Object.defineProperties(MvtStyle.prototype, {
        proxy: {
            get: function() {}
        }
    });

    MvtStyle.prototype.getStyle = function() {
        var openlayer = this._openlayer;
        var fill = new openlayer.style.Fill({
            color: ""
        });
        fill.setColor("#ffffff");

        var stroke = new openlayer.style.Stroke({
            color: "",
            width: 1
        });
        stroke.setWidth(1);
        stroke.setColor("#000000");

        var fillAndOutlineStyle = new openlayer.style.Style({
            fill: fill,
            stroke: stroke
        });

        return fillAndOutlineStyle;
    };

    function parseMapboxColorString(colorString){
        var tempS = colorString.substring(colorString.indexOf("(") + 1, colorString.indexOf(")"));
        tempS = tempS.split(",");
        var resultColor = [];
        resultColor.push(parseFloat(tempS[0]));
        resultColor.push(parseFloat(tempS[1]));
        resultColor.push(parseFloat(tempS[2]));
        resultColor.push(parseFloat(tempS[3]));
        return resultColor;
    }

    function colorWithOpacity(color, opacity) {
        if (color && opacity !== undefined) {
            var colorData = {
                color: [
                    color[0] * 255 / color[3],
                    color[1] * 255 / color[3],
                    color[2] * 255 / color[3],
                    color[3]
                ],
                opacity: color[3]
            };
            color = colorData.color;
            color[3] = colorData.opacity * opacity;
            if (color[3] === 0) {
                color = undefined;
            }
        }
        return color;
    }

    MvtStyle.prototype.getStyleByMapboxStyle = function(mapboxStyle) {
        var openlayer = this._openlayer;
        var type = mapboxStyle.type;
        var paint = mapboxStyle.paint;
        var layout = mapboxStyle.layout;
        if(!when.defined(type) || !when.defined(paint)){
            return this.getStyle();
        }
        if(type == "fill"){
            var fillStyle = new openlayer.style.Style({
            });
            var fill = new openlayer.style.Fill({
                color: "[255,255,255,1]"
            });
            fillStyle.setFill(fill);
            var fillOpcatiy = paint["fill-opacity"];
            if (when.defined(paint["fill-color"])) {
                var fillColor = parseMapboxColorString(paint["fill-color"]);
                if(when.defined(fillOpcatiy)){
                    fillColor[3] *= fillOpcatiy;
                }
                fill.setColor(fillColor);
            }
            if (when.defined(paint["fill-outline-color"])) {
                var fillOutlineStroke = new openlayer.style.Stroke({
                    color: "",
                    width: 1
                });
                fillOutlineStroke.setColor(paint["fill-outline-color"]);
                fillStyle.setStroke(fillOutlineStroke);
            }
            if (when.defined(paint["fill-pattern"])) {
                fillStyle.fillPatternName = paint["fill-pattern"];
            }
            return fillStyle;
        }
        else if(type == "line"){
            var lineStyle = new openlayer.style.Style({
            });
            var lineStroke = new openlayer.style.Stroke({
                color: "#000000",
                width: 1
            });
            lineStyle.setStroke(lineStroke);
            var lineOpcatiy = paint["line-opacity"];
            if (when.defined(paint["line-color"])) {
                var lineColor = parseMapboxColorString(paint["line-color"]);
                if(when.defined(lineOpcatiy)){
                    lineColor[3] *= lineOpcatiy;
                }
            }
            if (when.defined(paint["line-width"])) {
                var lineWidth = paint["line-width"];
                lineStroke.setWidth(lineWidth);
            }

            if (when.defined(paint["line-dasharray"])) {
                var lineDasharray = paint["line-dasharray"];
                lineStroke.setLineDash(lineDasharray);
            }

            if(when.defined(layout)){
                if (when.defined(layout["line-cap"])) {
                    var lineCap = layout["line-cap"];
                    lineStroke.setLineCap(lineCap);
                }
                if (when.defined(layout["line-join"])) {
                    var lineJoin = layout["line-join"];
                    lineStroke.setLineJoin(lineJoin);
                }
                if (when.defined(layout["line-miter-limit"])) {
                    var lineMiterLimit = layout["line-miter-limit"];
                    lineStroke.setMiterLimit(lineMiterLimit);
                }
            }

            lineStroke.setColor(lineColor);
            return lineStyle;
        }
        else if(type == "symbol"){
            var iconStyle = new openlayer.style.Style({
            });
            if(when.defined(layout) && when.defined(layout["icon-image"])){
                iconStyle.hasIconImage = true;
            }
            if(when.defined(layout) && when.defined(layout["text-field"])){
                iconStyle.hasTextStyle = true;
            }
            return iconStyle;
        }
        else if(type == "circle"){
            var circleRadius = paint["circle-radius"];
            var circleColor = paint["circle-color"];
            var circleStrokeColor = paint["circle-stroke-color"];
            var circleOpacity = paint["circle-opacity"];
            var circleStrokeOpacity = paint["circle-stroke-opacity"];
            var circleStrokeWidth = paint["circle-stroke-width"];
            var iconImg = new openlayer.style.Circle({
                radius: circleRadius,
                stroke: circleStrokeWidth === 0 ? undefined : new openlayer.style.Stroke({
                    width: circleStrokeWidth,
                    color: colorWithOpacity(circleStrokeColor, circleStrokeOpacity)
                }),
                fill: new openlayer.style.Fill({
                    color: colorWithOpacity(circleColor, circleOpacity)
                })
            });
            var circleStyle = new openlayer.style.Style({
            });
            circleStyle.setImage(iconImg);
            return circleStyle;
        }
        else{
            return this.getStyle();
        }
    };

    var scratchIDColor = new Color.Color();
    function convertIDtoColor(id, layerID){
        var colorB = Math.floor(id / 65536);
        var d = id - colorB * 65536;
        var colorG = Math.floor(d / 256);
        var colorR = d - colorG * 256;
        var alpha = 1;
        scratchIDColor.red = colorR / 256;
        scratchIDColor.green = colorG / 256;
        scratchIDColor.blue = colorB / 256;
        scratchIDColor.alpha = alpha;
        return scratchIDColor;
    }

    MvtStyle.prototype.getIDColorStyle = function(geometryType, id, layerID, lineWidth, radius, lineWidthExpand) {
        var openlayer = this._openlayer;
        var idColor = convertIDtoColor(id);
        var cssColor = idColor.toCssColorString();
        if(geometryType ==  'LineString' || geometryType ==  'LinearRing' || geometryType ==  'MultiLineString'){
            var expandWidth = 4;
            if (when.defined(lineWidth)) {
                expandWidth = lineWidth * 2 + lineWidthExpand;
            }
            var scratchIDStroke = new openlayer.style.Stroke({
                color: "",
                width: expandWidth
            });
            scratchIDStroke.setColor(cssColor);
            return new openlayer.style.Style({
                stroke: scratchIDStroke
            });
        }
        else if(geometryType == 'Point' || geometryType == 'MultiPoint'){
            var iconImg = new openlayer.style.Circle({
                radius: (radius - 0.5),
                fill: new openlayer.style.Fill({
                    color: cssColor
                })
            });
            var circleStyle = new openlayer.style.Style({
            });
            circleStyle.setImage(iconImg);
            return circleStyle;
        }
        else{
            var scratchIDFill = new openlayer.style.Fill({
                color: ""
            });
            scratchIDFill.setColor(cssColor);
            var resultStyle = new openlayer.style.Style({
                fill: scratchIDFill
            });
            if(when.defined(lineWidth)){
                var scratchIDStroke = new openlayer.style.Stroke({
                    color: "",
                    width: when.defined(lineWidth) ? lineWidth * 2 : 4
                });
                scratchIDStroke.setColor(cssColor);
                resultStyle.setStroke(scratchIDStroke);
            }
            return resultStyle;
        }
    };

    var templateRegEx = /^([^]*)\{(.*)\}([^]*)$/;
    function fromTemplate(text, properties) {
        var parts;
        do {
            parts = text.match(templateRegEx);
            if (parts) {
                const value = properties[parts[2]] || '';
                text = parts[1] + value + parts[3];
            }
        } while (parts);
        return text;
    }

    MvtStyle.prototype.getTextStyle = function(oldStyle, feature, mapboxStyleLayer) {
        var openlayer = this._openlayer;
        var paint = mapboxStyleLayer.paint;
        var layout = mapboxStyleLayer.layout;
        var textField = layout['text-field'];
        var label = fromTemplate(textField, feature.getProperties());
        if(!when.defined(label)){
            return;
        }
        var style = new openlayer.style.Style();
        var text = new openlayer.style.Text();
        style.setText(text);
        var textSize = layout['text-size'];
        var font = when.defaultValue(layout['text-font'], ['Open Sans Regular', 'Arial Unicode MS Regular']);
        var textTransform = layout['text-transform'];
        if (textTransform == 'uppercase') {
            label = label.toUpperCase();
        } else if (textTransform == 'lowercase') {
            label = label.toLowerCase();
        }
        var textMaxWidth = when.defaultValue(layout['text-max-width'], 10);
        //var wrappedLabel = wrapText(label, font, textMaxWidth);
        var wrappedLabel = label;
        text.setText(wrappedLabel);
        text.setFont(font);
        text.setRotation(0);

        var textAnchor = when.defaultValue(layout['text-anchor'], 'center');
        var placement = when.defaultValue(layout['symbol-placement'], 'point');
        text.setPlacement(placement);
        if (placement == 'point') {
            var textAlign = 'center';
            if (textAnchor.indexOf('left') !== -1) {
                textAlign = 'left';
            } else if (textAnchor.indexOf('right') !== -1) {
                textAlign = 'right';
            }
            text.setTextAlign(textAlign);
        } else {
            text.setTextAlign();
        }
        var textBaseline = 'middle';
        if (textAnchor.indexOf('bottom') == 0) {
            textBaseline = 'bottom';
        } else if (textAnchor.indexOf('top') == 0) {
            textBaseline = 'top';
        }
        text.setTextBaseline(textBaseline);
        var textOffset = when.defaultValue(layout['text-offset'], [0.0, 0.0]);
        var textTranslate = when.defaultValue(layout['text-translate'], [0.0, 0.0]);
        text.setOffsetX(textOffset[0] * textSize + textTranslate[0]);
        text.setOffsetY(textOffset[1] * textSize + textTranslate[1]);
        var opacity = paint['text-opacity'];
        var textColorFill = new openlayer.style.Fill();
        var textColor = paint["text-color"];
        if(when.defined(textColor)){
            textColor = parseMapboxColorString(textColor);
            textColorFill.setColor(textColor);
        }
        text.setFill(textColorFill);
        var textHaloColor = paint["text-halo-color"];
        if (when.defined(textHaloColor)) {
            var textHalo = new openlayer.style.Stroke();
            textHaloColor = parseMapboxColorString(textHaloColor);
            textHalo.setColor(textHaloColor);
            textHalo.setWidth(paint['text-halo-width']);
            text.setStroke(textHalo);
        } else {
            text.setStroke(undefined);
        }
        style.setZIndex(oldStyle.getZIndex());
        style.hasIconImage = oldStyle.hasIconImage;
        style.textSize = textSize;
        return style;
    };

    function covertIconAnchor(iconAnchor) {
        var anchorOffset = [0.5, 0.5];
        if (['top-left', 'top-right', 'bottom-left', 'bottom-right'].includes(iconAnchor)) {
            anchorOffset = [0, 0];
        }
        if (iconAnchor === 'left') {
            iconAnchor = 'top-left';
            anchorOffset = [0, 0.5];
        }
        if (iconAnchor === 'right') {
            iconAnchor = 'top-left';
            anchorOffset = [1, 0.5];
        }
        if (iconAnchor === 'bottom') {
            iconAnchor = 'top-left';
            anchorOffset = [0.5, 1];
        }
        if (iconAnchor === 'top') {
            iconAnchor = 'top-left';
            anchorOffset = [0.5, 0];
        }
        return {
            anchorOffset: anchorOffset,
            iconAnchor: iconAnchor
        };
    }

    var iconImageCache = {};

    MvtStyle.prototype.setIconImageForStyle = function(spriteImageDatas, style, mapboxStyleLayer){
        var openlayer = this._openlayer;
        var paint = mapboxStyleLayer.paint;
        var layout = mapboxStyleLayer.layout;
        var iconSize = when.defaultValue(layout["icon-size"], 1);
        var iconColor = paint['icon-color'];
        var iconTranslate = when.defaultValue(paint['icon-translate'], [0.0, 0.0]);
        var iconTranslateAnchor = when.defaultValue(paint['icon-translate-anchor'], 'map');
        var iconAnchor = when.defaultValue(layout['icon-anchor'], 'center');
        var anchorOffsetAndIconAnchor = covertIconAnchor(iconAnchor);
        var anchorOffset = anchorOffsetAndIconAnchor.anchorOffset;
        var iconOffset = when.defaultValue(layout['iconoffset'], [0.0, 0.0]);
        var iconOpacity = when.defaultValue(layout['icon-opacity'], 1.0);
        var spriteImageName = layout["icon-image"];
        if(!when.defined(spriteImageDatas[spriteImageName])){
            console.log('miss icon-image ' + spriteImageName);
            return;
        }

        var icon_cache_key = spriteImageName + '.' + iconSize + '.' + iconTranslate + '.' + iconTranslateAnchor + '.' + iconAnchor + '.' + iconOffset;
        if (when.defined(iconColor)) {
            icon_cache_key += '.' + iconColor;
        }
        var iconImg = iconImageCache[icon_cache_key];
        if(!when.defined(iconImg)){
            var spriteImage = spriteImageDatas[spriteImageName];
            var canvas;
            if(this._useOffscreen){
                canvas = new OffscreenCanvas(spriteImage.width, spriteImage.height);
            }
            else{
                canvas = document.createElement('canvas');
                canvas.width = spriteImage.width;
                canvas.height = spriteImage.height;
            }
            var ctx = canvas.getContext('2d');
            ctx.putImageData(spriteImage, 0, 0);
            var translateOffset = [iconTranslate[0] / spriteImage.width, iconTranslate[1] / spriteImage.height];
            iconImg = new openlayer.style.Icon({
                img: canvas,
                anchorOrigin: anchorOffsetAndIconAnchor.iconAnchor,
                anchor: [iconOffset[0] + anchorOffset[0] + translateOffset[0], iconOffset[1] + anchorOffset[1] - translateOffset[1]],
                imgSize: [canvas.width, canvas.height],
                scale: iconSize
            });
            iconImg.setOpacity(iconOpacity);
            iconImageCache[icon_cache_key] = iconImg;
        }

        style.setImage(iconImg);
        //style.setText(undefined);
    };

    var VALUE_EXTENT = 4096;
    var replays = ["Default", "Polygon", "LineString", "Image", "Symbol", "Text"];

    function MvtRenderer2D(options) {
        this._mvtStyleClass = options.mvtStyle;
        this._openlayer = options.openlayer;
    }

    Object.defineProperties(MvtRenderer2D.prototype, {
    });

    MvtRenderer2D.prototype.renderFeatures = function(options) {
        var canvas = options.colorCanvas;
        var idCanvas = options.idCanvas;
        var transform = options.transform;
        var layers = options.layers;
        var features = options.features;
        var tileLevel = options.tileLevel;
        var spriteImageCanvas = options.spriteImageCanvas;
        var spriteImageDatas = options.spriteImageDatas;
        var squaredTolerance = options.squaredTolerance;
        var showBillboard = options.showBillboard;
        var renderID = options.renderID;
        var renderColor = options.renderColor;
        var lineWidthExpand = options.lineWidthExpand;

        var ol = this._openlayer;
        var ctx = canvas.getContext('2d');

        var idFeatures = [];
        var iconImageObjects = [];
        var textObjects = [];
        var style = null;
        var declutterTree = ol.ext.rbush(9);
        var replayGroup = new ol.render.canvas.ReplayGroup(0, [0, 0, VALUE_EXTENT, VALUE_EXTENT], 8, 2, true, declutterTree);

        var featureLength = features.length;
        for (var r = 0; r < featureLength; r++) {
            var feature = features[r];
            var sourceLayer = feature.getProperties().layer;
            feature.index = sourceLayer + feature.getId();
            var featureHasStyle = false;
            var layerGroupById = layers[sourceLayer];
            var zIndex = 0;
            for (var layerId in layerGroupById) {
                var layerById = layerGroupById[layerId];

                var maxzoom = layerById.mapboxStyleLayer.maxzoom;
                var minzoom = layerById.mapboxStyleLayer.minzoom;
                if(tileLevel < minzoom || tileLevel > maxzoom){
                    continue;
                }

                var filterArray = layerById.filterArray;
                if (!when.defined(filterArray)) {
                    style = this._mvtStyleClass.getStyleByMapboxStyle(layerById.mapboxStyleLayer);
                }
                else {
                    var properties = feature.getProperties();
                    if (MvtFilter.filterTest(properties, filterArray)) {
                        style = this._mvtStyleClass.getStyleByMapboxStyle(layerById.mapboxStyleLayer);
                    }
                    else {
                        continue;
                    }
                }

                if (!when.defined(style)) {
                    continue;
                }

                this.createFillPatternForStyle(style, spriteImageCanvas, spriteImageDatas, ctx);

                if (when.defined(style.hasTextStyle)) {
                    var textStyle = this._mvtStyleClass.getTextStyle(style, feature, layerById.mapboxStyleLayer);
                    textStyle.setZIndex(zIndex);
                    if(showBillboard){
                        textObjects.push({
                            feature : feature,
                            style : textStyle
                        });
                    }
                    else{
                        if(renderColor){
                            ol.renderer.vector.renderFeature_(replayGroup, feature, textStyle, -1);
                        }
                    }
                }

                if (when.defined(style.hasIconImage) && !when.defined(style.getImage())) {
                    if(showBillboard){
                        iconImageObjects.push({
                            feature : feature,
                            style : layerById.mapboxStyleLayer
                        });
                        continue;
                    }
                    else{
                        this._mvtStyleClass.setIconImageForStyle(spriteImageDatas, style, layerById.mapboxStyleLayer);
                    }
                }

                style.setZIndex(zIndex);
                this.setPickStyleInFeature(feature, style);
                zIndex++;

                if(renderColor){
                    ol.renderer.vector.renderFeature_(replayGroup, feature, style, -1);
                }
                featureHasStyle = true;
            }
            if (featureHasStyle) {
                idFeatures.push(feature);
            }
        }

        if(renderColor){
            replayGroup.finish();
            var declutterReplays = {};
            replayGroup.replay(ctx, transform, 0, {}, replays, declutterReplays);
            if (declutterReplays) {
                ol.render.canvas.ReplayGroup.replayDeclutter(declutterReplays, ctx, 0.0);
            }
        }

        replayGroup = null;
        if(renderID){
            this.renderIDtoTexture(transform, idCanvas, idFeatures, 0, squaredTolerance, lineWidthExpand);
        }
        return {
            idFeatures : idFeatures,
            iconImageObjects : iconImageObjects,
            textObjects : textObjects
        }
    };

    MvtRenderer2D.prototype.renderIDtoTexture = function(transform, canvas, features, layerID, tileTolerance, lineWidthExpand) {
        var ol = this._openlayer;
        var ctx = canvas.getContext('2d');
        var declutterTree = ol.ext.rbush(9);
        var replayGroup = new ol.render.canvas.ReplayGroup(0, [0, 0, VALUE_EXTENT, VALUE_EXTENT], 8, 2, true, declutterTree);
        var featureLength = features.length;
        for (var r = 0; r < featureLength; r++) {
            var feature = features[r];
            var id = getFeatureID(feature);
            var idStyle = this._mvtStyleClass.getIDColorStyle(feature.getGeometry().getType(), id, layerID, feature.lineWidth, feature.radius, lineWidthExpand);
            idStyle.setZIndex(feature.zIndex);
            ol.renderer.vector.renderFeature_(replayGroup, feature, idStyle, -1);
        }
        replayGroup.finish();
        var declutterReplays = {};
        replayGroup.replay(ctx, transform, 0, {}, replays, declutterReplays);
        if (declutterReplays) {
            ol.render.canvas.ReplayGroup.replayDeclutter(declutterReplays, ctx, 0.0);
        }
        replayGroup = null;
    };

    MvtRenderer2D.prototype.createFillPatternForStyle = function(style, spriteImageCanvas, subSpriteImage, ctx){
        if(!when.defined(style.fillPatternName)){
            return;
        }
        var patternName = style.fillPatternName;
        var patternCanvas = null;
        if (when.defined(spriteImageCanvas[patternName])) {
            patternCanvas = spriteImageCanvas[patternName];
        }
        else {
            var imageData = subSpriteImage[patternName];
            if (!when.defined(imageData)) {
                console.log('miss sprite ' + patternName);
                return;
            }
            patternCanvas = document.createElement('canvas');
            patternCanvas.width = imageData.width;
            patternCanvas.height = imageData.height;
            var spriteCtx = patternCanvas.getContext('2d');
            spriteCtx.putImageData(imageData, 0, 0);
            spriteImageCanvas[patternName] = patternCanvas;
        }
        style.fill_.color_ = ctx.createPattern(patternCanvas, 'repeat');
    };

    MvtRenderer2D.prototype.setPickStyleInFeature = function(feature, style){
        var ol = this._openlayer;
        feature.zIndex = style.getZIndex();
        if(when.defined(style.getStroke())){
            var styleLineWidth = style.getStroke().getWidth();
            if(when.defined(feature.lineWidth)){
                feature.lineWidth = Math.max(feature.lineWidth, styleLineWidth);
            }
            else{
                feature.lineWidth = styleLineWidth;
            }
        }
        if(when.defined(style.getImage())){
            var imageStyle = style.getImage();
            var radius = 1.0;
            if(imageStyle instanceof ol.style.Icon ){
                var imageSize = imageStyle.getImageSize();
                radius = Math.max(imageSize[0], imageSize[1]) / 2.0;
                radius -= 1.0;
            }
            else if(imageStyle instanceof ol.style.Circle ){
                radius = imageStyle.getRadius();
            }
            if(when.defined(feature.radius)){
                feature.radius = Math.max(feature.radius, radius);
            }
            else{
                feature.radius = radius;
            }
        }
    };

    function getFeatureID(feature) {
        var id = feature.getId();
        // 只在颜色中记录256*256*256这么大范围的ID，超过这个范围的ID舍去
        var discard = Math.floor(id / 16777216);
        id = id - discard * 16777216;
        return id;
    }

    function ol() {
        }

        ol.array = {};

        /**
         * Performs a binary search on the provided sorted list and returns the index of the item if found. If it can't be found it'll return -1.
         * https://github.com/darkskyapp/binary-search
         *
         * @param {Array.<*>} haystack Items to search through.
         * @param {*} needle The item to look for.
         * @param {Function=} opt_comparator Comparator function.
         * @return {number} The index of the item if found, -1 if not.
         */
        ol.array.binarySearch = function(haystack, needle, opt_comparator) {
            var mid, cmp;
            var comparator = opt_comparator || ol.array.numberSafeCompareFunction;
            var low = 0;
            var high = haystack.length;
            var found = false;

            while (low < high) {
                /* Note that "(low + high) >>> 1" may overflow, and results in a typecast
                 * to double (which gives the wrong results). */
                mid = low + (high - low >> 1);
                cmp = +comparator(haystack[mid], needle);

                if (cmp < 0.0) { /* Too low. */
                    low  = mid + 1;

                } else { /* Key found or too high */
                    high = mid;
                    found = !cmp;
                }
            }

            /* Key not found. */
            return found ? low : ~low;
        };


        /**
         * Compare function for array sort that is safe for numbers.
         * @param {*} a The first object to be compared.
         * @param {*} b The second object to be compared.
         * @return {number} A negative number, zero, or a positive number as the first
         *     argument is less than, equal to, or greater than the second.
         */
        ol.array.numberSafeCompareFunction = function(a, b) {
            return a > b ? 1 : a < b ? -1 : 0;
        };


        /**
         * Whether the array contains the given object.
         * @param {Array.<*>} arr The array to test for the presence of the element.
         * @param {*} obj The object for which to test.
         * @return {boolean} The object is in the array.
         */
        ol.array.includes = function(arr, obj) {
            return arr.indexOf(obj) >= 0;
        };


        /**
         * @param {Array.<number>} arr Array.
         * @param {number} target Target.
         * @param {number} direction 0 means return the nearest, > 0
         *    means return the largest nearest, < 0 means return the
         *    smallest nearest.
         * @return {number} Index.
         */
        ol.array.linearFindNearest = function(arr, target, direction) {
            var n = arr.length;
            if (arr[0] <= target) {
                return 0;
            } else if (target <= arr[n - 1]) {
                return n - 1;
            } else {
                var i;
                if (direction > 0) {
                    for (i = 1; i < n; ++i) {
                        if (arr[i] < target) {
                            return i - 1;
                        }
                    }
                } else if (direction < 0) {
                    for (i = 1; i < n; ++i) {
                        if (arr[i] <= target) {
                            return i;
                        }
                    }
                } else {
                    for (i = 1; i < n; ++i) {
                        if (arr[i] == target) {
                            return i;
                        } else if (arr[i] < target) {
                            if (arr[i - 1] - target < target - arr[i]) {
                                return i - 1;
                            } else {
                                return i;
                            }
                        }
                    }
                }
                return n - 1;
            }
        };


        /**
         * @param {Array.<*>} arr Array.
         * @param {number} begin Begin index.
         * @param {number} end End index.
         */
        ol.array.reverseSubArray = function(arr, begin, end) {
            while (begin < end) {
                var tmp = arr[begin];
                arr[begin] = arr[end];
                arr[end] = tmp;
                ++begin;
                --end;
            }
        };

        /**
         * @param {Array.<VALUE>} arr The array to modify.
         * @param {Array.<VALUE>|VALUE} data The elements or arrays of elements
         *     to add to arr.
         * @template VALUE
         */
        ol.array.extend = function(arr, data) {
            var i;
            var extension = Array.isArray(data) ? data : [data];
            var length = extension.length;
            for (i = 0; i < length; i++) {
                arr[arr.length] = extension[i];
            }
        };


        /**
         * @param {Array.<VALUE>} arr The array to modify.
         * @param {VALUE} obj The element to remove.
         * @template VALUE
         * @return {boolean} If the element was removed.
         */
        ol.array.remove = function(arr, obj) {
            var i = arr.indexOf(obj);
            var found = i > -1;
            if (found) {
                arr.splice(i, 1);
            }
            return found;
        };


        /**
         * @param {Array.<VALUE>} arr The array to search in.
         * @param {function(VALUE, number, ?) : boolean} func The function to compare.
         * @template VALUE
         * @return {VALUE} The element found.
         */
        ol.array.find = function(arr, func) {
            var length = arr.length >>> 0;
            var value;

            for (var i = 0; i < length; i++) {
                value = arr[i];
                if (func(value, i, arr)) {
                    return value;
                }
            }
            return null;
        };


        /**
         * @param {Array|Uint8ClampedArray} arr1 The first array to compare.
         * @param {Array|Uint8ClampedArray} arr2 The second array to compare.
         * @return {boolean} Whether the two arrays are equal.
         */
        ol.array.equals = function(arr1, arr2) {
            var len1 = arr1.length;
            if (len1 !== arr2.length) {
                return false;
            }
            for (var i = 0; i < len1; i++) {
                if (arr1[i] !== arr2[i]) {
                    return false;
                }
            }
            return true;
        };


        /**
         * @param {Array.<*>} arr The array to sort (modifies original).
         * @param {Function} compareFnc Comparison function.
         */
        ol.array.stableSort = function(arr, compareFnc) {
            var length = arr.length;
            var tmp = Array(arr.length);
            var i;
            for (i = 0; i < length; i++) {
                tmp[i] = {index: i, value: arr[i]};
            }
            tmp.sort(function(a, b) {
                return compareFnc(a.value, b.value) || a.index - b.index;
            });
            for (i = 0; i < arr.length; i++) {
                arr[i] = tmp[i].value;
            }
        };


        /**
         * @param {Array.<*>} arr The array to search in.
         * @param {Function} func Comparison function.
         * @return {number} Return index.
         */
        ol.array.findIndex = function(arr, func) {
            var index;
            var found = !arr.every(function(el, idx) {
                index = idx;
                return !func(el, idx, arr);
            });
            return found ? index : -1;
        };


        /**
         * @param {Array.<*>} arr The array to test.
         * @param {Function=} opt_func Comparison function.
         * @param {boolean=} opt_strict Strictly sorted (default false).
         * @return {boolean} Return index.
         */
        ol.array.isSorted = function(arr, opt_func, opt_strict) {
            var compare = opt_func || ol.array.numberSafeCompareFunction;
            return arr.every(function(currentVal, index) {
                if (index === 0) {
                    return true;
                }
                var res = compare(arr[index - 1], currentVal);
                return !(res > 0 || opt_strict && res === 0);
            });
        };


        ol.ASSUME_TOUCH = false;
        ol.DEFAULT_MAX_ZOOM = 42;
        ol.DEFAULT_MIN_ZOOM = 0;
        ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD = 0.5;
        ol.DEFAULT_TILE_SIZE = 256;
        ol.DEFAULT_WMS_VERSION = '1.3.0';
        ol.ENABLE_CANVAS = true;
        ol.ENABLE_PROJ4JS = true;
        ol.ENABLE_RASTER_REPROJECTION = true;
        ol.ENABLE_WEBGL = true;
        ol.DEBUG_WEBGL = true;
        ol.INITIAL_ATLAS_SIZE = 256;
        ol.MAX_ATLAS_SIZE = -1;
        ol.MOUSEWHEELZOOM_MAXDELTA = 1;
        ol.OVERVIEWMAP_MAX_RATIO = 0.75;
        ol.OVERVIEWMAP_MIN_RATIO = 0.1;
        ol.RASTER_REPROJECTION_MAX_SOURCE_TILES = 100;
        ol.RASTER_REPROJECTION_MAX_SUBDIVISION = 10;
        ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25;
        ol.SIMPLIFY_TOLERANCE = 0.5;
        ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024;
        ol.VERSION = '';

        ol.inherits = function(childCtor, parentCtor) {
            childCtor.prototype = Object.create(parentCtor.prototype);
            childCtor.prototype.constructor = childCtor;
        };

        ol.nullFunction = function() {};

        ol.getUid = function(obj) {
            return obj.ol_uid ||
                (obj.ol_uid = ++ol.uidCounter_);
        };

        ol.asserts = {};
        ol.asserts.assert = function(assertion, errorCode) {
        };

        ol.has = {};

        var ua = typeof navigator !== 'undefined' ?
            navigator.userAgent.toLowerCase() : '';

        ol.has.FIREFOX = ua.indexOf('firefox') !== -1;
        ol.has.SAFARI = ua.indexOf('safari') !== -1 && ua.indexOf('chrom') == -1;
        ol.has.WEBKIT = ua.indexOf('webkit') !== -1 && ua.indexOf('edge') == -1;
        ol.has.MAC = ua.indexOf('macintosh') !== -1;
        ol.has.DEVICE_PIXEL_RATIO = 1;
        ol.has.CANVAS_LINE_DASH = true;

        ol.structs = {};

        /**
         * @enum {string}
         */
        ol.CollectionEventType = {
            ADD: 'add',
            REMOVE: 'remove'
        };

        /**
         * @enum {string}
         */
        ol.ObjectEventType = {
            PROPERTYCHANGE: 'propertychange'
        };

        ol.events = {};

        /**
         * @param {ol.EventsKey} listenerObj Listener object.
         * @return {ol.EventsListenerFunctionType} Bound listener.
         */
        ol.events.bindListener_ = function(listenerObj) {
            var boundListener = function(evt) {
                var listener = listenerObj.listener;
                var bindTo = listenerObj.bindTo || listenerObj.target;
                if (listenerObj.callOnce) {
                    ol.events.unlistenByKey(listenerObj);
                }
                return listener.call(bindTo, evt);
            };
            listenerObj.boundListener = boundListener;
            return boundListener;
        };

        ol.events.findListener_ = function(listeners, listener, opt_this,
                                           opt_setDeleteIndex) {
            var listenerObj;
            for (var i = 0, ii = listeners.length; i < ii; ++i) {
                listenerObj = listeners[i];
                if (listenerObj.listener === listener &&
                    listenerObj.bindTo === opt_this) {
                    if (opt_setDeleteIndex) {
                        listenerObj.deleteIndex = i;
                    }
                    return listenerObj;
                }
            }
            return undefined;
        };


        /**
         * @param {ol.EventTargetLike} target Target.
         * @param {string} type Type.
         * @return {Array.<ol.EventsKey>|undefined} Listeners.
         */
        ol.events.getListeners = function(target, type) {
            var listenerMap = target.ol_lm;
            return listenerMap ? listenerMap[type] : undefined;
        };


        /**
         * Get the lookup of listeners.  If one does not exist on the target, it is
         * created.
         * @param {ol.EventTargetLike} target Target.
         * @return {!Object.<string, Array.<ol.EventsKey>>} Map of
         *     listeners by event type.
         * @private
         */
        ol.events.getListenerMap_ = function(target) {
            var listenerMap = target.ol_lm;
            if (!listenerMap) {
                listenerMap = target.ol_lm = {};
            }
            return listenerMap;
        };


        /**
         * Clean up all listener objects of the given type.  All properties on the
         * listener objects will be removed, and if no listeners remain in the listener
         * map, it will be removed from the target.
         * @param {ol.EventTargetLike} target Target.
         * @param {string} type Type.
         * @private
         */
        ol.events.removeListeners_ = function(target, type) {
            var listeners = ol.events.getListeners(target, type);
            if (listeners) {
                for (var i = 0, ii = listeners.length; i < ii; ++i) {
                    target.removeEventListener(type, listeners[i].boundListener);
                    ol.obj.clear(listeners[i]);
                }
                listeners.length = 0;
                var listenerMap = target.ol_lm;
                if (listenerMap) {
                    delete listenerMap[type];
                    if (Object.keys(listenerMap).length === 0) {
                        delete target.ol_lm;
                    }
                }
            }
        };

        ol.events.listen = function(target, type, listener, opt_this, opt_once) {
            var listenerMap = ol.events.getListenerMap_(target);
            var listeners = listenerMap[type];
            if (!listeners) {
                listeners = listenerMap[type] = [];
            }
            var listenerObj = ol.events.findListener_(listeners, listener, opt_this,
                false);
            if (listenerObj) {
                if (!opt_once) {
                    // Turn one-off listener into a permanent one.
                    listenerObj.callOnce = false;
                }
            } else {
                listenerObj = /** @type {ol.EventsKey} */ ({
                    bindTo: opt_this,
                    callOnce: !!opt_once,
                    listener: listener,
                    target: target,
                    type: type
                });
                target.addEventListener(type, ol.events.bindListener_(listenerObj));
                listeners.push(listenerObj);
            }

            return listenerObj;
        };

        ol.events.listenOnce = function(target, type, listener, opt_this) {
            return ol.events.listen(target, type, listener, opt_this, true);
        };

        ol.events.unlisten = function(target, type, listener, opt_this) {
            var listeners = ol.events.getListeners(target, type);
            if (listeners) {
                var listenerObj = ol.events.findListener_(listeners, listener, opt_this,
                    true);
                if (listenerObj) {
                    ol.events.unlistenByKey(listenerObj);
                }
            }
        };

        ol.events.unlistenByKey = function(key) {
            if (key && key.target) {
                key.target.removeEventListener(key.type, key.boundListener);
                var listeners = ol.events.getListeners(key.target, key.type);
                if (listeners) {
                    var i = 'deleteIndex' in key ? key.deleteIndex : listeners.indexOf(key);
                    if (i !== -1) {
                        listeners.splice(i, 1);
                    }
                    if (listeners.length === 0) {
                        ol.events.removeListeners_(key.target, key.type);
                    }
                }
                ol.obj.clear(key);
            }
        };

        ol.events.unlistenAll = function(target) {
            var listenerMap = ol.events.getListenerMap_(target);
            for (var type in listenerMap) {
                ol.events.removeListeners_(target, type);
            }
        };

        /**
         * Objects that need to clean up after themselves.
         * @constructor
         */
        ol.Disposable = function() {};

        ol.Disposable.prototype.disposed_ = false;

        ol.Disposable.prototype.dispose = function() {
            if (!this.disposed_) {
                this.disposed_ = true;
                this.disposeInternal();
            }
        };

        ol.Disposable.prototype.disposeInternal = ol.nullFunction;

        ol.events.Event = {};

        ol.events.Event = function(type) {
            this.propagationStopped;
            this.type = type;
            this.target = null;
        };

        ol.events.Event.prototype.preventDefault =

        /**
         * Stop event propagation.
         * @function
         * @override
         * @api
         */
            ol.events.Event.prototype.stopPropagation = function() {
                this.propagationStopped = true;
            };


        /**
         * @param {Event|ol.events.Event} evt Event
         */
        ol.events.Event.stopPropagation = function(evt) {
            evt.stopPropagation();
        };


        /**
         * @param {Event|ol.events.Event} evt Event
         */
        ol.events.Event.preventDefault = function(evt) {
            evt.preventDefault();
        };

        ol.events.EventTarget = {};

        ol.events.EventTarget = function() {
            ol.Disposable.call(this);
            this.pendingRemovals_ = {};
            this.dispatching_ = {};
            this.listeners_ = {};

        };
        ol.inherits(ol.events.EventTarget, ol.Disposable);


        /**
         * @param {string} type Type.
         * @param {ol.EventsListenerFunctionType} listener Listener.
         */
        ol.events.EventTarget.prototype.addEventListener = function(type, listener) {
            var listeners = this.listeners_[type];
            if (!listeners) {
                listeners = this.listeners_[type] = [];
            }
            if (listeners.indexOf(listener) === -1) {
                listeners.push(listener);
            }
        };

        ol.events.EventTarget.prototype.dispatchEvent = function(event) {
            var evt = typeof event === 'string' ? new ol.events.Event(event) : event;
            var type = evt.type;
            evt.target = this;
            var listeners = this.listeners_[type];
            var propagate;
            if (listeners) {
                if (!(type in this.dispatching_)) {
                    this.dispatching_[type] = 0;
                    this.pendingRemovals_[type] = 0;
                }
                ++this.dispatching_[type];
                for (var i = 0, ii = listeners.length; i < ii; ++i) {
                    if (listeners[i].call(this, evt) === false || evt.propagationStopped) {
                        propagate = false;
                        break;
                    }
                }
                --this.dispatching_[type];
                if (this.dispatching_[type] === 0) {
                    var pendingRemovals = this.pendingRemovals_[type];
                    delete this.pendingRemovals_[type];
                    while (pendingRemovals--) {
                        this.removeEventListener(type, ol.nullFunction);
                    }
                    delete this.dispatching_[type];
                }
                return propagate;
            }
        };


        /**
         * @inheritDoc
         */
        ol.events.EventTarget.prototype.disposeInternal = function() {
            ol.events.unlistenAll(this);
        };


        /**
         * Get the listeners for a specified event type. Listeners are returned in the
         * order that they will be called in.
         *
         * @param {string} type Type.
         * @return {Array.<ol.EventsListenerFunctionType>} Listeners.
         */
        ol.events.EventTarget.prototype.getListeners = function(type) {
            return this.listeners_[type];
        };


        /**
         * @param {string=} opt_type Type. If not provided,
         *     `true` will be returned if this EventTarget has any listeners.
         * @return {boolean} Has listeners.
         */
        ol.events.EventTarget.prototype.hasListener = function(opt_type) {
            return opt_type ?
                opt_type in this.listeners_ :
                Object.keys(this.listeners_).length > 0;
        };


        /**
         * @param {string} type Type.
         * @param {ol.EventsListenerFunctionType} listener Listener.
         */
        ol.events.EventTarget.prototype.removeEventListener = function(type, listener) {
            var listeners = this.listeners_[type];
            if (listeners) {
                var index = listeners.indexOf(listener);
                if (type in this.pendingRemovals_) {
                    // make listener a no-op, and remove later in #dispatchEvent()
                    listeners[index] = ol.nullFunction;
                    ++this.pendingRemovals_[type];
                } else {
                    listeners.splice(index, 1);
                    if (listeners.length === 0) {
                        delete this.listeners_[type];
                    }
                }
            }
        };

        /**
         * @enum {string}
         * @const
         */
        ol.events.EventType = {
            CHANGE: 'change',
            CLEAR: 'clear',
            CLICK: 'click',
            DBLCLICK: 'click',
            DRAGENTER: 'dragenter',
            DRAGOVER: 'dragover',
            DROP: 'drop',
            ERROR: 'error',
            KEYDOWN: 'keydown',
            KEYPRESS: 'keypress',
            LOAD: 'load',
            MOUSEDOWN: 'mousedown',
            MOUSEMOVE: 'mousemove',
            MOUSEOUT: 'mouseout',
            MOUSEUP: 'mouseup',
            MOUSEWHEEL: 'mousewheel',
            MSPOINTERDOWN: 'MSPointerDown',
            RESIZE: 'resize',
            TOUCHSTART: 'touchstart',
            TOUCHMOVE: 'touchmove',
            TOUCHEND: 'touchend',
            WHEEL: 'wheel'
        };

        ol.Observable = function() {
            this.revision_ = 0;

        };
        ol.inherits(ol.Observable, ol.events.EventTarget);

        ol.Observable.unByKey = function(key) {
            if (Array.isArray(key)) {
                for (var i = 0, ii = key.length; i < ii; ++i) {
                    ol.events.unlistenByKey(key[i]);
                }
            } else {
                ol.events.unlistenByKey(/** @type {ol.EventsKey} */ (key));
            }
        };


        /**
         * Increases the revision counter and dispatches a 'change' event.
         * @api
         */
        ol.Observable.prototype.changed = function() {
            ++this.revision_;
            //this.dispatchEvent(ol.events.EventType.CHANGE);
        };


        /**
         * Dispatches an event and calls all listeners listening for events
         * of this type. The event parameter can either be a string or an
         * Object with a `type` property.
         *
         * @param {{type: string,
     *     target: (EventTarget|ol.events.EventTarget|undefined)}|ol.events.Event|
     *     string} event Event object.
         * @function
         * @api
         */
        ol.Observable.prototype.dispatchEvent;


        /**
         * Get the version number for this object.  Each time the object is modified,
         * its version number will be incremented.
         * @return {number} Revision.
         * @api
         */
        ol.Observable.prototype.getRevision = function() {
            return this.revision_;
        };


        /**
         * Listen for a certain type of event.
         * @param {string|Array.<string>} type The event type or array of event types.
         * @param {function(?): ?} listener The listener function.
         * @param {Object=} opt_this The object to use as `this` in `listener`.
         * @return {ol.EventsKey|Array.<ol.EventsKey>} Unique key for the listener. If
         *     called with an array of event types as the first argument, the return
         *     will be an array of keys.
         * @api
         */
        ol.Observable.prototype.on = function(type, listener, opt_this) {
            if (Array.isArray(type)) {
                var len = type.length;
                var keys = new Array(len);
                for (var i = 0; i < len; ++i) {
                    keys[i] = ol.events.listen(this, type[i], listener, opt_this);
                }
                return keys;
            } else {
                return ol.events.listen(
                    this, /** @type {string} */ (type), listener, opt_this);
            }
        };


        /**
         * Listen once for a certain type of event.
         * @param {string|Array.<string>} type The event type or array of event types.
         * @param {function(?): ?} listener The listener function.
         * @param {Object=} opt_this The object to use as `this` in `listener`.
         * @return {ol.EventsKey|Array.<ol.EventsKey>} Unique key for the listener. If
         *     called with an array of event types as the first argument, the return
         *     will be an array of keys.
         * @api
         */
        ol.Observable.prototype.once = function(type, listener, opt_this) {
            if (Array.isArray(type)) {
                var len = type.length;
                var keys = new Array(len);
                for (var i = 0; i < len; ++i) {
                    keys[i] = ol.events.listenOnce(this, type[i], listener, opt_this);
                }
                return keys;
            } else {
                return ol.events.listenOnce(
                    this, /** @type {string} */ (type), listener, opt_this);
            }
        };


        /**
         * Unlisten for a certain type of event.
         * @param {string|Array.<string>} type The event type or array of event types.
         * @param {function(?): ?} listener The listener function.
         * @param {Object=} opt_this The object which was used as `this` by the
         * `listener`.
         * @api
         */
        ol.Observable.prototype.un = function(type, listener, opt_this) {
            if (Array.isArray(type)) {
                for (var i = 0, ii = type.length; i < ii; ++i) {
                    ol.events.unlisten(this, type[i], listener, opt_this);
                }
                return;
            } else {
                ol.events.unlisten(this, /** @type {string} */ (type), listener, opt_this);
            }
        };

        ol.uidCounter_ = 0;

        ol.Object = function(opt_values) {
            ol.Observable.call(this);

            // Call ol.getUid to ensure that the order of objects' ids is the same as
            // the order in which they were created.  This also helps to ensure that
            // object properties are always added in the same order, which helps many
            // JavaScript engines generate faster code.
            ol.getUid(this);

            /**
             * @private
             * @type {!Object.<string, *>}
             */
            this.values_ = {};

            if (opt_values !== undefined) {
                this.setProperties(opt_values);
            }
        };
        ol.inherits(ol.Object, ol.Observable);


        /**
         * @private
         * @type {Object.<string, string>}
         */
        ol.Object.changeEventTypeCache_ = {};


        /**
         * @param {string} key Key name.
         * @return {string} Change name.
         */
        ol.Object.getChangeEventType = function(key) {
            return ol.Object.changeEventTypeCache_.hasOwnProperty(key) ?
                ol.Object.changeEventTypeCache_[key] :
                (ol.Object.changeEventTypeCache_[key] = 'change:' + key);
        };


        /**
         * Gets a value.
         * @param {string} key Key name.
         * @return {*} Value.
         * @api
         */
        ol.Object.prototype.get = function(key) {
            var value;
            if (this.values_.hasOwnProperty(key)) {
                value = this.values_[key];
            }
            return value;
        };


        /**
         * Get a list of object property names.
         * @return {Array.<string>} List of property names.
         * @api
         */
        ol.Object.prototype.getKeys = function() {
            return Object.keys(this.values_);
        };


        /**
         * Get an object of all property names and values.
         * @return {Object.<string, *>} Object.
         * @api
         */
        ol.Object.prototype.getProperties = function() {
            return ol.obj.assign({}, this.values_);
        };


        /**
         * @param {string} key Key name.
         * @param {*} oldValue Old value.
         */
        ol.Object.prototype.notify = function(key, oldValue) {
            // FIX ME
            return;
        };


        /**
         * Sets a value.
         * @param {string} key Key name.
         * @param {*} value Value.
         * @param {boolean=} opt_silent Update without triggering an event.
         * @api
         */
        ol.Object.prototype.set = function(key, value, opt_silent) {
            if (opt_silent) {
                this.values_[key] = value;
            } else {
                var oldValue = this.values_[key];
                this.values_[key] = value;
                if (oldValue !== value) {
                    this.notify(key, oldValue);
                }
            }
        };


        /**
         * Sets a collection of key-value pairs.  Note that this changes any existing
         * properties and adds new ones (it does not remove any existing properties).
         * @param {Object.<string, *>} values Values.
         * @param {boolean=} opt_silent Update without triggering an event.
         * @api
         */
        ol.Object.prototype.setProperties = function(values, opt_silent) {
            var key;
            for (key in values) {
                this.set(key, values[key], opt_silent);
            }
        };


        /**
         * Unsets a property.
         * @param {string} key Key name.
         * @param {boolean=} opt_silent Unset without triggering an event.
         * @api
         */
        ol.Object.prototype.unset = function(key, opt_silent) {
            if (key in this.values_) {
                var oldValue = this.values_[key];
                delete this.values_[key];
                if (!opt_silent) {
                    this.notify(key, oldValue);
                }
            }
        };

        ol.Object.Event = function(type, key, oldValue) {
            ol.events.Event.call(this, type);
            this.key = key;
            this.oldValue = oldValue;

        };
        ol.inherits(ol.Object.Event, ol.events.Event);

        ol.functions = {};
        /**
         * Always returns true.
         * @returns {boolean} true.
         */
        ol.functions.TRUE = function() {
            return true;
        };

        /**
         * Always returns false.
         * @returns {boolean} false.
         */
        ol.functions.FALSE = function() {
            return false;
        };

        ol.math = {};

        ol.math.clamp = function(value, min, max) {
            return Math.min(Math.max(value, min), max);
        };

        ol.math.cosh = (function() {
            // Wrapped in a iife, to save the overhead of checking for the native
            // implementation on every invocation.
            var cosh;
            if ('cosh' in Math) {
                // The environment supports the native Math.cosh function, use it…
                cosh = Math.cosh;
            } else {
                // … else, use the reference implementation of MDN:
                cosh = function(x) {
                    var y = Math.exp(x);
                    return (y + 1 / y) / 2;
                };
            }
            return cosh;
        }());


        /**
         * @param {number} x X.
         * @return {number} The smallest power of two greater than or equal to x.
         */
        ol.math.roundUpToPowerOfTwo = function(x) {
            ol.asserts.assert(0 < x, 29); // `x` must be greater than `0`
            return Math.pow(2, Math.ceil(Math.log(x) / Math.LN2));
        };

        ol.math.squaredSegmentDistance = function(x, y, x1, y1, x2, y2) {
            var dx = x2 - x1;
            var dy = y2 - y1;
            if (dx !== 0 || dy !== 0) {
                var t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
                if (t > 1) {
                    x1 = x2;
                    y1 = y2;
                } else if (t > 0) {
                    x1 += dx * t;
                    y1 += dy * t;
                }
            }
            return ol.math.squaredDistance(x, y, x1, y1);
        };

        ol.math.squaredDistance = function(x1, y1, x2, y2) {
            var dx = x2 - x1;
            var dy = y2 - y1;
            return dx * dx + dy * dy;
        };

        ol.math.solveLinearSystem = function(mat) {
            var n = mat.length;

            for (var i = 0; i < n; i++) {
                // Find max in the i-th column (ignoring i - 1 first rows)
                var maxRow = i;
                var maxEl = Math.abs(mat[i][i]);
                for (var r = i + 1; r < n; r++) {
                    var absValue = Math.abs(mat[r][i]);
                    if (absValue > maxEl) {
                        maxEl = absValue;
                        maxRow = r;
                    }
                }

                if (maxEl === 0) {
                    return null; // matrix is singular
                }

                // Swap max row with i-th (current) row
                var tmp = mat[maxRow];
                mat[maxRow] = mat[i];
                mat[i] = tmp;

                // Subtract the i-th row to make all the remaining rows 0 in the i-th column
                for (var j = i + 1; j < n; j++) {
                    var coef = -mat[j][i] / mat[i][i];
                    for (var k = i; k < n + 1; k++) {
                        if (i == k) {
                            mat[j][k] = 0;
                        } else {
                            mat[j][k] += coef * mat[i][k];
                        }
                    }
                }
            }

            // Solve Ax=b for upper triangular matrix A (mat)
            var x = new Array(n);
            for (var l = n - 1; l >= 0; l--) {
                x[l] = mat[l][n] / mat[l][l];
                for (var m = l - 1; m >= 0; m--) {
                    mat[m][n] -= mat[m][l] * x[l];
                }
            }
            return x;
        };

        ol.math.toDegrees = function(angleInRadians) {
            return angleInRadians * 180 / Math.PI;
        };

        ol.math.toRadians = function(angleInDegrees) {
            return angleInDegrees * Math.PI / 180;
        };

        ol.math.modulo = function(a, b) {
            var r = a % b;
            return r * b < 0 ? r + b : r;
        };

        ol.math.lerp = function(a, b, x) {
            return a + x * (b - a);
        };

        ol.ImageState = {
            IDLE: 0,
            LOADING: 1,
            LOADED: 2,
            ERROR: 3
        };

        ol.color = {};

        ol.color.HEX_COLOR_RE_ = /^#(?:[0-9a-f]{3,4}){1,2}$/i;

        ol.color.NAMED_COLOR_RE_ = /^([a-z]*)$/i;

        ol.color.asArray = function(color) {
            if (Array.isArray(color)) {
                return color;
            } else {
                return ol.color.fromString(/** @type {string} */ (color));
            }
        };

        ol.color.asString = function(color) {
            if (typeof color === 'string') {
                return color;
            } else {
                return ol.color.toString(color);
            }
        };

        ol.color.fromNamed = function(color) {
            var el = document.createElement('div');
            el.style.color = color;
            document.body.appendChild(el);
            var rgb = getComputedStyle(el).color;
            document.body.removeChild(el);
            return rgb;
        };

        ol.color.fromString = (
            function() {

                // We maintain a small cache of parsed strings.  To provide cheap LRU-like
                // semantics, whenever the cache grows too large we simply delete an
                // arbitrary 25% of the entries.

                /**
                 * @const
                 * @type {number}
                 */
                var MAX_CACHE_SIZE = 1024;

                /**
                 * @type {Object.<string, ol.Color>}
                 */
                var cache = {};

                /**
                 * @type {number}
                 */
                var cacheSize = 0;

                return (
                    /**
                     * @param {string} s String.
                     * @return {ol.Color} Color.
                     */
                        function(s) {
                        var color;
                        if (cache.hasOwnProperty(s)) {
                            color = cache[s];
                        } else {
                            if (cacheSize >= MAX_CACHE_SIZE) {
                                var i = 0;
                                var key;
                                for (key in cache) {
                                    if ((i++ & 3) === 0) {
                                        delete cache[key];
                                        --cacheSize;
                                    }
                                }
                            }
                            color = ol.color.fromStringInternal_(s);
                            cache[s] = color;
                            ++cacheSize;
                        }
                        return color;
                    });

            })();

        ol.color.fromStringInternal_ = function(s) {
            var r, g, b, a, color, parts;

            if (ol.color.NAMED_COLOR_RE_.exec(s)) {
                s = ol.color.fromNamed(s);
            }

            if (ol.color.HEX_COLOR_RE_.exec(s)) { // hex
                var n = s.length - 1; // number of hex digits
                var d; // number of digits per channel
                if (n <= 4) {
                    d = 1;
                } else {
                    d = 2;
                }
                var hasAlpha = n === 4 || n === 8;
                r = parseInt(s.substr(1 + 0 * d, d), 16);
                g = parseInt(s.substr(1 + 1 * d, d), 16);
                b = parseInt(s.substr(1 + 2 * d, d), 16);
                if (hasAlpha) {
                    a = parseInt(s.substr(1 + 3 * d, d), 16);
                } else {
                    a = 255;
                }
                if (d == 1) {
                    r = (r << 4) + r;
                    g = (g << 4) + g;
                    b = (b << 4) + b;
                    if (hasAlpha) {
                        a = (a << 4) + a;
                    }
                }
                color = [r, g, b, a / 255];
            } else if (s.indexOf('rgba(') == 0) { // rgba()
                parts = s.slice(5, -1).split(',').map(Number);
                color = ol.color.normalize(parts);
            } else if (s.indexOf('rgb(') == 0) { // rgb()
                parts = s.slice(4, -1).split(',').map(Number);
                parts.push(1);
                color = ol.color.normalize(parts);
            } else {
                ol.asserts.assert(false, 14); // Invalid color
            }
            return /** @type {ol.Color} */ (color);
        };


        /**
         * @param {ol.Color} color Color.
         * @param {ol.Color=} opt_color Color.
         * @return {ol.Color} Clamped color.
         */
        ol.color.normalize = function(color, opt_color) {
            var result = opt_color || [];
            result[0] = ol.math.clamp((color[0] + 0.5) | 0, 0, 255);
            result[1] = ol.math.clamp((color[1] + 0.5) | 0, 0, 255);
            result[2] = ol.math.clamp((color[2] + 0.5) | 0, 0, 255);
            result[3] = ol.math.clamp(color[3], 0, 1);
            return result;
        };


        /**
         * @param {ol.Color} color Color.
         * @return {string} String.
         */
        ol.color.toString = function(color) {
            var r = color[0];
            if (r != (r | 0)) {
                r = (r + 0.5) | 0;
            }
            var g = color[1];
            if (g != (g | 0)) {
                g = (g + 0.5) | 0;
            }
            var b = color[2];
            if (b != (b | 0)) {
                b = (b + 0.5) | 0;
            }
            var a = color[3] === undefined ? 1 : color[3];
            return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
        };

        ol.colorlike = {};

        ol.colorlike.asColorLike = function(color) {
            if (ol.colorlike.isColorLike(color)) {
                return /** @type {string|CanvasPattern|CanvasGradient} */ (color);
            } else {
                return ol.color.asString(/** @type {ol.Color} */ (color));
            }
        };

        ol.colorlike.isColorLike = function(color) {
            return (
                typeof color === 'string' ||
                    color instanceof CanvasPattern ||
                    color instanceof CanvasGradient
                );
        };

        ol.css = {};
        ol.css.CLASS_HIDDEN = 'ol-hidden';
        ol.css.CLASS_SELECTABLE = 'ol-selectable';
        ol.css.CLASS_UNSELECTABLE = 'ol-unselectable';
        ol.css.CLASS_UNSUPPORTED = 'ol-unsupported';
        ol.css.CLASS_CONTROL = 'ol-control';
        ol.css.getFontFamilies = (function() {
            var style;
            var cache = {};
            return function(font) {
                if (!style) {
                    style = document.createElement('div').style;
                }
                if (!(font in cache)) {
                    style.font = font;
                    var family = style.fontFamily;
                    style.font = '';
                    if (!family) {
                        return null;
                    }
                    cache[font] = family.split(/,\s?/);
                }
                return cache[font];
            };
        })();

        ol.dom = {};

        /**
         * Create an html canvas element and returns its 2d context.
         * @param {number=} opt_width Canvas width.
         * @param {number=} opt_height Canvas height.
         * @return {CanvasRenderingContext2D} The context.
         */
        ol.dom.createCanvasContext2D = function(opt_width, opt_height) {
            //var canvas = document.createElement('CANVAS');
            var canvas;
            if (opt_width && opt_height) {
                canvas = new OffscreenCanvas(opt_width, opt_height);
            }
            else{
                canvas = new OffscreenCanvas(1, 1);
            }
            return canvas.getContext('2d');
        };


        /**
         * Get the current computed width for the given element including margin,
         * padding and border.
         * Equivalent to jQuery's `$(el).outerWidth(true)`.
         * @param {!Element} element Element.
         * @return {number} The width.
         */
        ol.dom.outerWidth = function(element) {
            var width = element.offsetWidth;
            var style = getComputedStyle(element);
            width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);

            return width;
        };


        /**
         * Get the current computed height for the given element including margin,
         * padding and border.
         * Equivalent to jQuery's `$(el).outerHeight(true)`.
         * @param {!Element} element Element.
         * @return {number} The height.
         */
        ol.dom.outerHeight = function(element) {
            var height = element.offsetHeight;
            var style = getComputedStyle(element);
            height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);

            return height;
        };

        /**
         * @param {Node} newNode Node to replace old node
         * @param {Node} oldNode The node to be replaced
         */
        ol.dom.replaceNode = function(newNode, oldNode) {
            var parent = oldNode.parentNode;
            if (parent) {
                parent.replaceChild(newNode, oldNode);
            }
        };

        /**
         * @param {Node} node The node to remove.
         * @returns {Node} The node that was removed or null.
         */
        ol.dom.removeNode = function(node) {
            return node && node.parentNode ? node.parentNode.removeChild(node) : null;
        };

        /**
         * @param {Node} node The node to remove the children from.
         */
        ol.dom.removeChildren = function(node) {
            while (node.lastChild) {
                node.removeChild(node.lastChild);
            }
        };

        ol.extent = {};

        ol.extent.Corner = {
            BOTTOM_LEFT: 'bottom-left',
            BOTTOM_RIGHT: 'bottom-right',
            TOP_LEFT: 'top-left',
            TOP_RIGHT: 'top-right'
        };

        /**
         * Relationship to an extent.
         * @enum {number}
         */
        ol.extent.Relationship = {
            UNKNOWN: 0,
            INTERSECTING: 1,
            ABOVE: 2,
            RIGHT: 4,
            BELOW: 8,
            LEFT: 16
        };

        /**
         * Build an extent that includes all given coordinates.
         *
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @return {ol.Extent} Bounding extent.
         * @api
         */
        ol.extent.boundingExtent = function(coordinates) {
            var extent = ol.extent.createEmpty();
            for (var i = 0, ii = coordinates.length; i < ii; ++i) {
                ol.extent.extendCoordinate(extent, coordinates[i]);
            }
            return extent;
        };

        ol.extent.boundingExtentXYs_ = function(xs, ys, opt_extent) {
            var minX = Math.min.apply(null, xs);
            var minY = Math.min.apply(null, ys);
            var maxX = Math.max.apply(null, xs);
            var maxY = Math.max.apply(null, ys);
            return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
        };

        ol.extent.buffer = function(extent, value, opt_extent) {
            if (opt_extent) {
                opt_extent[0] = extent[0] - value;
                opt_extent[1] = extent[1] - value;
                opt_extent[2] = extent[2] + value;
                opt_extent[3] = extent[3] + value;
                return opt_extent;
            } else {
                return [
                    extent[0] - value,
                    extent[1] - value,
                    extent[2] + value,
                    extent[3] + value
                ];
            }
        };

        ol.extent.clone = function(extent, opt_extent) {
            if (opt_extent) {
                opt_extent[0] = extent[0];
                opt_extent[1] = extent[1];
                opt_extent[2] = extent[2];
                opt_extent[3] = extent[3];
                return opt_extent;
            } else {
                return extent.slice();
            }
        };

        ol.extent.closestSquaredDistanceXY = function(extent, x, y) {
            var dx, dy;
            if (x < extent[0]) {
                dx = extent[0] - x;
            } else if (extent[2] < x) {
                dx = x - extent[2];
            } else {
                dx = 0;
            }
            if (y < extent[1]) {
                dy = extent[1] - y;
            } else if (extent[3] < y) {
                dy = y - extent[3];
            } else {
                dy = 0;
            }
            return dx * dx + dy * dy;
        };


        /**
         * Check if the passed coordinate is contained or on the edge of the extent.
         *
         * @param {ol.Extent} extent Extent.
         * @param {ol.Coordinate} coordinate Coordinate.
         * @return {boolean} The coordinate is contained in the extent.
         * @api
         */
        ol.extent.containsCoordinate = function(extent, coordinate) {
            return ol.extent.containsXY(extent, coordinate[0], coordinate[1]);
        };


        /**
         * Check if one extent contains another.
         *
         * An extent is deemed contained if it lies completely within the other extent,
         * including if they share one or more edges.
         *
         * @param {ol.Extent} extent1 Extent 1.
         * @param {ol.Extent} extent2 Extent 2.
         * @return {boolean} The second extent is contained by or on the edge of the
         *     first.
         * @api
         */
        ol.extent.containsExtent = function(extent1, extent2) {
            return extent1[0] <= extent2[0] && extent2[2] <= extent1[2] &&
                extent1[1] <= extent2[1] && extent2[3] <= extent1[3];
        };


        /**
         * Check if the passed coordinate is contained or on the edge of the extent.
         *
         * @param {ol.Extent} extent Extent.
         * @param {number} x X coordinate.
         * @param {number} y Y coordinate.
         * @return {boolean} The x, y values are contained in the extent.
         * @api
         */
        ol.extent.containsXY = function(extent, x, y) {
            return extent[0] <= x && x <= extent[2] && extent[1] <= y && y <= extent[3];
        };


        /**
         * Get the relationship between a coordinate and extent.
         * @param {ol.Extent} extent The extent.
         * @param {ol.Coordinate} coordinate The coordinate.
         * @return {number} The relationship (bitwise compare with
         *     ol.extent.Relationship).
         */
        ol.extent.coordinateRelationship = function(extent, coordinate) {
            var minX = extent[0];
            var minY = extent[1];
            var maxX = extent[2];
            var maxY = extent[3];
            var x = coordinate[0];
            var y = coordinate[1];
            var relationship = ol.extent.Relationship.UNKNOWN;
            if (x < minX) {
                relationship = relationship | ol.extent.Relationship.LEFT;
            } else if (x > maxX) {
                relationship = relationship | ol.extent.Relationship.RIGHT;
            }
            if (y < minY) {
                relationship = relationship | ol.extent.Relationship.BELOW;
            } else if (y > maxY) {
                relationship = relationship | ol.extent.Relationship.ABOVE;
            }
            if (relationship === ol.extent.Relationship.UNKNOWN) {
                relationship = ol.extent.Relationship.INTERSECTING;
            }
            return relationship;
        };


        /**
         * Create an empty extent.
         * @return {ol.Extent} Empty extent.
         * @api
         */
        ol.extent.createEmpty = function() {
            return [Infinity, Infinity, -Infinity, -Infinity];
        };


        /**
         * Create a new extent or update the provided extent.
         * @param {number} minX Minimum X.
         * @param {number} minY Minimum Y.
         * @param {number} maxX Maximum X.
         * @param {number} maxY Maximum Y.
         * @param {ol.Extent=} opt_extent Destination extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.createOrUpdate = function(minX, minY, maxX, maxY, opt_extent) {
            if (opt_extent) {
                opt_extent[0] = minX;
                opt_extent[1] = minY;
                opt_extent[2] = maxX;
                opt_extent[3] = maxY;
                return opt_extent;
            } else {
                return [minX, minY, maxX, maxY];
            }
        };


        /**
         * Create a new empty extent or make the provided one empty.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.createOrUpdateEmpty = function(opt_extent) {
            return ol.extent.createOrUpdate(
                Infinity, Infinity, -Infinity, -Infinity, opt_extent);
        };


        /**
         * @param {ol.Coordinate} coordinate Coordinate.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.createOrUpdateFromCoordinate = function(coordinate, opt_extent) {
            var x = coordinate[0];
            var y = coordinate[1];
            return ol.extent.createOrUpdate(x, y, x, y, opt_extent);
        };


        /**
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.createOrUpdateFromCoordinates = function(coordinates, opt_extent) {
            var extent = ol.extent.createOrUpdateEmpty(opt_extent);
            return ol.extent.extendCoordinates(extent, coordinates);
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.createOrUpdateFromFlatCoordinates = function(flatCoordinates, offset, end, stride, opt_extent) {
            var extent = ol.extent.createOrUpdateEmpty(opt_extent);
            return ol.extent.extendFlatCoordinates(
                extent, flatCoordinates, offset, end, stride);
        };


        /**
         * @param {Array.<Array.<ol.Coordinate>>} rings Rings.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.createOrUpdateFromRings = function(rings, opt_extent) {
            var extent = ol.extent.createOrUpdateEmpty(opt_extent);
            return ol.extent.extendRings(extent, rings);
        };


        /**
         * Determine if two extents are equivalent.
         * @param {ol.Extent} extent1 Extent 1.
         * @param {ol.Extent} extent2 Extent 2.
         * @return {boolean} The two extents are equivalent.
         * @api
         */
        ol.extent.equals = function(extent1, extent2) {
            return extent1[0] == extent2[0] && extent1[2] == extent2[2] &&
                extent1[1] == extent2[1] && extent1[3] == extent2[3];
        };


        /**
         * Modify an extent to include another extent.
         * @param {ol.Extent} extent1 The extent to be modified.
         * @param {ol.Extent} extent2 The extent that will be included in the first.
         * @return {ol.Extent} A reference to the first (extended) extent.
         * @api
         */
        ol.extent.extend = function(extent1, extent2) {
            if (extent2[0] < extent1[0]) {
                extent1[0] = extent2[0];
            }
            if (extent2[2] > extent1[2]) {
                extent1[2] = extent2[2];
            }
            if (extent2[1] < extent1[1]) {
                extent1[1] = extent2[1];
            }
            if (extent2[3] > extent1[3]) {
                extent1[3] = extent2[3];
            }
            return extent1;
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {ol.Coordinate} coordinate Coordinate.
         */
        ol.extent.extendCoordinate = function(extent, coordinate) {
            if (coordinate[0] < extent[0]) {
                extent[0] = coordinate[0];
            }
            if (coordinate[0] > extent[2]) {
                extent[2] = coordinate[0];
            }
            if (coordinate[1] < extent[1]) {
                extent[1] = coordinate[1];
            }
            if (coordinate[1] > extent[3]) {
                extent[3] = coordinate[1];
            }
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @return {ol.Extent} Extent.
         */
        ol.extent.extendCoordinates = function(extent, coordinates) {
            var i, ii;
            for (i = 0, ii = coordinates.length; i < ii; ++i) {
                ol.extent.extendCoordinate(extent, coordinates[i]);
            }
            return extent;
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @return {ol.Extent} Extent.
         */
        ol.extent.extendFlatCoordinates = function(extent, flatCoordinates, offset, end, stride) {
            for (; offset < end; offset += stride) {
                ol.extent.extendXY(
                    extent, flatCoordinates[offset], flatCoordinates[offset + 1]);
            }
            return extent;
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {Array.<Array.<ol.Coordinate>>} rings Rings.
         * @return {ol.Extent} Extent.
         */
        ol.extent.extendRings = function(extent, rings) {
            var i, ii;
            for (i = 0, ii = rings.length; i < ii; ++i) {
                ol.extent.extendCoordinates(extent, rings[i]);
            }
            return extent;
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {number} x X.
         * @param {number} y Y.
         */
        ol.extent.extendXY = function(extent, x, y) {
            extent[0] = Math.min(extent[0], x);
            extent[1] = Math.min(extent[1], y);
            extent[2] = Math.max(extent[2], x);
            extent[3] = Math.max(extent[3], y);
        };


        /**
         * This function calls `callback` for each corner of the extent. If the
         * callback returns a truthy value the function returns that value
         * immediately. Otherwise the function returns `false`.
         * @param {ol.Extent} extent Extent.
         * @param {function(this:T, ol.Coordinate): S} callback Callback.
         * @param {T=} opt_this Value to use as `this` when executing `callback`.
         * @return {S|boolean} Value.
         * @template S, T
         */
        ol.extent.forEachCorner = function(extent, callback, opt_this) {
            var val;
            val = callback.call(opt_this, ol.extent.getBottomLeft(extent));
            if (val) {
                return val;
            }
            val = callback.call(opt_this, ol.extent.getBottomRight(extent));
            if (val) {
                return val;
            }
            val = callback.call(opt_this, ol.extent.getTopRight(extent));
            if (val) {
                return val;
            }
            val = callback.call(opt_this, ol.extent.getTopLeft(extent));
            if (val) {
                return val;
            }
            return false;
        };


        /**
         * Get the size of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {number} Area.
         * @api
         */
        ol.extent.getArea = function(extent) {
            var area = 0;
            if (!ol.extent.isEmpty(extent)) {
                area = ol.extent.getWidth(extent) * ol.extent.getHeight(extent);
            }
            return area;
        };


        /**
         * Get the bottom left coordinate of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {ol.Coordinate} Bottom left coordinate.
         * @api
         */
        ol.extent.getBottomLeft = function(extent) {
            return [extent[0], extent[1]];
        };


        /**
         * Get the bottom right coordinate of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {ol.Coordinate} Bottom right coordinate.
         * @api
         */
        ol.extent.getBottomRight = function(extent) {
            return [extent[2], extent[1]];
        };


        /**
         * Get the center coordinate of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {ol.Coordinate} Center.
         * @api
         */
        ol.extent.getCenter = function(extent) {
            return [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
        };


        /**
         * Get a corner coordinate of an extent.
         * @param {ol.Extent} extent Extent.
         * @param {ol.extent.Corner} corner Corner.
         * @return {ol.Coordinate} Corner coordinate.
         */
        ol.extent.getCorner = function(extent, corner) {
            var coordinate;
            if (corner === ol.extent.Corner.BOTTOM_LEFT) {
                coordinate = ol.extent.getBottomLeft(extent);
            } else if (corner === ol.extent.Corner.BOTTOM_RIGHT) {
                coordinate = ol.extent.getBottomRight(extent);
            } else if (corner === ol.extent.Corner.TOP_LEFT) {
                coordinate = ol.extent.getTopLeft(extent);
            } else if (corner === ol.extent.Corner.TOP_RIGHT) {
                coordinate = ol.extent.getTopRight(extent);
            } else {
                ol.asserts.assert(false, 13); // Invalid corner
            }
            return /** @type {!ol.Coordinate} */ (coordinate);
        };


        /**
         * @param {ol.Extent} extent1 Extent 1.
         * @param {ol.Extent} extent2 Extent 2.
         * @return {number} Enlarged area.
         */
        ol.extent.getEnlargedArea = function(extent1, extent2) {
            var minX = Math.min(extent1[0], extent2[0]);
            var minY = Math.min(extent1[1], extent2[1]);
            var maxX = Math.max(extent1[2], extent2[2]);
            var maxY = Math.max(extent1[3], extent2[3]);
            return (maxX - minX) * (maxY - minY);
        };


        /**
         * @param {ol.Coordinate} center Center.
         * @param {number} resolution Resolution.
         * @param {number} rotation Rotation.
         * @param {ol.Size} size Size.
         * @param {ol.Extent=} opt_extent Destination extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.getForViewAndSize = function(center, resolution, rotation, size, opt_extent) {
            var dx = resolution * size[0] / 2;
            var dy = resolution * size[1] / 2;
            var cosRotation = Math.cos(rotation);
            var sinRotation = Math.sin(rotation);
            var xCos = dx * cosRotation;
            var xSin = dx * sinRotation;
            var yCos = dy * cosRotation;
            var ySin = dy * sinRotation;
            var x = center[0];
            var y = center[1];
            var x0 = x - xCos + ySin;
            var x1 = x - xCos - ySin;
            var x2 = x + xCos - ySin;
            var x3 = x + xCos + ySin;
            var y0 = y - xSin - yCos;
            var y1 = y - xSin + yCos;
            var y2 = y + xSin + yCos;
            var y3 = y + xSin - yCos;
            return ol.extent.createOrUpdate(
                Math.min(x0, x1, x2, x3), Math.min(y0, y1, y2, y3),
                Math.max(x0, x1, x2, x3), Math.max(y0, y1, y2, y3),
                opt_extent);
        };


        /**
         * Get the height of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {number} Height.
         * @api
         */
        ol.extent.getHeight = function(extent) {
            return extent[3] - extent[1];
        };


        /**
         * @param {ol.Extent} extent1 Extent 1.
         * @param {ol.Extent} extent2 Extent 2.
         * @return {number} Intersection area.
         */
        ol.extent.getIntersectionArea = function(extent1, extent2) {
            var intersection = ol.extent.getIntersection(extent1, extent2);
            return ol.extent.getArea(intersection);
        };


        /**
         * Get the intersection of two extents.
         * @param {ol.Extent} extent1 Extent 1.
         * @param {ol.Extent} extent2 Extent 2.
         * @param {ol.Extent=} opt_extent Optional extent to populate with intersection.
         * @return {ol.Extent} Intersecting extent.
         * @api
         */
        ol.extent.getIntersection = function(extent1, extent2, opt_extent) {
            var intersection = opt_extent ? opt_extent : ol.extent.createEmpty();
            if (ol.extent.intersects(extent1, extent2)) {
                if (extent1[0] > extent2[0]) {
                    intersection[0] = extent1[0];
                } else {
                    intersection[0] = extent2[0];
                }
                if (extent1[1] > extent2[1]) {
                    intersection[1] = extent1[1];
                } else {
                    intersection[1] = extent2[1];
                }
                if (extent1[2] < extent2[2]) {
                    intersection[2] = extent1[2];
                } else {
                    intersection[2] = extent2[2];
                }
                if (extent1[3] < extent2[3]) {
                    intersection[3] = extent1[3];
                } else {
                    intersection[3] = extent2[3];
                }
            }
            return intersection;
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @return {number} Margin.
         */
        ol.extent.getMargin = function(extent) {
            return ol.extent.getWidth(extent) + ol.extent.getHeight(extent);
        };


        /**
         * Get the size (width, height) of an extent.
         * @param {ol.Extent} extent The extent.
         * @return {ol.Size} The extent size.
         * @api
         */
        ol.extent.getSize = function(extent) {
            return [extent[2] - extent[0], extent[3] - extent[1]];
        };


        /**
         * Get the top left coordinate of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {ol.Coordinate} Top left coordinate.
         * @api
         */
        ol.extent.getTopLeft = function(extent) {
            return [extent[0], extent[3]];
        };


        /**
         * Get the top right coordinate of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {ol.Coordinate} Top right coordinate.
         * @api
         */
        ol.extent.getTopRight = function(extent) {
            return [extent[2], extent[3]];
        };


        /**
         * Get the width of an extent.
         * @param {ol.Extent} extent Extent.
         * @return {number} Width.
         * @api
         */
        ol.extent.getWidth = function(extent) {
            return extent[2] - extent[0];
        };


        /**
         * Determine if one extent intersects another.
         * @param {ol.Extent} extent1 Extent 1.
         * @param {ol.Extent} extent2 Extent.
         * @return {boolean} The two extents intersect.
         * @api
         */
        ol.extent.intersects = function(extent1, extent2) {
            return extent1[0] <= extent2[2] &&
                extent1[2] >= extent2[0] &&
                extent1[1] <= extent2[3] &&
                extent1[3] >= extent2[1];
        };


        /**
         * Determine if an extent is empty.
         * @param {ol.Extent} extent Extent.
         * @return {boolean} Is empty.
         * @api
         */
        ol.extent.isEmpty = function(extent) {
            return extent[2] < extent[0] || extent[3] < extent[1];
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} Extent.
         */
        ol.extent.returnOrUpdate = function(extent, opt_extent) {
            if (opt_extent) {
                opt_extent[0] = extent[0];
                opt_extent[1] = extent[1];
                opt_extent[2] = extent[2];
                opt_extent[3] = extent[3];
                return opt_extent;
            } else {
                return extent;
            }
        };


        /**
         * @param {ol.Extent} extent Extent.
         * @param {number} value Value.
         */
        ol.extent.scaleFromCenter = function(extent, value) {
            var deltaX = ((extent[2] - extent[0]) / 2) * (value - 1);
            var deltaY = ((extent[3] - extent[1]) / 2) * (value - 1);
            extent[0] -= deltaX;
            extent[2] += deltaX;
            extent[1] -= deltaY;
            extent[3] += deltaY;
        };


        /**
         * Determine if the segment between two coordinates intersects (crosses,
         * touches, or is contained by) the provided extent.
         * @param {ol.Extent} extent The extent.
         * @param {ol.Coordinate} start Segment start coordinate.
         * @param {ol.Coordinate} end Segment end coordinate.
         * @return {boolean} The segment intersects the extent.
         */
        ol.extent.intersectsSegment = function(extent, start, end) {
            var intersects = false;
            var startRel = ol.extent.coordinateRelationship(extent, start);
            var endRel = ol.extent.coordinateRelationship(extent, end);
            if (startRel === ol.extent.Relationship.INTERSECTING ||
                endRel === ol.extent.Relationship.INTERSECTING) {
                intersects = true;
            } else {
                var minX = extent[0];
                var minY = extent[1];
                var maxX = extent[2];
                var maxY = extent[3];
                var startX = start[0];
                var startY = start[1];
                var endX = end[0];
                var endY = end[1];
                var slope = (endY - startY) / (endX - startX);
                var x, y;
                if (!!(endRel & ol.extent.Relationship.ABOVE) &&
                    !(startRel & ol.extent.Relationship.ABOVE)) {
                    // potentially intersects top
                    x = endX - ((endY - maxY) / slope);
                    intersects = x >= minX && x <= maxX;
                }
                if (!intersects && !!(endRel & ol.extent.Relationship.RIGHT) &&
                    !(startRel & ol.extent.Relationship.RIGHT)) {
                    // potentially intersects right
                    y = endY - ((endX - maxX) * slope);
                    intersects = y >= minY && y <= maxY;
                }
                if (!intersects && !!(endRel & ol.extent.Relationship.BELOW) &&
                    !(startRel & ol.extent.Relationship.BELOW)) {
                    // potentially intersects bottom
                    x = endX - ((endY - minY) / slope);
                    intersects = x >= minX && x <= maxX;
                }
                if (!intersects && !!(endRel & ol.extent.Relationship.LEFT) &&
                    !(startRel & ol.extent.Relationship.LEFT)) {
                    // potentially intersects left
                    y = endY - ((endX - minX) * slope);
                    intersects = y >= minY && y <= maxY;
                }

            }
            return intersects;
        };


        /**
         * Apply a transform function to the extent.
         * @param {ol.Extent} extent Extent.
         * @param {ol.TransformFunction} transformFn Transform function.  Called with
         * [minX, minY, maxX, maxY] extent coordinates.
         * @param {ol.Extent=} opt_extent Destination extent.
         * @return {ol.Extent} Extent.
         * @api
         */
        ol.extent.applyTransform = function(extent, transformFn, opt_extent) {
            var coordinates = [
                extent[0], extent[1],
                extent[0], extent[3],
                extent[2], extent[1],
                extent[2], extent[3]
            ];
            transformFn(coordinates, coordinates, 2);
            var xs = [coordinates[0], coordinates[2], coordinates[4], coordinates[6]];
            var ys = [coordinates[1], coordinates[3], coordinates[5], coordinates[7]];
            return ol.extent.boundingExtentXYs_(xs, ys, opt_extent);
        };

        ol.obj = {};

        /**
         * Polyfill for Object.assign().  Assigns enumerable and own properties from
         * one or more source objects to a target object.
         *
         * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
         * @param {!Object} target The target object.
         * @param {...Object} var_sources The source object(s).
         * @return {!Object} The modified target object.
         */
        ol.obj.assign = (typeof Object.assign === 'function') ? Object.assign : function(target, var_sources) {
            if (target === undefined || target === null) {
                throw new TypeError('Cannot convert undefined or null to object');
            }

            var output = Object(target);
            for (var i = 1, ii = arguments.length; i < ii; ++i) {
                var source = arguments[i];
                if (source !== undefined && source !== null) {
                    for (var key in source) {
                        if (source.hasOwnProperty(key)) {
                            output[key] = source[key];
                        }
                    }
                }
            }
            return output;
        };


        /**
         * Removes all properties from an object.
         * @param {Object} object The object to clear.
         */
        ol.obj.clear = function(object) {
            for (var property in object) {
                delete object[property];
            }
        };


        /**
         * Get an array of property values from an object.
         * @param {Object<K,V>} object The object from which to get the values.
         * @return {!Array<V>} The property values.
         * @template K,V
         */
        ol.obj.getValues = function(object) {
            var values = [];
            for (var property in object) {
                values.push(object[property]);
            }
            return values;
        };


        /**
         * Determine if an object has any properties.
         * @param {Object} object The object to check.
         * @return {boolean} The object is empty.
         */
        ol.obj.isEmpty = function(object) {
            var property;
            for (property in object) {
                return false;
            }
            return !property;
        };

        ol.transform = {};

        /**
         * @private
         * @type {ol.Transform}
         */
        ol.transform.tmp_ = new Array(6);


        /**
         * Create an identity transform.
         * @return {!ol.Transform} Identity transform.
         */
        ol.transform.create = function() {
            return [1, 0, 0, 1, 0, 0];
        };


        /**
         * Resets the given transform to an identity transform.
         * @param {!ol.Transform} transform Transform.
         * @return {!ol.Transform} Transform.
         */
        ol.transform.reset = function(transform) {
            return ol.transform.set(transform, 1, 0, 0, 1, 0, 0);
        };


        /**
         * Multiply the underlying matrices of two transforms and return the result in
         * the first transform.
         * @param {!ol.Transform} transform1 Transform parameters of matrix 1.
         * @param {!ol.Transform} transform2 Transform parameters of matrix 2.
         * @return {!ol.Transform} transform1 multiplied with transform2.
         */
        ol.transform.multiply = function(transform1, transform2) {
            var a1 = transform1[0];
            var b1 = transform1[1];
            var c1 = transform1[2];
            var d1 = transform1[3];
            var e1 = transform1[4];
            var f1 = transform1[5];
            var a2 = transform2[0];
            var b2 = transform2[1];
            var c2 = transform2[2];
            var d2 = transform2[3];
            var e2 = transform2[4];
            var f2 = transform2[5];

            transform1[0] = a1 * a2 + c1 * b2;
            transform1[1] = b1 * a2 + d1 * b2;
            transform1[2] = a1 * c2 + c1 * d2;
            transform1[3] = b1 * c2 + d1 * d2;
            transform1[4] = a1 * e2 + c1 * f2 + e1;
            transform1[5] = b1 * e2 + d1 * f2 + f1;

            return transform1;
        };

        /**
         * Set the transform components a-f on a given transform.
         * @param {!ol.Transform} transform Transform.
         * @param {number} a The a component of the transform.
         * @param {number} b The b component of the transform.
         * @param {number} c The c component of the transform.
         * @param {number} d The d component of the transform.
         * @param {number} e The e component of the transform.
         * @param {number} f The f component of the transform.
         * @return {!ol.Transform} Matrix with transform applied.
         */
        ol.transform.set = function(transform, a, b, c, d, e, f) {
            transform[0] = a;
            transform[1] = b;
            transform[2] = c;
            transform[3] = d;
            transform[4] = e;
            transform[5] = f;
            return transform;
        };


        /**
         * Set transform on one matrix from another matrix.
         * @param {!ol.Transform} transform1 Matrix to set transform to.
         * @param {!ol.Transform} transform2 Matrix to set transform from.
         * @return {!ol.Transform} transform1 with transform from transform2 applied.
         */
        ol.transform.setFromArray = function(transform1, transform2) {
            transform1[0] = transform2[0];
            transform1[1] = transform2[1];
            transform1[2] = transform2[2];
            transform1[3] = transform2[3];
            transform1[4] = transform2[4];
            transform1[5] = transform2[5];
            return transform1;
        };


        /**
         * Transforms the given coordinate with the given transform returning the
         * resulting, transformed coordinate. The coordinate will be modified in-place.
         *
         * @param {ol.Transform} transform The transformation.
         * @param {ol.Coordinate|ol.Pixel} coordinate The coordinate to transform.
         * @return {ol.Coordinate|ol.Pixel} return coordinate so that operations can be
         *     chained together.
         */
        ol.transform.apply = function(transform, coordinate) {
            var x = coordinate[0], y = coordinate[1];
            coordinate[0] = transform[0] * x + transform[2] * y + transform[4];
            coordinate[1] = transform[1] * x + transform[3] * y + transform[5];
            return coordinate;
        };


        /**
         * Applies rotation to the given transform.
         * @param {!ol.Transform} transform Transform.
         * @param {number} angle Angle in radians.
         * @return {!ol.Transform} The rotated transform.
         */
        ol.transform.rotate = function(transform, angle) {
            var cos = Math.cos(angle);
            var sin = Math.sin(angle);
            return ol.transform.multiply(transform,
                ol.transform.set(ol.transform.tmp_, cos, sin, -sin, cos, 0, 0));
        };


        /**
         * Applies scale to a given transform.
         * @param {!ol.Transform} transform Transform.
         * @param {number} x Scale factor x.
         * @param {number} y Scale factor y.
         * @return {!ol.Transform} The scaled transform.
         */
        ol.transform.scale = function(transform, x, y) {
            return ol.transform.multiply(transform,
                ol.transform.set(ol.transform.tmp_, x, 0, 0, y, 0, 0));
        };


        /**
         * Applies translation to the given transform.
         * @param {!ol.Transform} transform Transform.
         * @param {number} dx Translation x.
         * @param {number} dy Translation y.
         * @return {!ol.Transform} The translated transform.
         */
        ol.transform.translate = function(transform, dx, dy) {
            return ol.transform.multiply(transform,
                ol.transform.set(ol.transform.tmp_, 1, 0, 0, 1, dx, dy));
        };


        /**
         * Creates a composite transform given an initial translation, scale, rotation, and
         * final translation (in that order only, not commutative).
         * @param {!ol.Transform} transform The transform (will be modified in place).
         * @param {number} dx1 Initial translation x.
         * @param {number} dy1 Initial translation y.
         * @param {number} sx Scale factor x.
         * @param {number} sy Scale factor y.
         * @param {number} angle Rotation (in counter-clockwise radians).
         * @param {number} dx2 Final translation x.
         * @param {number} dy2 Final translation y.
         * @return {!ol.Transform} The composite transform.
         */
        ol.transform.compose = function(transform, dx1, dy1, sx, sy, angle, dx2, dy2) {
            var sin = Math.sin(angle);
            var cos = Math.cos(angle);
            transform[0] = sx * cos;
            transform[1] = sy * sin;
            transform[2] = -sx * sin;
            transform[3] = sy * cos;
            transform[4] = dx2 * sx * cos - dy2 * sx * sin + dx1;
            transform[5] = dx2 * sy * sin + dy2 * sy * cos + dy1;
            return transform;
        };


        /**
         * Invert the given transform.
         * @param {!ol.Transform} transform Transform.
         * @return {!ol.Transform} Inverse of the transform.
         */
        ol.transform.invert = function(transform) {
            var det = ol.transform.determinant(transform);
            //ol.asserts.assert(det !== 0, 32); // Transformation matrix cannot be inverted

            var a = transform[0];
            var b = transform[1];
            var c = transform[2];
            var d = transform[3];
            var e = transform[4];
            var f = transform[5];

            transform[0] = d / det;
            transform[1] = -b / det;
            transform[2] = -c / det;
            transform[3] = a / det;
            transform[4] = (c * f - d * e) / det;
            transform[5] = -(a * f - b * e) / det;

            return transform;
        };


        /**
         * Returns the determinant of the given matrix.
         * @param {!ol.Transform} mat Matrix.
         * @return {number} Determinant.
         */
        ol.transform.determinant = function(mat) {
            return mat[0] * mat[3] - mat[1] * mat[2];
        };

        ol.geom = {};
        ol.geom.flat = {};
        ol.geom.flat.center = {};
        ol.geom.flat.reverse = {};
        ol.geom.flat.orient = {};


        ol.geom.flat.transform = {};
        ol.geom.flat.transform.transform2D = function(flatCoordinates, offset, end, stride, transform, opt_dest) {
            var dest = opt_dest ? opt_dest : [];
            var i = 0;
            var j;
            for (j = offset; j < end; j += stride) {
                var x = flatCoordinates[j];
                var y = flatCoordinates[j + 1];
                dest[i++] = transform[0] * x + transform[2] * y + transform[4];
                dest[i++] = transform[1] * x + transform[3] * y + transform[5];
            }
            if (opt_dest && dest.length != i) {
                dest.length = i;
            }
            return dest;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {number} angle Angle.
         * @param {Array.<number>} anchor Rotation anchor point.
         * @param {Array.<number>=} opt_dest Destination.
         * @return {Array.<number>} Transformed coordinates.
         */
        ol.geom.flat.transform.rotate = function(flatCoordinates, offset, end, stride, angle, anchor, opt_dest) {
            var dest = opt_dest ? opt_dest : [];
            var cos = Math.cos(angle);
            var sin = Math.sin(angle);
            var anchorX = anchor[0];
            var anchorY = anchor[1];
            var i = 0;
            for (var j = offset; j < end; j += stride) {
                var deltaX = flatCoordinates[j] - anchorX;
                var deltaY = flatCoordinates[j + 1] - anchorY;
                dest[i++] = anchorX + deltaX * cos - deltaY * sin;
                dest[i++] = anchorY + deltaX * sin + deltaY * cos;
                for (var k = j + 2; k < j + stride; ++k) {
                    dest[i++] = flatCoordinates[k];
                }
            }
            if (opt_dest && dest.length != i) {
                dest.length = i;
            }
            return dest;
        };


        /**
         * Scale the coordinates.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {number} sx Scale factor in the x-direction.
         * @param {number} sy Scale factor in the y-direction.
         * @param {Array.<number>} anchor Scale anchor point.
         * @param {Array.<number>=} opt_dest Destination.
         * @return {Array.<number>} Transformed coordinates.
         */
        ol.geom.flat.transform.scale = function(flatCoordinates, offset, end, stride, sx, sy, anchor, opt_dest) {
            var dest = opt_dest ? opt_dest : [];
            var anchorX = anchor[0];
            var anchorY = anchor[1];
            var i = 0;
            for (var j = offset; j < end; j += stride) {
                var deltaX = flatCoordinates[j] - anchorX;
                var deltaY = flatCoordinates[j + 1] - anchorY;
                dest[i++] = anchorX + sx * deltaX;
                dest[i++] = anchorY + sy * deltaY;
                for (var k = j + 2; k < j + stride; ++k) {
                    dest[i++] = flatCoordinates[k];
                }
            }
            if (opt_dest && dest.length != i) {
                dest.length = i;
            }
            return dest;
        };

        /**
         * The coordinate layout for geometries, indicating whether a 3rd or 4th z ('Z')
         * or measure ('M') coordinate is available. Supported values are `'XY'`,
         * `'XYZ'`, `'XYM'`, `'XYZM'`.
         * @enum {string}
         */
        ol.geom.GeometryLayout = {
            XY: 'XY',
            XYZ: 'XYZ',
            XYM: 'XYM',
            XYZM: 'XYZM'
        };

        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         */
        ol.geom.flat.reverse.coordinates = function(flatCoordinates, offset, end, stride) {
            while (offset < end - stride) {
                var i;
                for (i = 0; i < stride; ++i) {
                    var tmp = flatCoordinates[offset + i];
                    flatCoordinates[offset + i] = flatCoordinates[end - stride + i];
                    flatCoordinates[end - stride + i] = tmp;
                }
                offset += stride;
                end -= stride;
            }
        };

        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @return {boolean} Is clockwise.
         */
        ol.geom.flat.orient.linearRingIsClockwise = function(flatCoordinates, offset, end, stride) {
            // http://tinyurl.com/clockwise-method
            // https://github.com/OSGeo/gdal/blob/trunk/gdal/ogr/ogrlinearring.cpp
            var edge = 0;
            var x1 = flatCoordinates[end - stride];
            var y1 = flatCoordinates[end - stride + 1];
            for (; offset < end; offset += stride) {
                var x2 = flatCoordinates[offset];
                var y2 = flatCoordinates[offset + 1];
                edge += (x2 - x1) * (y2 + y1);
                x1 = x2;
                y1 = y2;
            }
            return edge > 0;
        };


        /**
         * Determines if linear rings are oriented.  By default, left-hand orientation
         * is tested (first ring must be clockwise, remaining rings counter-clockwise).
         * To test for right-hand orientation, use the `opt_right` argument.
         *
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<number>} ends Array of end indexes.
         * @param {number} stride Stride.
         * @param {boolean=} opt_right Test for right-hand orientation
         *     (counter-clockwise exterior ring and clockwise interior rings).
         * @return {boolean} Rings are correctly oriented.
         */
        ol.geom.flat.orient.linearRingsAreOriented = function(flatCoordinates, offset, ends, stride, opt_right) {
            var right = opt_right !== undefined ? opt_right : false;
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(
                    flatCoordinates, offset, end, stride);
                if (i === 0) {
                    if ((right && isClockwise) || (!right && !isClockwise)) {
                        return false;
                    }
                } else {
                    if ((right && !isClockwise) || (!right && isClockwise)) {
                        return false;
                    }
                }
                offset = end;
            }
            return true;
        };


        /**
         * Determines if linear rings are oriented.  By default, left-hand orientation
         * is tested (first ring must be clockwise, remaining rings counter-clockwise).
         * To test for right-hand orientation, use the `opt_right` argument.
         *
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<Array.<number>>} endss Array of array of end indexes.
         * @param {number} stride Stride.
         * @param {boolean=} opt_right Test for right-hand orientation
         *     (counter-clockwise exterior ring and clockwise interior rings).
         * @return {boolean} Rings are correctly oriented.
         */
        ol.geom.flat.orient.linearRingssAreOriented = function(flatCoordinates, offset, endss, stride, opt_right) {
            var i, ii;
            for (i = 0, ii = endss.length; i < ii; ++i) {
                if (!ol.geom.flat.orient.linearRingsAreOriented(
                    flatCoordinates, offset, endss[i], stride, opt_right)) {
                    return false;
                }
            }
            return true;
        };


        /**
         * Orient coordinates in a flat array of linear rings.  By default, rings
         * are oriented following the left-hand rule (clockwise for exterior and
         * counter-clockwise for interior rings).  To orient according to the
         * right-hand rule, use the `opt_right` argument.
         *
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<number>} ends Ends.
         * @param {number} stride Stride.
         * @param {boolean=} opt_right Follow the right-hand rule for orientation.
         * @return {number} End.
         */
        ol.geom.flat.orient.orientLinearRings = function(flatCoordinates, offset, ends, stride, opt_right) {
            var right = opt_right !== undefined ? opt_right : false;
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(
                    flatCoordinates, offset, end, stride);
                var reverse = i === 0 ?
                    (right && isClockwise) || (!right && !isClockwise) :
                    (right && !isClockwise) || (!right && isClockwise);
                if (reverse) {
                    ol.geom.flat.reverse.coordinates(flatCoordinates, offset, end, stride);
                }
                offset = end;
            }
            return offset;
        };


        /**
         * Orient coordinates in a flat array of linear rings.  By default, rings
         * are oriented following the left-hand rule (clockwise for exterior and
         * counter-clockwise for interior rings).  To orient according to the
         * right-hand rule, use the `opt_right` argument.
         *
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<Array.<number>>} endss Array of array of end indexes.
         * @param {number} stride Stride.
         * @param {boolean=} opt_right Follow the right-hand rule for orientation.
         * @return {number} End.
         */
        ol.geom.flat.orient.orientLinearRingss = function(flatCoordinates, offset, endss, stride, opt_right) {
            var i, ii;
            for (i = 0, ii = endss.length; i < ii; ++i) {
                offset = ol.geom.flat.orient.orientLinearRings(
                    flatCoordinates, offset, endss[i], stride, opt_right);
            }
            return offset;
        };

        ol.geom.flat.simplify = {};

        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {number} squaredTolerance Squared tolerance.
         * @param {boolean} highQuality Highest quality.
         * @param {Array.<number>=} opt_simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @return {Array.<number>} Simplified line string.
         */
        ol.geom.flat.simplify.lineString = function(flatCoordinates, offset, end,
                                                    stride, squaredTolerance, highQuality, opt_simplifiedFlatCoordinates) {
            var simplifiedFlatCoordinates = opt_simplifiedFlatCoordinates !== undefined ?
                opt_simplifiedFlatCoordinates : [];
            if (!highQuality) {
                end = ol.geom.flat.simplify.radialDistance(flatCoordinates, offset, end,
                    stride, squaredTolerance,
                    simplifiedFlatCoordinates, 0);
                flatCoordinates = simplifiedFlatCoordinates;
                offset = 0;
                stride = 2;
            }
            simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
                flatCoordinates, offset, end, stride, squaredTolerance,
                simplifiedFlatCoordinates, 0);
            return simplifiedFlatCoordinates;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {number} squaredTolerance Squared tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.douglasPeucker = function(flatCoordinates, offset, end,
                                                        stride, squaredTolerance, simplifiedFlatCoordinates, simplifiedOffset) {
            var n = (end - offset) / stride;
            if (n < 3) {
                for (; offset < end; offset += stride) {
                    simplifiedFlatCoordinates[simplifiedOffset++] =
                        flatCoordinates[offset];
                    simplifiedFlatCoordinates[simplifiedOffset++] =
                        flatCoordinates[offset + 1];
                }
                return simplifiedOffset;
            }
            /** @type {Array.<number>} */
            var markers = new Array(n);
            markers[0] = 1;
            markers[n - 1] = 1;
            /** @type {Array.<number>} */
            var stack = [offset, end - stride];
            var index = 0;
            var i;
            while (stack.length > 0) {
                var last = stack.pop();
                var first = stack.pop();
                var maxSquaredDistance = 0;
                var x1 = flatCoordinates[first];
                var y1 = flatCoordinates[first + 1];
                var x2 = flatCoordinates[last];
                var y2 = flatCoordinates[last + 1];
                for (i = first + stride; i < last; i += stride) {
                    var x = flatCoordinates[i];
                    var y = flatCoordinates[i + 1];
                    var squaredDistance = ol.math.squaredSegmentDistance(
                        x, y, x1, y1, x2, y2);
                    if (squaredDistance > maxSquaredDistance) {
                        index = i;
                        maxSquaredDistance = squaredDistance;
                    }
                }
                if (maxSquaredDistance > squaredTolerance) {
                    markers[(index - offset) / stride] = 1;
                    if (first + stride < index) {
                        stack.push(first, index);
                    }
                    if (index + stride < last) {
                        stack.push(index, last);
                    }
                }
            }
            for (i = 0; i < n; ++i) {
                if (markers[i]) {
                    simplifiedFlatCoordinates[simplifiedOffset++] =
                        flatCoordinates[offset + i * stride];
                    simplifiedFlatCoordinates[simplifiedOffset++] =
                        flatCoordinates[offset + i * stride + 1];
                }
            }
            return simplifiedOffset;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<number>} ends Ends.
         * @param {number} stride Stride.
         * @param {number} squaredTolerance Squared tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @param {Array.<number>} simplifiedEnds Simplified ends.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.douglasPeuckers = function(flatCoordinates, offset,
                                                         ends, stride, squaredTolerance, simplifiedFlatCoordinates,
                                                         simplifiedOffset, simplifiedEnds) {
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                simplifiedOffset = ol.geom.flat.simplify.douglasPeucker(
                    flatCoordinates, offset, end, stride, squaredTolerance,
                    simplifiedFlatCoordinates, simplifiedOffset);
                simplifiedEnds.push(simplifiedOffset);
                offset = end;
            }
            return simplifiedOffset;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<Array.<number>>} endss Endss.
         * @param {number} stride Stride.
         * @param {number} squaredTolerance Squared tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.douglasPeuckerss = function(
            flatCoordinates, offset, endss, stride, squaredTolerance,
            simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
            var i, ii;
            for (i = 0, ii = endss.length; i < ii; ++i) {
                var ends = endss[i];
                var simplifiedEnds = [];
                simplifiedOffset = ol.geom.flat.simplify.douglasPeuckers(
                    flatCoordinates, offset, ends, stride, squaredTolerance,
                    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
                simplifiedEndss.push(simplifiedEnds);
                offset = ends[ends.length - 1];
            }
            return simplifiedOffset;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {number} squaredTolerance Squared tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.radialDistance = function(flatCoordinates, offset, end,
                                                        stride, squaredTolerance, simplifiedFlatCoordinates, simplifiedOffset) {
            if (end <= offset + stride) {
                // zero or one point, no simplification possible, so copy and return
                for (; offset < end; offset += stride) {
                    simplifiedFlatCoordinates[simplifiedOffset++] = flatCoordinates[offset];
                    simplifiedFlatCoordinates[simplifiedOffset++] =
                        flatCoordinates[offset + 1];
                }
                return simplifiedOffset;
            }
            var x1 = flatCoordinates[offset];
            var y1 = flatCoordinates[offset + 1];
            // copy first point
            simplifiedFlatCoordinates[simplifiedOffset++] = x1;
            simplifiedFlatCoordinates[simplifiedOffset++] = y1;
            var x2 = x1;
            var y2 = y1;
            for (offset += stride; offset < end; offset += stride) {
                x2 = flatCoordinates[offset];
                y2 = flatCoordinates[offset + 1];
                if (ol.math.squaredDistance(x1, y1, x2, y2) > squaredTolerance) {
                    // copy point at offset
                    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
                    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
                    x1 = x2;
                    y1 = y2;
                }
            }
            if (x2 != x1 || y2 != y1) {
                // copy last point
                simplifiedFlatCoordinates[simplifiedOffset++] = x2;
                simplifiedFlatCoordinates[simplifiedOffset++] = y2;
            }
            return simplifiedOffset;
        };


        /**
         * @param {number} value Value.
         * @param {number} tolerance Tolerance.
         * @return {number} Rounded value.
         */
        ol.geom.flat.simplify.snap = function(value, tolerance) {
            return tolerance * Math.round(value / tolerance);
        };


        /**
         * Simplifies a line string using an algorithm designed by Tim Schaub.
         * Coordinates are snapped to the nearest value in a virtual grid and
         * consecutive duplicate coordinates are discarded.  This effectively preserves
         * topology as the simplification of any subsection of a line string is
         * independent of the rest of the line string.  This means that, for examples,
         * the common edge between two polygons will be simplified to the same line
         * string independently in both polygons.  This implementation uses a single
         * pass over the coordinates and eliminates intermediate collinear points.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {number} tolerance Tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.quantize = function(flatCoordinates, offset, end, stride,
                                                  tolerance, simplifiedFlatCoordinates, simplifiedOffset) {
            // do nothing if the line is empty
            if (offset == end) {
                return simplifiedOffset;
            }
            // snap the first coordinate (P1)
            var x1 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
            var y1 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
            offset += stride;
            // add the first coordinate to the output
            simplifiedFlatCoordinates[simplifiedOffset++] = x1;
            simplifiedFlatCoordinates[simplifiedOffset++] = y1;
            // find the next coordinate that does not snap to the same value as the first
            // coordinate (P2)
            var x2, y2;
            do {
                x2 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
                y2 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
                offset += stride;
                if (offset == end) {
                    // all coordinates snap to the same value, the line collapses to a point
                    // push the last snapped value anyway to ensure that the output contains
                    // at least two points
                    // FIXME should we really return at least two points anyway?
                    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
                    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
                    return simplifiedOffset;
                }
            } while (x2 == x1 && y2 == y1);
            while (offset < end) {
                var x3, y3;
                // snap the next coordinate (P3)
                x3 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
                y3 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
                offset += stride;
                // skip P3 if it is equal to P2
                if (x3 == x2 && y3 == y2) {
                    continue;
                }
                // calculate the delta between P1 and P2
                var dx1 = x2 - x1;
                var dy1 = y2 - y1;
                // calculate the delta between P3 and P1
                var dx2 = x3 - x1;
                var dy2 = y3 - y1;
                // if P1, P2, and P3 are colinear and P3 is further from P1 than P2 is from
                // P1 in the same direction then P2 is on the straight line between P1 and
                // P3
                if ((dx1 * dy2 == dy1 * dx2) &&
                    ((dx1 < 0 && dx2 < dx1) || dx1 == dx2 || (dx1 > 0 && dx2 > dx1)) &&
                    ((dy1 < 0 && dy2 < dy1) || dy1 == dy2 || (dy1 > 0 && dy2 > dy1))) {
                    // discard P2 and set P2 = P3
                    x2 = x3;
                    y2 = y3;
                    continue;
                }
                // either P1, P2, and P3 are not colinear, or they are colinear but P3 is
                // between P3 and P1 or on the opposite half of the line to P2.  add P2,
                // and continue with P1 = P2 and P2 = P3
                simplifiedFlatCoordinates[simplifiedOffset++] = x2;
                simplifiedFlatCoordinates[simplifiedOffset++] = y2;
                x1 = x2;
                y1 = y2;
                x2 = x3;
                y2 = y3;
            }
            // add the last point (P2)
            simplifiedFlatCoordinates[simplifiedOffset++] = x2;
            simplifiedFlatCoordinates[simplifiedOffset++] = y2;
            return simplifiedOffset;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<number>} ends Ends.
         * @param {number} stride Stride.
         * @param {number} tolerance Tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @param {Array.<number>} simplifiedEnds Simplified ends.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.quantizes = function(
            flatCoordinates, offset, ends, stride,
            tolerance,
            simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds) {
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                simplifiedOffset = ol.geom.flat.simplify.quantize(
                    flatCoordinates, offset, end, stride,
                    tolerance,
                    simplifiedFlatCoordinates, simplifiedOffset);
                simplifiedEnds.push(simplifiedOffset);
                offset = end;
            }
            return simplifiedOffset;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<Array.<number>>} endss Endss.
         * @param {number} stride Stride.
         * @param {number} tolerance Tolerance.
         * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
         *     coordinates.
         * @param {number} simplifiedOffset Simplified offset.
         * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
         * @return {number} Simplified offset.
         */
        ol.geom.flat.simplify.quantizess = function(
            flatCoordinates, offset, endss, stride,
            tolerance,
            simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
            var i, ii;
            for (i = 0, ii = endss.length; i < ii; ++i) {
                var ends = endss[i];
                var simplifiedEnds = [];
                simplifiedOffset = ol.geom.flat.simplify.quantizes(
                    flatCoordinates, offset, ends, stride,
                    tolerance,
                    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
                simplifiedEndss.push(simplifiedEnds);
                offset = ends[ends.length - 1];
            }
            return simplifiedOffset;
        };

        ol.geom.GeometryType = {
            POINT: 'Point',
            LINE_STRING: 'LineString',
            LINEAR_RING: 'LinearRing',
            POLYGON: 'Polygon',
            MULTI_POINT: 'MultiPoint',
            MULTI_LINE_STRING: 'MultiLineString',
            MULTI_POLYGON: 'MultiPolygon',
            GEOMETRY_COLLECTION: 'GeometryCollection',
            CIRCLE: 'Circle'
        };

        /**
         * @classdesc
         * Abstract base class; normally only used for creating subclasses and not
         * instantiated in apps.
         * Base class for vector geometries.
         *
         * To get notified of changes to the geometry, register a listener for the
         * generic `change` event on your geometry instance.
         *
         * @constructor
         * @abstract
         * @extends {ol.Object}
         * @api
         */
        ol.geom.Geometry = function() {

            ol.Object.call(this);

            /**
             * @private
             * @type {ol.Extent}
             */
            this.extent_ = ol.extent.createEmpty();

            /**
             * @private
             * @type {number}
             */
            this.extentRevision_ = -1;

            /**
             * @protected
             * @type {Object.<string, ol.geom.Geometry>}
             */
            this.simplifiedGeometryCache = {};

            /**
             * @protected
             * @type {number}
             */
            this.simplifiedGeometryMaxMinSquaredTolerance = 0;

            /**
             * @protected
             * @type {number}
             */
            this.simplifiedGeometryRevision = 0;

            /**
             * @private
             * @type {ol.Transform}
             */
            this.tmpTransform_ = ol.transform.create();

        };
        ol.inherits(ol.geom.Geometry, ol.Object);


        /**
         * Make a complete copy of the geometry.
         * @abstract
         * @return {!ol.geom.Geometry} Clone.
         */
        ol.geom.Geometry.prototype.clone = function() {};


        /**
         * @abstract
         * @param {number} x X.
         * @param {number} y Y.
         * @param {ol.Coordinate} closestPoint Closest point.
         * @param {number} minSquaredDistance Minimum squared distance.
         * @return {number} Minimum squared distance.
         */
        ol.geom.Geometry.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {};


        /**
         * Return the closest point of the geometry to the passed point as
         * {@link ol.Coordinate coordinate}.
         * @param {ol.Coordinate} point Point.
         * @param {ol.Coordinate=} opt_closestPoint Closest point.
         * @return {ol.Coordinate} Closest point.
         * @api
         */
        ol.geom.Geometry.prototype.getClosestPoint = function(point, opt_closestPoint) {
            var closestPoint = opt_closestPoint ? opt_closestPoint : [NaN, NaN];
            this.closestPointXY(point[0], point[1], closestPoint, Infinity);
            return closestPoint;
        };


        /**
         * Returns true if this geometry includes the specified coordinate. If the
         * coordinate is on the boundary of the geometry, returns false.
         * @param {ol.Coordinate} coordinate Coordinate.
         * @return {boolean} Contains coordinate.
         * @api
         */
        ol.geom.Geometry.prototype.intersectsCoordinate = function(coordinate) {
            return this.containsXY(coordinate[0], coordinate[1]);
        };


        /**
         * @abstract
         * @param {ol.Extent} extent Extent.
         * @protected
         * @return {ol.Extent} extent Extent.
         */
        ol.geom.Geometry.prototype.computeExtent = function(extent) {};


        /**
         * @param {number} x X.
         * @param {number} y Y.
         * @return {boolean} Contains (x, y).
         */
        ol.geom.Geometry.prototype.containsXY = ol.functions.FALSE;


        /**
         * Get the extent of the geometry.
         * @param {ol.Extent=} opt_extent Extent.
         * @return {ol.Extent} extent Extent.
         * @api
         */
        ol.geom.Geometry.prototype.getExtent = function(opt_extent) {
            if (this.extentRevision_ != this.getRevision()) {
                this.extent_ = this.computeExtent(this.extent_);
                this.extentRevision_ = this.getRevision();
            }
            return ol.extent.returnOrUpdate(this.extent_, opt_extent);
        };


        /**
         * Rotate the geometry around a given coordinate. This modifies the geometry
         * coordinates in place.
         * @abstract
         * @param {number} angle Rotation angle in radians.
         * @param {ol.Coordinate} anchor The rotation center.
         * @api
         */
        ol.geom.Geometry.prototype.rotate = function(angle, anchor) {};


        /**
         * Scale the geometry (with an optional origin).  This modifies the geometry
         * coordinates in place.
         * @abstract
         * @param {number} sx The scaling factor in the x-direction.
         * @param {number=} opt_sy The scaling factor in the y-direction (defaults to
         *     sx).
         * @param {ol.Coordinate=} opt_anchor The scale origin (defaults to the center
         *     of the geometry extent).
         * @api
         */
        ol.geom.Geometry.prototype.scale = function(sx, opt_sy, opt_anchor) {};


        /**
         * Create a simplified version of this geometry.  For linestrings, this uses
         * the the {@link
         * https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
         * Douglas Peucker} algorithm.  For polygons, a quantization-based
         * simplification is used to preserve topology.
         * @function
         * @param {number} tolerance The tolerance distance for simplification.
         * @return {ol.geom.Geometry} A new, simplified version of the original
         *     geometry.
         * @api
         */
        ol.geom.Geometry.prototype.simplify = function(tolerance) {
            return this.getSimplifiedGeometry(tolerance * tolerance);
        };


        /**
         * Create a simplified version of this geometry using the Douglas Peucker
         * algorithm.
         * @see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
         * @abstract
         * @param {number} squaredTolerance Squared tolerance.
         * @return {ol.geom.Geometry} Simplified geometry.
         */
        ol.geom.Geometry.prototype.getSimplifiedGeometry = function(squaredTolerance) {};


        /**
         * Get the type of this geometry.
         * @abstract
         * @return {ol.geom.GeometryType} Geometry type.
         */
        ol.geom.Geometry.prototype.getType = function() {};


        /**
         * Apply a transform function to each coordinate of the geometry.
         * The geometry is modified in place.
         * If you do not want the geometry modified in place, first `clone()` it and
         * then use this function on the clone.
         * @abstract
         * @param {ol.TransformFunction} transformFn Transform.
         */
        ol.geom.Geometry.prototype.applyTransform = function(transformFn) {};


        /**
         * Test if the geometry and the passed extent intersect.
         * @abstract
         * @param {ol.Extent} extent Extent.
         * @return {boolean} `true` if the geometry and the extent intersect.
         */
        ol.geom.Geometry.prototype.intersectsExtent = function(extent) {};


        /**
         * Translate the geometry.  This modifies the geometry coordinates in place.  If
         * instead you want a new geometry, first `clone()` this geometry.
         * @abstract
         * @param {number} deltaX Delta X.
         * @param {number} deltaY Delta Y.
         */
        ol.geom.Geometry.prototype.translate = function(deltaX, deltaY) {};


        /**
         * Transform each coordinate of the geometry from one coordinate reference
         * system to another. The geometry is modified in place.
         * For example, a line will be transformed to a line and a circle to a circle.
         * If you do not want the geometry modified in place, first `clone()` it and
         * then use this function on the clone.
         *
         * @param {ol.ProjectionLike} source The current projection.  Can be a
         *     string identifier or a {@link ol.proj.Projection} object.
         * @param {ol.ProjectionLike} destination The desired projection.  Can be a
         *     string identifier or a {@link ol.proj.Projection} object.
         * @return {ol.geom.Geometry} This geometry.  Note that original geometry is
         *     modified in place.
         * @api
         */
        ol.geom.Geometry.prototype.transform = function(source, destination) {
            var tmpTransform = this.tmpTransform_;
            source = ol.proj.get(source);
            var transformFn = source.getUnits() == ol.proj.Units.TILE_PIXELS ?
                function(inCoordinates, outCoordinates, stride) {
                    var pixelExtent = source.getExtent();
                    var projectedExtent = source.getWorldExtent();
                    var scale = ol.extent.getHeight(projectedExtent) / ol.extent.getHeight(pixelExtent);
                    ol.transform.compose(tmpTransform,
                        projectedExtent[0], projectedExtent[3],
                        scale, -scale, 0,
                        0, 0);
                    ol.geom.flat.transform.transform2D(inCoordinates, 0, inCoordinates.length, stride,
                        tmpTransform, outCoordinates);
                    return ol.proj.getTransform(source, destination)(inCoordinates, outCoordinates, stride);
                } :
                ol.proj.getTransform(source, destination);
            this.applyTransform(transformFn);
            return this;
        };


        /**
         * @classdesc
         * Abstract base class; only used for creating subclasses; do not instantiate
         * in apps, as cannot be rendered.
         *
         * @constructor
         * @abstract
         * @extends {ol.geom.Geometry}
         * @api
         */
        ol.geom.SimpleGeometry = function() {

            ol.geom.Geometry.call(this);

            /**
             * @protected
             * @type {ol.geom.GeometryLayout}
             */
            this.layout = ol.geom.GeometryLayout.XY;

            /**
             * @protected
             * @type {number}
             */
            this.stride = 2;

            /**
             * @protected
             * @type {Array.<number>}
             */
            this.flatCoordinates = null;

        };
        ol.inherits(ol.geom.SimpleGeometry, ol.geom.Geometry);


        /**
         * @param {number} stride Stride.
         * @private
         * @return {ol.geom.GeometryLayout} layout Layout.
         */
        ol.geom.SimpleGeometry.getLayoutForStride_ = function(stride) {
            var layout;
            if (stride == 2) {
                layout = ol.geom.GeometryLayout.XY;
            } else if (stride == 3) {
                layout = ol.geom.GeometryLayout.XYZ;
            } else if (stride == 4) {
                layout = ol.geom.GeometryLayout.XYZM;
            }
            return /** @type {ol.geom.GeometryLayout} */ (layout);
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @return {number} Stride.
         */
        ol.geom.SimpleGeometry.getStrideForLayout = function(layout) {
            var stride;
            if (layout == ol.geom.GeometryLayout.XY) {
                stride = 2;
            } else if (layout == ol.geom.GeometryLayout.XYZ || layout == ol.geom.GeometryLayout.XYM) {
                stride = 3;
            } else if (layout == ol.geom.GeometryLayout.XYZM) {
                stride = 4;
            }
            return /** @type {number} */ (stride);
        };


        /**
         * @inheritDoc
         */
        ol.geom.SimpleGeometry.prototype.containsXY = ol.functions.FALSE;


        /**
         * @inheritDoc
         */
        ol.geom.SimpleGeometry.prototype.computeExtent = function(extent) {
            return ol.extent.createOrUpdateFromFlatCoordinates(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
                extent);
        };


        /**
         * @abstract
         * @return {Array} Coordinates.
         */
        ol.geom.SimpleGeometry.prototype.getCoordinates = function() {};


        /**
         * Return the first coordinate of the geometry.
         * @return {ol.Coordinate} First coordinate.
         * @api
         */
        ol.geom.SimpleGeometry.prototype.getFirstCoordinate = function() {
            return this.flatCoordinates.slice(0, this.stride);
        };


        /**
         * @return {Array.<number>} Flat coordinates.
         */
        ol.geom.SimpleGeometry.prototype.getFlatCoordinates = function() {
            return this.flatCoordinates;
        };


        /**
         * Return the last coordinate of the geometry.
         * @return {ol.Coordinate} Last point.
         * @api
         */
        ol.geom.SimpleGeometry.prototype.getLastCoordinate = function() {
            return this.flatCoordinates.slice(this.flatCoordinates.length - this.stride);
        };


        /**
         * Return the {@link ol.geom.GeometryLayout layout} of the geometry.
         * @return {ol.geom.GeometryLayout} Layout.
         * @api
         */
        ol.geom.SimpleGeometry.prototype.getLayout = function() {
            return this.layout;
        };


        /**
         * @inheritDoc
         */
        ol.geom.SimpleGeometry.prototype.getSimplifiedGeometry = function(squaredTolerance) {
            if (this.simplifiedGeometryRevision != this.getRevision()) {
                ol.obj.clear(this.simplifiedGeometryCache);
                this.simplifiedGeometryMaxMinSquaredTolerance = 0;
                this.simplifiedGeometryRevision = this.getRevision();
            }
            // If squaredTolerance is negative or if we know that simplification will not
            // have any effect then just return this.
            if (squaredTolerance < 0 ||
                (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
                    squaredTolerance <= this.simplifiedGeometryMaxMinSquaredTolerance)) {
                return this;
            }
            var key = squaredTolerance.toString();
            if (this.simplifiedGeometryCache.hasOwnProperty(key)) {
                return this.simplifiedGeometryCache[key];
            } else {
                var simplifiedGeometry =
                    this.getSimplifiedGeometryInternal(squaredTolerance);
                var simplifiedFlatCoordinates = simplifiedGeometry.getFlatCoordinates();
                if (simplifiedFlatCoordinates.length < this.flatCoordinates.length) {
                    this.simplifiedGeometryCache[key] = simplifiedGeometry;
                    return simplifiedGeometry;
                } else {
                    // Simplification did not actually remove any coordinates.  We now know
                    // that any calls to getSimplifiedGeometry with a squaredTolerance less
                    // than or equal to the current squaredTolerance will also not have any
                    // effect.  This allows us to short circuit simplification (saving CPU
                    // cycles) and prevents the cache of simplified geometries from filling
                    // up with useless identical copies of this geometry (saving memory).
                    this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
                    return this;
                }
            }
        };


        /**
         * @param {number} squaredTolerance Squared tolerance.
         * @return {ol.geom.SimpleGeometry} Simplified geometry.
         * @protected
         */
        ol.geom.SimpleGeometry.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
            return this;
        };


        /**
         * @return {number} Stride.
         */
        ol.geom.SimpleGeometry.prototype.getStride = function() {
            return this.stride;
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @protected
         */
        ol.geom.SimpleGeometry.prototype.setFlatCoordinatesInternal = function(layout, flatCoordinates) {
            this.stride = ol.geom.SimpleGeometry.getStrideForLayout(layout);
            this.layout = layout;
            this.flatCoordinates = flatCoordinates;
        };


        /**
         * @abstract
         * @param {Array} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         */
        ol.geom.SimpleGeometry.prototype.setCoordinates = function(coordinates, opt_layout) {};


        /**
         * @param {ol.geom.GeometryLayout|undefined} layout Layout.
         * @param {Array} coordinates Coordinates.
         * @param {number} nesting Nesting.
         * @protected
         */
        ol.geom.SimpleGeometry.prototype.setLayout = function(layout, coordinates, nesting) {
            /** @type {number} */
            var stride;
            if (layout) {
                stride = ol.geom.SimpleGeometry.getStrideForLayout(layout);
            } else {
                var i;
                for (i = 0; i < nesting; ++i) {
                    if (coordinates.length === 0) {
                        this.layout = ol.geom.GeometryLayout.XY;
                        this.stride = 2;
                        return;
                    } else {
                        coordinates = /** @type {Array} */ (coordinates[0]);
                    }
                }
                stride = coordinates.length;
                layout = ol.geom.SimpleGeometry.getLayoutForStride_(stride);
            }
            this.layout = layout;
            this.stride = stride;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.SimpleGeometry.prototype.applyTransform = function(transformFn) {
            if (this.flatCoordinates) {
                transformFn(this.flatCoordinates, this.flatCoordinates, this.stride);
                this.changed();
            }
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.SimpleGeometry.prototype.rotate = function(angle, anchor) {
            var flatCoordinates = this.getFlatCoordinates();
            if (flatCoordinates) {
                var stride = this.getStride();
                ol.geom.flat.transform.rotate(
                    flatCoordinates, 0, flatCoordinates.length,
                    stride, angle, anchor, flatCoordinates);
                this.changed();
            }
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.SimpleGeometry.prototype.scale = function(sx, opt_sy, opt_anchor) {
            var sy = opt_sy;
            if (sy === undefined) {
                sy = sx;
            }
            var anchor = opt_anchor;
            if (!anchor) {
                anchor = ol.extent.getCenter(this.getExtent());
            }
            var flatCoordinates = this.getFlatCoordinates();
            if (flatCoordinates) {
                var stride = this.getStride();
                ol.geom.flat.transform.scale(
                    flatCoordinates, 0, flatCoordinates.length,
                    stride, sx, sy, anchor, flatCoordinates);
                this.changed();
            }
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.SimpleGeometry.prototype.translate = function(deltaX, deltaY) {
            var flatCoordinates = this.getFlatCoordinates();
            if (flatCoordinates) {
                var stride = this.getStride();
                ol.geom.flat.transform.translate(
                    flatCoordinates, 0, flatCoordinates.length, stride,
                    deltaX, deltaY, flatCoordinates);
                this.changed();
            }
        };


        /**
         * @param {ol.geom.SimpleGeometry} simpleGeometry Simple geometry.
         * @param {ol.Transform} transform Transform.
         * @param {Array.<number>=} opt_dest Destination.
         * @return {Array.<number>} Transformed flat coordinates.
         */
        ol.geom.SimpleGeometry.transform2D = function(simpleGeometry, transform, opt_dest) {
            var flatCoordinates = simpleGeometry.getFlatCoordinates();
            if (!flatCoordinates) {
                return null;
            } else {
                var stride = simpleGeometry.getStride();
                return ol.geom.flat.transform.transform2D(
                    flatCoordinates, 0, flatCoordinates.length, stride,
                    transform, opt_dest);
            }
        };

        ol.geom.Polygon = function(coordinates, opt_layout) {

            ol.geom.SimpleGeometry.call(this);

            /**
             * @type {Array.<number>}
             * @private
             */
            this.ends_ = [];

            /**
             * @private
             * @type {number}
             */
            this.flatInteriorPointRevision_ = -1;

            /**
             * @private
             * @type {ol.Coordinate}
             */
            this.flatInteriorPoint_ = null;

            /**
             * @private
             * @type {number}
             */
            this.maxDelta_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.maxDeltaRevision_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.orientedRevision_ = -1;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.orientedFlatCoordinates_ = null;

            this.setCoordinates(coordinates, opt_layout);

        };
        ol.inherits(ol.geom.Polygon, ol.geom.SimpleGeometry);


        /**
         * Append the passed linear ring to this polygon.
         * @param {ol.geom.LinearRing} linearRing Linear ring.
         * @api
         */
        ol.geom.Polygon.prototype.appendLinearRing = function(linearRing) {
            if (!this.flatCoordinates) {
                this.flatCoordinates = linearRing.getFlatCoordinates().slice();
            } else {
                ol.array.extend(this.flatCoordinates, linearRing.getFlatCoordinates());
            }
            this.ends_.push(this.flatCoordinates.length);
            this.changed();
        };


        /**
         * Make a complete copy of the geometry.
         * @return {!ol.geom.Polygon} Clone.
         * @override
         * @api
         */
        ol.geom.Polygon.prototype.clone = function() {
            var polygon = new ol.geom.Polygon(null);
            polygon.setFlatCoordinates(
                this.layout, this.flatCoordinates.slice(), this.ends_.slice());
            return polygon;
        };


        /**
         * @inheritDoc
         */
        ol.geom.Polygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
            if (minSquaredDistance <
                ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
                return minSquaredDistance;
            }
            if (this.maxDeltaRevision_ != this.getRevision()) {
                this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getsMaxSquaredDelta(
                    this.flatCoordinates, 0, this.ends_, this.stride, 0));
                this.maxDeltaRevision_ = this.getRevision();
            }
            return ol.geom.flat.closest.getsClosestPoint(
                this.flatCoordinates, 0, this.ends_, this.stride,
                this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
        };


        /**
         * @inheritDoc
         */
        ol.geom.Polygon.prototype.containsXY = function(x, y) {
            return ol.geom.flat.contains.linearRingsContainsXY(
                this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, x, y);
        };


        /**
         * Return the area of the polygon on projected plane.
         * @return {number} Area (on projected plane).
         * @api
         */
        ol.geom.Polygon.prototype.getArea = function() {
            return ol.geom.flat.area.linearRings(
                this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride);
        };


        /**
         * Get the coordinate array for this geometry.  This array has the structure
         * of a GeoJSON coordinate array for polygons.
         *
         * @param {boolean=} opt_right Orient coordinates according to the right-hand
         *     rule (counter-clockwise for exterior and clockwise for interior rings).
         *     If `false`, coordinates will be oriented according to the left-hand rule
         *     (clockwise for exterior and counter-clockwise for interior rings).
         *     By default, coordinate orientation will depend on how the geometry was
         *     constructed.
         * @return {Array.<Array.<ol.Coordinate>>} Coordinates.
         * @override
         * @api
         */
        ol.geom.Polygon.prototype.getCoordinates = function(opt_right) {
            var flatCoordinates;
            if (opt_right !== undefined) {
                flatCoordinates = this.getOrientedFlatCoordinates().slice();
                ol.geom.flat.orient.orientLinearRings(
                    flatCoordinates, 0, this.ends_, this.stride, opt_right);
            } else {
                flatCoordinates = this.flatCoordinates;
            }

            return ol.geom.flat.inflate.coordinatess(
                flatCoordinates, 0, this.ends_, this.stride);
        };


        /**
         * @return {Array.<number>} Ends.
         */
        ol.geom.Polygon.prototype.getEnds = function() {
            return this.ends_;
        };


        /**
         * @return {Array.<number>} Interior point.
         */
        ol.geom.Polygon.prototype.getFlatInteriorPoint = function() {
            if (this.flatInteriorPointRevision_ != this.getRevision()) {
                var flatCenter = ol.extent.getCenter(this.getExtent());
                this.flatInteriorPoint_ = ol.geom.flat.interiorpoint.linearRings(
                    this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride,
                    flatCenter, 0);
                this.flatInteriorPointRevision_ = this.getRevision();
            }
            return this.flatInteriorPoint_;
        };


        /**
         * Return an interior point of the polygon.
         * @return {ol.geom.Point} Interior point as XYM coordinate, where M is the
         * length of the horizontal intersection that the point belongs to.
         * @api
         */
        ol.geom.Polygon.prototype.getInteriorPoint = function() {
            return new ol.geom.Point(this.getFlatInteriorPoint(), ol.geom.GeometryLayout.XYM);
        };


        /**
         * Return the number of rings of the polygon,  this includes the exterior
         * ring and any interior rings.
         *
         * @return {number} Number of rings.
         * @api
         */
        ol.geom.Polygon.prototype.getLinearRingCount = function() {
            return this.ends_.length;
        };


        /**
         * Return the Nth linear ring of the polygon geometry. Return `null` if the
         * given index is out of range.
         * The exterior linear ring is available at index `0` and the interior rings
         * at index `1` and beyond.
         *
         * @param {number} index Index.
         * @return {ol.geom.LinearRing} Linear ring.
         * @api
         */
        ol.geom.Polygon.prototype.getLinearRing = function(index) {
            if (index < 0 || this.ends_.length <= index) {
                return null;
            }
            var linearRing = new ol.geom.LinearRing(null);
            linearRing.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
                index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]));
            return linearRing;
        };


        /**
         * Return the linear rings of the polygon.
         * @return {Array.<ol.geom.LinearRing>} Linear rings.
         * @api
         */
        ol.geom.Polygon.prototype.getLinearRings = function() {
            var layout = this.layout;
            var flatCoordinates = this.flatCoordinates;
            var ends = this.ends_;
            var linearRings = [];
            var offset = 0;
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                var linearRing = new ol.geom.LinearRing(null);
                linearRing.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
                linearRings.push(linearRing);
                offset = end;
            }
            return linearRings;
        };


        /**
         * @return {Array.<number>} Oriented flat coordinates.
         */
        ol.geom.Polygon.prototype.getOrientedFlatCoordinates = function() {
            if (this.orientedRevision_ != this.getRevision()) {
                var flatCoordinates = this.flatCoordinates;
                if (ol.geom.flat.orient.linearRingsAreOriented(
                    flatCoordinates, 0, this.ends_, this.stride)) {
                    this.orientedFlatCoordinates_ = flatCoordinates;
                } else {
                    this.orientedFlatCoordinates_ = flatCoordinates.slice();
                    this.orientedFlatCoordinates_.length =
                        ol.geom.flat.orient.orientLinearRings(
                            this.orientedFlatCoordinates_, 0, this.ends_, this.stride);
                }
                this.orientedRevision_ = this.getRevision();
            }
            return this.orientedFlatCoordinates_;
        };


        /**
         * @inheritDoc
         */
        ol.geom.Polygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
            var simplifiedFlatCoordinates = [];
            var simplifiedEnds = [];
            simplifiedFlatCoordinates.length = ol.geom.flat.simplify.quantizes(
                this.flatCoordinates, 0, this.ends_, this.stride,
                Math.sqrt(squaredTolerance),
                simplifiedFlatCoordinates, 0, simplifiedEnds);
            var simplifiedPolygon = new ol.geom.Polygon(null);
            simplifiedPolygon.setFlatCoordinates(
                ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
            return simplifiedPolygon;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.Polygon.prototype.getType = function() {
            return ol.geom.GeometryType.POLYGON;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.Polygon.prototype.intersectsExtent = function(extent) {
            return ol.geom.flat.intersectsextent.linearRings(
                this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, extent);
        };


        /**
         * Set the coordinates of the polygon.
         * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @override
         * @api
         */
        ol.geom.Polygon.prototype.setCoordinates = function(coordinates, opt_layout) {
            if (!coordinates) {
                this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.ends_);
            } else {
                this.setLayout(opt_layout, coordinates, 2);
                if (!this.flatCoordinates) {
                    this.flatCoordinates = [];
                }
                var ends = ol.geom.flat.deflate.coordinatess(
                    this.flatCoordinates, 0, coordinates, this.stride, this.ends_);
                this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
                //this.changed();
            }
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {Array.<number>} ends Ends.
         */
        ol.geom.Polygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
            this.setFlatCoordinatesInternal(layout, flatCoordinates);
            this.ends_ = ends;
            //this.changed();
        };


        /**
         * Create an approximation of a circle on the surface of a sphere.
         * @param {ol.Sphere} sphere The sphere.
         * @param {ol.Coordinate} center Center (`[lon, lat]` in degrees).
         * @param {number} radius The great-circle distance from the center to
         *     the polygon vertices.
         * @param {number=} opt_n Optional number of vertices for the resulting
         *     polygon. Default is `32`.
         * @return {ol.geom.Polygon} The "circular" polygon.
         * @api
         */
        ol.geom.Polygon.circular = function(sphere, center, radius, opt_n) {
            var n = opt_n ? opt_n : 32;
            /** @type {Array.<number>} */
            var flatCoordinates = [];
            var i;
            for (i = 0; i < n; ++i) {
                ol.array.extend(
                    flatCoordinates, sphere.offset(center, radius, 2 * Math.PI * i / n));
            }
            flatCoordinates.push(flatCoordinates[0], flatCoordinates[1]);
            var polygon = new ol.geom.Polygon(null);
            polygon.setFlatCoordinates(
                ol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
            return polygon;
        };


        /**
         * Create a polygon from an extent. The layout used is `XY`.
         * @param {ol.Extent} extent The extent.
         * @return {ol.geom.Polygon} The polygon.
         * @api
         */
        ol.geom.Polygon.fromExtent = function(extent) {
            var minX = extent[0];
            var minY = extent[1];
            var maxX = extent[2];
            var maxY = extent[3];
            var flatCoordinates =
                [minX, minY, minX, maxY, maxX, maxY, maxX, minY, minX, minY];
            var polygon = new ol.geom.Polygon(null);
            polygon.setFlatCoordinates(
                ol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
            return polygon;
        };


        /**
         * Create a regular polygon from a circle.
         * @param {ol.geom.Circle} circle Circle geometry.
         * @param {number=} opt_sides Number of sides of the polygon. Default is 32.
         * @param {number=} opt_angle Start angle for the first vertex of the polygon in
         *     radians. Default is 0.
         * @return {ol.geom.Polygon} Polygon geometry.
         * @api
         */
        ol.geom.Polygon.fromCircle = function(circle, opt_sides, opt_angle) {
            var sides = opt_sides ? opt_sides : 32;
            var stride = circle.getStride();
            var layout = circle.getLayout();
            var polygon = new ol.geom.Polygon(null, layout);
            var arrayLength = stride * (sides + 1);
            var flatCoordinates = new Array(arrayLength);
            for (var i = 0; i < arrayLength; i++) {
                flatCoordinates[i] = 0;
            }
            var ends = [flatCoordinates.length];
            polygon.setFlatCoordinates(layout, flatCoordinates, ends);
            ol.geom.Polygon.makeRegular(
                polygon, circle.getCenter(), circle.getRadius(), opt_angle);
            return polygon;
        };


        /**
         * Modify the coordinates of a polygon to make it a regular polygon.
         * @param {ol.geom.Polygon} polygon Polygon geometry.
         * @param {ol.Coordinate} center Center of the regular polygon.
         * @param {number} radius Radius of the regular polygon.
         * @param {number=} opt_angle Start angle for the first vertex of the polygon in
         *     radians. Default is 0.
         */
        ol.geom.Polygon.makeRegular = function(polygon, center, radius, opt_angle) {
            var flatCoordinates = polygon.getFlatCoordinates();
            var layout = polygon.getLayout();
            var stride = polygon.getStride();
            var ends = polygon.getEnds();
            var sides = flatCoordinates.length / stride - 1;
            var startAngle = opt_angle ? opt_angle : 0;
            var angle, offset;
            for (var i = 0; i <= sides; ++i) {
                offset = i * stride;
                angle = startAngle + (ol.math.modulo(i, sides) * 2 * Math.PI / sides);
                flatCoordinates[offset] = center[0] + (radius * Math.cos(angle));
                flatCoordinates[offset + 1] = center[1] + (radius * Math.sin(angle));
            }
            polygon.setFlatCoordinates(layout, flatCoordinates, ends);
        };

        /**
         * @classdesc
         * Linestring geometry.
         *
         * @constructor
         * @extends {ol.geom.SimpleGeometry}
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @api
         */
        ol.geom.LineString = function(coordinates, opt_layout) {

            ol.geom.SimpleGeometry.call(this);

            /**
             * @private
             * @type {ol.Coordinate}
             */
            this.flatMidpoint_ = null;

            /**
             * @private
             * @type {number}
             */
            this.flatMidpointRevision_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.maxDelta_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.maxDeltaRevision_ = -1;

            this.setCoordinates(coordinates, opt_layout);

        };
        ol.inherits(ol.geom.LineString, ol.geom.SimpleGeometry);


        /**
         * Append the passed coordinate to the coordinates of the linestring.
         * @param {ol.Coordinate} coordinate Coordinate.
         * @api
         */
        ol.geom.LineString.prototype.appendCoordinate = function(coordinate) {
            if (!this.flatCoordinates) {
                this.flatCoordinates = coordinate.slice();
            } else {
                ol.array.extend(this.flatCoordinates, coordinate);
            }
            this.changed();
        };


        /**
         * Make a complete copy of the geometry.
         * @return {!ol.geom.LineString} Clone.
         * @override
         * @api
         */
        ol.geom.LineString.prototype.clone = function() {
            var lineString = new ol.geom.LineString(null);
            lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
            return lineString;
        };


        /**
         * @inheritDoc
         */
        ol.geom.LineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
            if (minSquaredDistance <
                ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
                return minSquaredDistance;
            }
            if (this.maxDeltaRevision_ != this.getRevision()) {
                this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getMaxSquaredDelta(
                    this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
                this.maxDeltaRevision_ = this.getRevision();
            }
            return ol.geom.flat.closest.getClosestPoint(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
                this.maxDelta_, false, x, y, closestPoint, minSquaredDistance);
        };


        /**
         * Iterate over each segment, calling the provided callback.
         * If the callback returns a truthy value the function returns that
         * value immediately. Otherwise the function returns `false`.
         *
         * @param {function(this: S, ol.Coordinate, ol.Coordinate): T} callback Function
         *     called for each segment.
         * @param {S=} opt_this The object to be used as the value of 'this'
         *     within callback.
         * @return {T|boolean} Value.
         * @template T,S
         * @api
         */
        ol.geom.LineString.prototype.forEachSegment = function(callback, opt_this) {
            return ol.geom.flat.segments.forEach(this.flatCoordinates, 0,
                this.flatCoordinates.length, this.stride, callback, opt_this);
        };


        /**
         * Returns the coordinate at `m` using linear interpolation, or `null` if no
         * such coordinate exists.
         *
         * `opt_extrapolate` controls extrapolation beyond the range of Ms in the
         * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first
         * M will return the first coordinate and Ms greater than the last M will
         * return the last coordinate.
         *
         * @param {number} m M.
         * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`.
         * @return {ol.Coordinate} Coordinate.
         * @api
         */
        ol.geom.LineString.prototype.getCoordinateAtM = function(m, opt_extrapolate) {
            if (this.layout != ol.geom.GeometryLayout.XYM &&
                this.layout != ol.geom.GeometryLayout.XYZM) {
                return null;
            }
            var extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false;
            return ol.geom.flat.interpolate.lineStringCoordinateAtM(this.flatCoordinates, 0,
                this.flatCoordinates.length, this.stride, m, extrapolate);
        };


        /**
         * Return the coordinates of the linestring.
         * @return {Array.<ol.Coordinate>} Coordinates.
         * @override
         * @api
         */
        ol.geom.LineString.prototype.getCoordinates = function() {
            return ol.geom.flat.inflate.coordinates(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
        };


        /**
         * Return the coordinate at the provided fraction along the linestring.
         * The `fraction` is a number between 0 and 1, where 0 is the start of the
         * linestring and 1 is the end.
         * @param {number} fraction Fraction.
         * @param {ol.Coordinate=} opt_dest Optional coordinate whose values will
         *     be modified. If not provided, a new coordinate will be returned.
         * @return {ol.Coordinate} Coordinate of the interpolated point.
         * @api
         */
        ol.geom.LineString.prototype.getCoordinateAt = function(fraction, opt_dest) {
            return ol.geom.flat.interpolate.lineString(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
                fraction, opt_dest);
        };


        /**
         * Return the length of the linestring on projected plane.
         * @return {number} Length (on projected plane).
         * @api
         */
        ol.geom.LineString.prototype.getLength = function() {
            return ol.geom.flat.length.lineString(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
        };


        /**
         * @return {Array.<number>} Flat midpoint.
         */
        ol.geom.LineString.prototype.getFlatMidpoint = function() {
            if (this.flatMidpointRevision_ != this.getRevision()) {
                this.flatMidpoint_ = this.getCoordinateAt(0.5, this.flatMidpoint_);
                this.flatMidpointRevision_ = this.getRevision();
            }
            return this.flatMidpoint_;
        };


        /**
         * @inheritDoc
         */
        ol.geom.LineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
            var simplifiedFlatCoordinates = [];
            simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
                squaredTolerance, simplifiedFlatCoordinates, 0);
            var simplifiedLineString = new ol.geom.LineString(null);
            simplifiedLineString.setFlatCoordinates(
                ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
            return simplifiedLineString;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.LineString.prototype.getType = function() {
            return ol.geom.GeometryType.LINE_STRING;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.LineString.prototype.intersectsExtent = function(extent) {
            return ol.geom.flat.intersectsextent.lineString(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
                extent);
        };


        /**
         * Set the coordinates of the linestring.
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @override
         * @api
         */
        ol.geom.LineString.prototype.setCoordinates = function(coordinates, opt_layout) {
            if (!coordinates) {
                this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
            } else {
                this.setLayout(opt_layout, coordinates, 1);
                if (!this.flatCoordinates) {
                    this.flatCoordinates = [];
                }
                this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
                    this.flatCoordinates, 0, coordinates, this.stride);
                this.changed();
            }
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         */
        ol.geom.LineString.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
            this.setFlatCoordinatesInternal(layout, flatCoordinates);
            this.changed();
        };

        ol.geom.Point = function(coordinates, opt_layout) {
            ol.geom.SimpleGeometry.call(this);
            this.setCoordinates(coordinates, opt_layout);
        };
        ol.inherits(ol.geom.Point, ol.geom.SimpleGeometry);

        ol.geom.Point.prototype.clone = function() {
            var point = new ol.geom.Point(null);
            point.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
            return point;
        };


        /**
         * @inheritDoc
         */
        ol.geom.Point.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
            var flatCoordinates = this.flatCoordinates;
            var squaredDistance = ol.math.squaredDistance(
                x, y, flatCoordinates[0], flatCoordinates[1]);
            if (squaredDistance < minSquaredDistance) {
                var stride = this.stride;
                var i;
                for (i = 0; i < stride; ++i) {
                    closestPoint[i] = flatCoordinates[i];
                }
                closestPoint.length = stride;
                return squaredDistance;
            } else {
                return minSquaredDistance;
            }
        };

        ol.geom.Point.prototype.getCoordinates = function() {
            return !this.flatCoordinates ? [] : this.flatCoordinates.slice();
        };

        ol.geom.Point.prototype.computeExtent = function(extent) {
            return ol.extent.createOrUpdateFromCoordinate(this.flatCoordinates, extent);
        };

        ol.geom.Point.prototype.getType = function() {
            return ol.geom.GeometryType.POINT;
        };

        ol.geom.Point.prototype.intersectsExtent = function(extent) {
            return ol.extent.containsXY(extent,
                this.flatCoordinates[0], this.flatCoordinates[1]);
        };

        ol.geom.Point.prototype.setCoordinates = function(coordinates, opt_layout) {
            if (!coordinates) {
                this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
            } else {
                this.setLayout(opt_layout, coordinates, 0);
                if (!this.flatCoordinates) {
                    this.flatCoordinates = [];
                }
                this.flatCoordinates.length = ol.geom.flat.deflate.coordinate(
                    this.flatCoordinates, 0, coordinates, this.stride);
                this.changed();
            }
        };

        ol.geom.Point.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
            this.setFlatCoordinatesInternal(layout, flatCoordinates);
            this.changed();
        };

        /**
         * @classdesc
         * Multi-linestring geometry.
         *
         * @constructor
         * @extends {ol.geom.SimpleGeometry}
         * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @api
         */
        ol.geom.MultiLineString = function(coordinates, opt_layout) {

            ol.geom.SimpleGeometry.call(this);

            /**
             * @type {Array.<number>}
             * @private
             */
            this.ends_ = [];

            /**
             * @private
             * @type {number}
             */
            this.maxDelta_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.maxDeltaRevision_ = -1;

            this.setCoordinates(coordinates, opt_layout);

        };
        ol.inherits(ol.geom.MultiLineString, ol.geom.SimpleGeometry);


        /**
         * Append the passed linestring to the multilinestring.
         * @param {ol.geom.LineString} lineString LineString.
         * @api
         */
        ol.geom.MultiLineString.prototype.appendLineString = function(lineString) {
            if (!this.flatCoordinates) {
                this.flatCoordinates = lineString.getFlatCoordinates().slice();
            } else {
                ol.array.extend(
                    this.flatCoordinates, lineString.getFlatCoordinates().slice());
            }
            this.ends_.push(this.flatCoordinates.length);
            this.changed();
        };


        /**
         * Make a complete copy of the geometry.
         * @return {!ol.geom.MultiLineString} Clone.
         * @override
         * @api
         */
        ol.geom.MultiLineString.prototype.clone = function() {
            var multiLineString = new ol.geom.MultiLineString(null);
            multiLineString.setFlatCoordinates(
                this.layout, this.flatCoordinates.slice(), this.ends_.slice());
            return multiLineString;
        };


        /**
         * @inheritDoc
         */
        ol.geom.MultiLineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
            if (minSquaredDistance <
                ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
                return minSquaredDistance;
            }
            if (this.maxDeltaRevision_ != this.getRevision()) {
                this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getsMaxSquaredDelta(
                    this.flatCoordinates, 0, this.ends_, this.stride, 0));
                this.maxDeltaRevision_ = this.getRevision();
            }
            return ol.geom.flat.closest.getsClosestPoint(
                this.flatCoordinates, 0, this.ends_, this.stride,
                this.maxDelta_, false, x, y, closestPoint, minSquaredDistance);
        };


        /**
         * Returns the coordinate at `m` using linear interpolation, or `null` if no
         * such coordinate exists.
         *
         * `opt_extrapolate` controls extrapolation beyond the range of Ms in the
         * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first
         * M will return the first coordinate and Ms greater than the last M will
         * return the last coordinate.
         *
         * `opt_interpolate` controls interpolation between consecutive LineStrings
         * within the MultiLineString. If `opt_interpolate` is `true` the coordinates
         * will be linearly interpolated between the last coordinate of one LineString
         * and the first coordinate of the next LineString.  If `opt_interpolate` is
         * `false` then the function will return `null` for Ms falling between
         * LineStrings.
         *
         * @param {number} m M.
         * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`.
         * @param {boolean=} opt_interpolate Interpolate. Default is `false`.
         * @return {ol.Coordinate} Coordinate.
         * @api
         */
        ol.geom.MultiLineString.prototype.getCoordinateAtM = function(m, opt_extrapolate, opt_interpolate) {
            if ((this.layout != ol.geom.GeometryLayout.XYM &&
                this.layout != ol.geom.GeometryLayout.XYZM) ||
                this.flatCoordinates.length === 0) {
                return null;
            }
            var extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false;
            var interpolate = opt_interpolate !== undefined ? opt_interpolate : false;
            return ol.geom.flat.interpolate.lineStringsCoordinateAtM(this.flatCoordinates, 0,
                this.ends_, this.stride, m, extrapolate, interpolate);
        };


        /**
         * Return the coordinates of the multilinestring.
         * @return {Array.<Array.<ol.Coordinate>>} Coordinates.
         * @override
         * @api
         */
        ol.geom.MultiLineString.prototype.getCoordinates = function() {
            return ol.geom.flat.inflate.coordinatess(
                this.flatCoordinates, 0, this.ends_, this.stride);
        };


        /**
         * @return {Array.<number>} Ends.
         */
        ol.geom.MultiLineString.prototype.getEnds = function() {
            return this.ends_;
        };


        /**
         * Return the linestring at the specified index.
         * @param {number} index Index.
         * @return {ol.geom.LineString} LineString.
         * @api
         */
        ol.geom.MultiLineString.prototype.getLineString = function(index) {
            if (index < 0 || this.ends_.length <= index) {
                return null;
            }
            var lineString = new ol.geom.LineString(null);
            lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
                index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]));
            return lineString;
        };


        /**
         * Return the linestrings of this multilinestring.
         * @return {Array.<ol.geom.LineString>} LineStrings.
         * @api
         */
        ol.geom.MultiLineString.prototype.getLineStrings = function() {
            var flatCoordinates = this.flatCoordinates;
            var ends = this.ends_;
            var layout = this.layout;
            /** @type {Array.<ol.geom.LineString>} */
            var lineStrings = [];
            var offset = 0;
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                var lineString = new ol.geom.LineString(null);
                lineString.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
                lineStrings.push(lineString);
                offset = end;
            }
            return lineStrings;
        };


        /**
         * @return {Array.<number>} Flat midpoints.
         */
        ol.geom.MultiLineString.prototype.getFlatMidpoints = function() {
            var midpoints = [];
            var flatCoordinates = this.flatCoordinates;
            var offset = 0;
            var ends = this.ends_;
            var stride = this.stride;
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                var midpoint = ol.geom.flat.interpolate.lineString(
                    flatCoordinates, offset, end, stride, 0.5);
                ol.array.extend(midpoints, midpoint);
                offset = end;
            }
            return midpoints;
        };


        /**
         * @inheritDoc
         */
        ol.geom.MultiLineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
            var simplifiedFlatCoordinates = [];
            var simplifiedEnds = [];
            simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeuckers(
                this.flatCoordinates, 0, this.ends_, this.stride, squaredTolerance,
                simplifiedFlatCoordinates, 0, simplifiedEnds);
            var simplifiedMultiLineString = new ol.geom.MultiLineString(null);
            simplifiedMultiLineString.setFlatCoordinates(
                ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
            return simplifiedMultiLineString;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.MultiLineString.prototype.getType = function() {
            return ol.geom.GeometryType.MULTI_LINE_STRING;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.MultiLineString.prototype.intersectsExtent = function(extent) {
            return ol.geom.flat.intersectsextent.lineStrings(
                this.flatCoordinates, 0, this.ends_, this.stride, extent);
        };


        /**
         * Set the coordinates of the multilinestring.
         * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @override
         * @api
         */
        ol.geom.MultiLineString.prototype.setCoordinates = function(coordinates, opt_layout) {
            if (!coordinates) {
                this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.ends_);
            } else {
                this.setLayout(opt_layout, coordinates, 2);
                if (!this.flatCoordinates) {
                    this.flatCoordinates = [];
                }
                var ends = ol.geom.flat.deflate.coordinatess(
                    this.flatCoordinates, 0, coordinates, this.stride, this.ends_);
                this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
                this.changed();
            }
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {Array.<number>} ends Ends.
         */
        ol.geom.MultiLineString.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
            this.setFlatCoordinatesInternal(layout, flatCoordinates);
            this.ends_ = ends;
            this.changed();
        };


        /**
         * @param {Array.<ol.geom.LineString>} lineStrings LineStrings.
         */
        ol.geom.MultiLineString.prototype.setLineStrings = function(lineStrings) {
            var layout = this.getLayout();
            var flatCoordinates = [];
            var ends = [];
            var i, ii;
            for (i = 0, ii = lineStrings.length; i < ii; ++i) {
                var lineString = lineStrings[i];
                if (i === 0) {
                    layout = lineString.getLayout();
                }
                ol.array.extend(flatCoordinates, lineString.getFlatCoordinates());
                ends.push(flatCoordinates.length);
            }
            this.setFlatCoordinates(layout, flatCoordinates, ends);
        };

        /**
         * @classdesc
         * Multi-point geometry.
         *
         * @constructor
         * @extends {ol.geom.SimpleGeometry}
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @api
         */
        ol.geom.MultiPoint = function(coordinates, opt_layout) {
            ol.geom.SimpleGeometry.call(this);
            this.setCoordinates(coordinates, opt_layout);
        };
        ol.inherits(ol.geom.MultiPoint, ol.geom.SimpleGeometry);


        /**
         * Append the passed point to this multipoint.
         * @param {ol.geom.Point} point Point.
         * @api
         */
        ol.geom.MultiPoint.prototype.appendPoint = function(point) {
            if (!this.flatCoordinates) {
                this.flatCoordinates = point.getFlatCoordinates().slice();
            } else {
                ol.array.extend(this.flatCoordinates, point.getFlatCoordinates());
            }
            this.changed();
        };


        /**
         * Make a complete copy of the geometry.
         * @return {!ol.geom.MultiPoint} Clone.
         * @override
         * @api
         */
        ol.geom.MultiPoint.prototype.clone = function() {
            var multiPoint = new ol.geom.MultiPoint(null);
            multiPoint.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
            return multiPoint;
        };


        /**
         * @inheritDoc
         */
        ol.geom.MultiPoint.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
            if (minSquaredDistance <
                ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
                return minSquaredDistance;
            }
            var flatCoordinates = this.flatCoordinates;
            var stride = this.stride;
            var i, ii, j;
            for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
                var squaredDistance = ol.math.squaredDistance(
                    x, y, flatCoordinates[i], flatCoordinates[i + 1]);
                if (squaredDistance < minSquaredDistance) {
                    minSquaredDistance = squaredDistance;
                    for (j = 0; j < stride; ++j) {
                        closestPoint[j] = flatCoordinates[i + j];
                    }
                    closestPoint.length = stride;
                }
            }
            return minSquaredDistance;
        };


        /**
         * Return the coordinates of the multipoint.
         * @return {Array.<ol.Coordinate>} Coordinates.
         * @override
         * @api
         */
        ol.geom.MultiPoint.prototype.getCoordinates = function() {
            return ol.geom.flat.inflate.coordinates(
                this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
        };


        /**
         * Return the point at the specified index.
         * @param {number} index Index.
         * @return {ol.geom.Point} Point.
         * @api
         */
        ol.geom.MultiPoint.prototype.getPoint = function(index) {
            var n = !this.flatCoordinates ?
                0 : this.flatCoordinates.length / this.stride;
            if (index < 0 || n <= index) {
                return null;
            }
            var point = new ol.geom.Point(null);
            point.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
                index * this.stride, (index + 1) * this.stride));
            return point;
        };


        /**
         * Return the points of this multipoint.
         * @return {Array.<ol.geom.Point>} Points.
         * @api
         */
        ol.geom.MultiPoint.prototype.getPoints = function() {
            var flatCoordinates = this.flatCoordinates;
            var layout = this.layout;
            var stride = this.stride;
            /** @type {Array.<ol.geom.Point>} */
            var points = [];
            var i, ii;
            for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
                var point = new ol.geom.Point(null);
                point.setFlatCoordinates(layout, flatCoordinates.slice(i, i + stride));
                points.push(point);
            }
            return points;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.MultiPoint.prototype.getType = function() {
            return ol.geom.GeometryType.MULTI_POINT;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.MultiPoint.prototype.intersectsExtent = function(extent) {
            var flatCoordinates = this.flatCoordinates;
            var stride = this.stride;
            var i, ii, x, y;
            for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
                x = flatCoordinates[i];
                y = flatCoordinates[i + 1];
                if (ol.extent.containsXY(extent, x, y)) {
                    return true;
                }
            }
            return false;
        };


        /**
         * Set the coordinates of the multipoint.
         * @param {Array.<ol.Coordinate>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @override
         * @api
         */
        ol.geom.MultiPoint.prototype.setCoordinates = function(coordinates, opt_layout) {
            if (!coordinates) {
                this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
            } else {
                this.setLayout(opt_layout, coordinates, 1);
                if (!this.flatCoordinates) {
                    this.flatCoordinates = [];
                }
                this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
                    this.flatCoordinates, 0, coordinates, this.stride);
                this.changed();
            }
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         */
        ol.geom.MultiPoint.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
            this.setFlatCoordinatesInternal(layout, flatCoordinates);
            this.changed();
        };

        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<Array.<number>>} endss Endss.
         * @param {number} stride Stride.
         * @return {Array.<number>} Flat centers.
         */
        ol.geom.flat.center.linearRingss = {};

        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<Array.<number>>} endss Endss.
         * @param {number} stride Stride.
         * @return {Array.<number>} Flat centers.
         */
        ol.geom.flat.center.linearRingss = function(flatCoordinates, offset, endss, stride) {
            var flatCenters = [];
            var i, ii;
            var extent = ol.extent.createEmpty();
            for (i = 0, ii = endss.length; i < ii; ++i) {
                var ends = endss[i];
                extent = ol.extent.createOrUpdateFromFlatCoordinates(
                    flatCoordinates, offset, ends[0], stride);
                flatCenters.push((extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2);
                offset = ends[ends.length - 1];
            }
            return flatCenters;
        };

        /**
         * @classdesc
         * Multi-polygon geometry.
         *
         * @constructor
         * @extends {ol.geom.SimpleGeometry}
         * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @api
         */
        ol.geom.MultiPolygon = function(coordinates, opt_layout) {

            ol.geom.SimpleGeometry.call(this);

            /**
             * @type {Array.<Array.<number>>}
             * @private
             */
            this.endss_ = [];

            /**
             * @private
             * @type {number}
             */
            this.flatInteriorPointsRevision_ = -1;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.flatInteriorPoints_ = null;

            /**
             * @private
             * @type {number}
             */
            this.maxDelta_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.maxDeltaRevision_ = -1;

            /**
             * @private
             * @type {number}
             */
            this.orientedRevision_ = -1;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.orientedFlatCoordinates_ = null;

            this.setCoordinates(coordinates, opt_layout);

        };
        ol.inherits(ol.geom.MultiPolygon, ol.geom.SimpleGeometry);


        /**
         * Append the passed polygon to this multipolygon.
         * @param {ol.geom.Polygon} polygon Polygon.
         * @api
         */
        ol.geom.MultiPolygon.prototype.appendPolygon = function(polygon) {
            /** @type {Array.<number>} */
            var ends;
            if (!this.flatCoordinates) {
                this.flatCoordinates = polygon.getFlatCoordinates().slice();
                ends = polygon.getEnds().slice();
                this.endss_.push();
            } else {
                var offset = this.flatCoordinates.length;
                ol.array.extend(this.flatCoordinates, polygon.getFlatCoordinates());
                ends = polygon.getEnds().slice();
                var i, ii;
                for (i = 0, ii = ends.length; i < ii; ++i) {
                    ends[i] += offset;
                }
            }
            this.endss_.push(ends);
            this.changed();
        };


        /**
         * Make a complete copy of the geometry.
         * @return {!ol.geom.MultiPolygon} Clone.
         * @override
         * @api
         */
        ol.geom.MultiPolygon.prototype.clone = function() {
            var multiPolygon = new ol.geom.MultiPolygon(null);

            var len = this.endss_.length;
            var newEndss = new Array(len);
            for (var i = 0; i < len; ++i) {
                newEndss[i] = this.endss_[i].slice();
            }

            multiPolygon.setFlatCoordinates(
                this.layout, this.flatCoordinates.slice(), newEndss);
            return multiPolygon;
        };


        /**
         * @inheritDoc
         */
        ol.geom.MultiPolygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
            if (minSquaredDistance <
                ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
                return minSquaredDistance;
            }
            if (this.maxDeltaRevision_ != this.getRevision()) {
                this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getssMaxSquaredDelta(
                    this.flatCoordinates, 0, this.endss_, this.stride, 0));
                this.maxDeltaRevision_ = this.getRevision();
            }
            return ol.geom.flat.closest.getssClosestPoint(
                this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
                this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
        };


        /**
         * @inheritDoc
         */
        ol.geom.MultiPolygon.prototype.containsXY = function(x, y) {
            return ol.geom.flat.contains.linearRingssContainsXY(
                this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, x, y);
        };


        /**
         * Return the area of the multipolygon on projected plane.
         * @return {number} Area (on projected plane).
         * @api
         */
        ol.geom.MultiPolygon.prototype.getArea = function() {
            return ol.geom.flat.area.linearRingss(
                this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride);
        };


        /**
         * Get the coordinate array for this geometry.  This array has the structure
         * of a GeoJSON coordinate array for multi-polygons.
         *
         * @param {boolean=} opt_right Orient coordinates according to the right-hand
         *     rule (counter-clockwise for exterior and clockwise for interior rings).
         *     If `false`, coordinates will be oriented according to the left-hand rule
         *     (clockwise for exterior and counter-clockwise for interior rings).
         *     By default, coordinate orientation will depend on how the geometry was
         *     constructed.
         * @return {Array.<Array.<Array.<ol.Coordinate>>>} Coordinates.
         * @override
         * @api
         */
        ol.geom.MultiPolygon.prototype.getCoordinates = function(opt_right) {
            var flatCoordinates;
            if (opt_right !== undefined) {
                flatCoordinates = this.getOrientedFlatCoordinates().slice();
                ol.geom.flat.orient.orientLinearRingss(
                    flatCoordinates, 0, this.endss_, this.stride, opt_right);
            } else {
                flatCoordinates = this.flatCoordinates;
            }

            return ol.geom.flat.inflate.coordinatesss(
                flatCoordinates, 0, this.endss_, this.stride);
        };


        /**
         * @return {Array.<Array.<number>>} Endss.
         */
        ol.geom.MultiPolygon.prototype.getEndss = function() {
            return this.endss_;
        };


        /**
         * @return {Array.<number>} Flat interior points.
         */
        ol.geom.MultiPolygon.prototype.getFlatInteriorPoints = function() {
            if (this.flatInteriorPointsRevision_ != this.getRevision()) {
                var flatCenters = ol.geom.flat.center.linearRingss(
                    this.flatCoordinates, 0, this.endss_, this.stride);
                this.flatInteriorPoints_ = ol.geom.flat.interiorpoint.linearRingss(
                    this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
                    flatCenters);
                this.flatInteriorPointsRevision_ = this.getRevision();
            }
            return this.flatInteriorPoints_;
        };


        /**
         * Return the interior points as {@link ol.geom.MultiPoint multipoint}.
         * @return {ol.geom.MultiPoint} Interior points as XYM coordinates, where M is
         * the length of the horizontal intersection that the point belongs to.
         * @api
         */
        ol.geom.MultiPolygon.prototype.getInteriorPoints = function() {
            var interiorPoints = new ol.geom.MultiPoint(null);
            interiorPoints.setFlatCoordinates(ol.geom.GeometryLayout.XYM,
                this.getFlatInteriorPoints().slice());
            return interiorPoints;
        };


        /**
         * @return {Array.<number>} Oriented flat coordinates.
         */
        ol.geom.MultiPolygon.prototype.getOrientedFlatCoordinates = function() {
            if (this.orientedRevision_ != this.getRevision()) {
                var flatCoordinates = this.flatCoordinates;
                if (ol.geom.flat.orient.linearRingssAreOriented(
                    flatCoordinates, 0, this.endss_, this.stride)) {
                    this.orientedFlatCoordinates_ = flatCoordinates;
                } else {
                    this.orientedFlatCoordinates_ = flatCoordinates.slice();
                    this.orientedFlatCoordinates_.length =
                        ol.geom.flat.orient.orientLinearRingss(
                            this.orientedFlatCoordinates_, 0, this.endss_, this.stride);
                }
                this.orientedRevision_ = this.getRevision();
            }
            return this.orientedFlatCoordinates_;
        };


        /**
         * @inheritDoc
         */
        ol.geom.MultiPolygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
            var simplifiedFlatCoordinates = [];
            var simplifiedEndss = [];
            simplifiedFlatCoordinates.length = ol.geom.flat.simplify.quantizess(
                this.flatCoordinates, 0, this.endss_, this.stride,
                Math.sqrt(squaredTolerance),
                simplifiedFlatCoordinates, 0, simplifiedEndss);
            var simplifiedMultiPolygon = new ol.geom.MultiPolygon(null);
            simplifiedMultiPolygon.setFlatCoordinates(
                ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEndss);
            return simplifiedMultiPolygon;
        };


        /**
         * Return the polygon at the specified index.
         * @param {number} index Index.
         * @return {ol.geom.Polygon} Polygon.
         * @api
         */
        ol.geom.MultiPolygon.prototype.getPolygon = function(index) {
            if (index < 0 || this.endss_.length <= index) {
                return null;
            }
            var offset;
            if (index === 0) {
                offset = 0;
            } else {
                var prevEnds = this.endss_[index - 1];
                offset = prevEnds[prevEnds.length - 1];
            }
            var ends = this.endss_[index].slice();
            var end = ends[ends.length - 1];
            if (offset !== 0) {
                var i, ii;
                for (i = 0, ii = ends.length; i < ii; ++i) {
                    ends[i] -= offset;
                }
            }
            var polygon = new ol.geom.Polygon(null);
            polygon.setFlatCoordinates(
                this.layout, this.flatCoordinates.slice(offset, end), ends);
            return polygon;
        };


        /**
         * Return the polygons of this multipolygon.
         * @return {Array.<ol.geom.Polygon>} Polygons.
         * @api
         */
        ol.geom.MultiPolygon.prototype.getPolygons = function() {
            var layout = this.layout;
            var flatCoordinates = this.flatCoordinates;
            var endss = this.endss_;
            var polygons = [];
            var offset = 0;
            var i, ii, j, jj;
            for (i = 0, ii = endss.length; i < ii; ++i) {
                var ends = endss[i].slice();
                var end = ends[ends.length - 1];
                if (offset !== 0) {
                    for (j = 0, jj = ends.length; j < jj; ++j) {
                        ends[j] -= offset;
                    }
                }
                var polygon = new ol.geom.Polygon(null);
                polygon.setFlatCoordinates(
                    layout, flatCoordinates.slice(offset, end), ends);
                polygons.push(polygon);
                offset = end;
            }
            return polygons;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.MultiPolygon.prototype.getType = function() {
            return ol.geom.GeometryType.MULTI_POLYGON;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.geom.MultiPolygon.prototype.intersectsExtent = function(extent) {
            return ol.geom.flat.intersectsextent.linearRingss(
                this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, extent);
        };


        /**
         * Set the coordinates of the multipolygon.
         * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinates Coordinates.
         * @param {ol.geom.GeometryLayout=} opt_layout Layout.
         * @override
         * @api
         */
        ol.geom.MultiPolygon.prototype.setCoordinates = function(coordinates, opt_layout) {
            if (!coordinates) {
                this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.endss_);
            } else {
                this.setLayout(opt_layout, coordinates, 3);
                if (!this.flatCoordinates) {
                    this.flatCoordinates = [];
                }
                var endss = ol.geom.flat.deflate.coordinatesss(
                    this.flatCoordinates, 0, coordinates, this.stride, this.endss_);
                if (endss.length === 0) {
                    this.flatCoordinates.length = 0;
                } else {
                    var lastEnds = endss[endss.length - 1];
                    this.flatCoordinates.length = lastEnds.length === 0 ?
                        0 : lastEnds[lastEnds.length - 1];
                }
                this.changed();
            }
        };


        /**
         * @param {ol.geom.GeometryLayout} layout Layout.
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {Array.<Array.<number>>} endss Endss.
         */
        ol.geom.MultiPolygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, endss) {
            this.setFlatCoordinatesInternal(layout, flatCoordinates);
            this.endss_ = endss;
            this.changed();
        };


        /**
         * @param {Array.<ol.geom.Polygon>} polygons Polygons.
         */
        ol.geom.MultiPolygon.prototype.setPolygons = function(polygons) {
            var layout = this.getLayout();
            var flatCoordinates = [];
            var endss = [];
            var i, ii, ends;
            for (i = 0, ii = polygons.length; i < ii; ++i) {
                var polygon = polygons[i];
                if (i === 0) {
                    layout = polygon.getLayout();
                }
                var offset = flatCoordinates.length;
                ends = polygon.getEnds();
                var j, jj;
                for (j = 0, jj = ends.length; j < jj; ++j) {
                    ends[j] += offset;
                }
                ol.array.extend(flatCoordinates, polygon.getFlatCoordinates());
                endss.push(ends);
            }
            this.setFlatCoordinates(layout, flatCoordinates, endss);
        };


        ol.Feature = function(opt_geometryOrProperties) {

            ol.Object.call(this);

            /**
             * @private
             * @type {number|string|undefined}
             */
            this.id_ = undefined;

            /**
             * @type {string}
             * @private
             */
            this.geometryName_ = 'geometry';

            /**
             * User provided style.
             * @private
             * @type {ol.style.Style|Array.<ol.style.Style>|
       *     ol.FeatureStyleFunction}
             */
            this.style_ = null;

            /**
             * @private
             * @type {ol.FeatureStyleFunction|undefined}
             */
            this.styleFunction_ = undefined;

            /**
             * @private
             * @type {?ol.EventsKey}
             */
            this.geometryChangeKey_ = null;

    //        ol.events.listen(
    //            this, ol.Object.getChangeEventType(this.geometryName_),
    //            this.handleGeometryChanged_, this);

            if (opt_geometryOrProperties !== undefined) {
                if (opt_geometryOrProperties instanceof ol.geom.Geometry ||
                    !opt_geometryOrProperties) {
                    var geometry = opt_geometryOrProperties;
                    this.setGeometry(geometry);
                } else {
                    /** @type {Object.<string, *>} */
                    var properties = opt_geometryOrProperties;
                    this.setProperties(properties);
                }
            }
        };
        ol.inherits(ol.Feature, ol.Object);


        /**
         * Clone this feature. If the original feature has a geometry it
         * is also cloned. The feature id is not set in the clone.
         * @return {ol.Feature} The clone.
         * @api
         */
        ol.Feature.prototype.clone = function() {
            var clone = new ol.Feature(this.getProperties());
            clone.setGeometryName(this.getGeometryName());
            var geometry = this.getGeometry();
            if (geometry) {
                clone.setGeometry(geometry.clone());
            }
            var style = this.getStyle();
            if (style) {
                clone.setStyle(style);
            }
            return clone;
        };


        /**
         * Get the feature's default geometry.  A feature may have any number of named
         * geometries.  The "default" geometry (the one that is rendered by default) is
         * set when calling {@link ol.Feature#setGeometry}.
         * @return {ol.geom.Geometry|undefined} The default geometry for the feature.
         * @api
         * @observable
         */
        ol.Feature.prototype.getGeometry = function() {
            return /** @type {ol.geom.Geometry|undefined} */ (
                this.get(this.geometryName_));
        };


        /**
         * Get the feature identifier.  This is a stable identifier for the feature and
         * is either set when reading data from a remote source or set explicitly by
         * calling {@link ol.Feature#setId}.
         * @return {number|string|undefined} Id.
         * @api
         */
        ol.Feature.prototype.getId = function() {
            return this.id_;
        };


        /**
         * Get the name of the feature's default geometry.  By default, the default
         * geometry is named `geometry`.
         * @return {string} Get the property name associated with the default geometry
         *     for this feature.
         * @api
         */
        ol.Feature.prototype.getGeometryName = function() {
            return this.geometryName_;
        };


        /**
         * Get the feature's style. Will return what was provided to the
         * {@link ol.Feature#setStyle} method.
         * @return {ol.style.Style|Array.<ol.style.Style>|
     *     ol.FeatureStyleFunction|ol.StyleFunction} The feature style.
         * @api
         */
        ol.Feature.prototype.getStyle = function() {
            return this.style_;
        };


        /**
         * Get the feature's style function.
         * @return {ol.FeatureStyleFunction|undefined} Return a function
         * representing the current style of this feature.
         * @api
         */
        ol.Feature.prototype.getStyleFunction = function() {
            return this.styleFunction_;
        };


        /**
         * @private
         */
        ol.Feature.prototype.handleGeometryChange_ = function() {
            this.changed();
        };


        /**
         * @private
         */
        ol.Feature.prototype.handleGeometryChanged_ = function() {
            if (this.geometryChangeKey_) {
                ol.events.unlistenByKey(this.geometryChangeKey_);
                this.geometryChangeKey_ = null;
            }
            var geometry = this.getGeometry();
            if (geometry) {
                this.geometryChangeKey_ = ol.events.listen(geometry,
                    ol.events.EventType.CHANGE, this.handleGeometryChange_, this);
            }
            this.changed();
        };


        /**
         * Set the default geometry for the feature.  This will update the property
         * with the name returned by {@link ol.Feature#getGeometryName}.
         * @param {ol.geom.Geometry|undefined} geometry The new geometry.
         * @api
         * @observable
         */
        ol.Feature.prototype.setGeometry = function(geometry) {
            this.set(this.geometryName_, geometry);
        };


        /**
         * Set the style for the feature.  This can be a single style object, an array
         * of styles, or a function that takes a resolution and returns an array of
         * styles. If it is `null` the feature has no style (a `null` style).
         * @param {ol.style.Style|Array.<ol.style.Style>|
     *     ol.FeatureStyleFunction|ol.StyleFunction} style Style for this feature.
         * @api
         * @fires ol.events.Event#event:change
         */
        ol.Feature.prototype.setStyle = function(style) {
            this.style_ = style;
            this.styleFunction_ = !style ?
                undefined : ol.Feature.createStyleFunction(style);
            this.changed();
        };


        /**
         * Set the feature id.  The feature id is considered stable and may be used when
         * requesting features or comparing identifiers returned from a remote source.
         * The feature id can be used with the {@link ol.source.Vector#getFeatureById}
         * method.
         * @param {number|string|undefined} id The feature id.
         * @api
         * @fires ol.events.Event#event:change
         */
        ol.Feature.prototype.setId = function(id) {
            this.id_ = id;
            this.changed();
        };


        /**
         * Set the property name to be used when getting the feature's default geometry.
         * When calling {@link ol.Feature#getGeometry}, the value of the property with
         * this name will be returned.
         * @param {string} name The property name of the default geometry.
         * @api
         */
        ol.Feature.prototype.setGeometryName = function(name) {
            ol.events.unlisten(
                this, ol.Object.getChangeEventType(this.geometryName_),
                this.handleGeometryChanged_, this);
            this.geometryName_ = name;
            ol.events.listen(
                this, ol.Object.getChangeEventType(this.geometryName_),
                this.handleGeometryChanged_, this);
            this.handleGeometryChanged_();
        };


        /**
         * Convert the provided object into a feature style function.  Functions passed
         * through unchanged.  Arrays of ol.style.Style or single style objects wrapped
         * in a new feature style function.
         * @param {ol.FeatureStyleFunction|!Array.<ol.style.Style>|!ol.style.Style} obj
         *     A feature style function, a single style, or an array of styles.
         * @return {ol.FeatureStyleFunction} A style function.
         */
        ol.Feature.createStyleFunction = function(obj) {
            var styleFunction;

            if (typeof obj === 'function') {
                if (obj.length == 2) {
                    styleFunction = function(resolution) {
                        return /** @type {ol.StyleFunction} */ (obj)(this, resolution);
                    };
                } else {
                    styleFunction = obj;
                }
            } else {
                /**
                 * @type {Array.<ol.style.Style>}
                 */
                var styles;
                if (Array.isArray(obj)) {
                    styles = obj;
                } else {
                    ol.asserts.assert(obj instanceof ol.style.Style,
                        41); // Expected an `ol.style.Style` or an array of `ol.style.Style`
                    styles = [obj];
                }
                styleFunction = function() {
                    return styles;
                };
            }
            return styleFunction;
        };

        ol.format = {};

        ol.format.Feature = function() {

            /**
             * @protected
             * @type {ol.proj.Projection}
             */
            this.defaultDataProjection = null;

            /**
             * @protected
             * @type {ol.proj.Projection}
             */
            this.defaultFeatureProjection = null;

        };


        /**
         * Adds the data projection to the read options.
         * @param {Document|Node|Object|string} source Source.
         * @param {olx.format.ReadOptions=} opt_options Options.
         * @return {olx.format.ReadOptions|undefined} Options.
         * @protected
         */
        ol.format.Feature.prototype.getReadOptions = function(source, opt_options) {
            var options;
            if (opt_options) {
                options = {
                    dataProjection: opt_options.dataProjection ?
                        opt_options.dataProjection : this.readProjection(source),
                    featureProjection: opt_options.featureProjection
                };
            }
            return this.adaptOptions(options);
        };


        /**
         * Sets the `defaultDataProjection` on the options, if no `dataProjection`
         * is set.
         * @param {olx.format.WriteOptions|olx.format.ReadOptions|undefined} options
         *     Options.
         * @protected
         * @return {olx.format.WriteOptions|olx.format.ReadOptions|undefined}
         *     Updated options.
         */
        ol.format.Feature.prototype.adaptOptions = function(options) {
            return ol.obj.assign({
                dataProjection: this.defaultDataProjection,
                featureProjection: this.defaultFeatureProjection
            }, options);
        };


        /**
         * Get the extent from the source of the last {@link readFeatures} call.
         * @return {ol.Extent} Tile extent.
         */
        ol.format.Feature.prototype.getLastExtent = function() {
            return null;
        };


        /**
         * @abstract
         * @return {ol.format.FormatType} Format.
         */
        ol.format.Feature.prototype.getType = function() {};


        /**
         * Read a single feature from a source.
         *
         * @abstract
         * @param {Document|Node|Object|string} source Source.
         * @param {olx.format.ReadOptions=} opt_options Read options.
         * @return {ol.Feature} Feature.
         */
        ol.format.Feature.prototype.readFeature = function(source, opt_options) {};


        /**
         * Read all features from a source.
         *
         * @abstract
         * @param {Document|Node|ArrayBuffer|Object|string} source Source.
         * @param {olx.format.ReadOptions=} opt_options Read options.
         * @return {Array.<ol.Feature>} Features.
         */
        ol.format.Feature.prototype.readFeatures = function(source, opt_options) {};


        /**
         * Read a single geometry from a source.
         *
         * @abstract
         * @param {Document|Node|Object|string} source Source.
         * @param {olx.format.ReadOptions=} opt_options Read options.
         * @return {ol.geom.Geometry} Geometry.
         */
        ol.format.Feature.prototype.readGeometry = function(source, opt_options) {};


        /**
         * Read the projection from a source.
         *
         * @abstract
         * @param {Document|Node|Object|string} source Source.
         * @return {ol.proj.Projection} Projection.
         */
        ol.format.Feature.prototype.readProjection = function(source) {};


        /**
         * Encode a feature in this format.
         *
         * @abstract
         * @param {ol.Feature} feature Feature.
         * @param {olx.format.WriteOptions=} opt_options Write options.
         * @return {string} Result.
         */
        ol.format.Feature.prototype.writeFeature = function(feature, opt_options) {};


        /**
         * Encode an array of features in this format.
         *
         * @abstract
         * @param {Array.<ol.Feature>} features Features.
         * @param {olx.format.WriteOptions=} opt_options Write options.
         * @return {string} Result.
         */
        ol.format.Feature.prototype.writeFeatures = function(features, opt_options) {};


        /**
         * Write a single geometry in this format.
         *
         * @abstract
         * @param {ol.geom.Geometry} geometry Geometry.
         * @param {olx.format.WriteOptions=} opt_options Write options.
         * @return {string} Result.
         */
        ol.format.Feature.prototype.writeGeometry = function(geometry, opt_options) {};


        /**
         * @param {ol.geom.Geometry|ol.Extent} geometry Geometry.
         * @param {boolean} write Set to true for writing, false for reading.
         * @param {(olx.format.WriteOptions|olx.format.ReadOptions)=} opt_options
         *     Options.
         * @return {ol.geom.Geometry|ol.Extent} Transformed geometry.
         * @protected
         */
        ol.format.Feature.transformWithOptions = function(
            geometry, write, opt_options) {

            /**
             * @type {ol.geom.Geometry|ol.Extent}
             */
            var transformed;
            {
                transformed = geometry;
            }
            if (write && opt_options && opt_options.decimals !== undefined) {
                var power = Math.pow(10, opt_options.decimals);
                // if decimals option on write, round each coordinate appropriately
                /**
                 * @param {Array.<number>} coordinates Coordinates.
                 * @return {Array.<number>} Transformed coordinates.
                 */
                var transform = function(coordinates) {
                    for (var i = 0, ii = coordinates.length; i < ii; ++i) {
                        coordinates[i] = Math.round(coordinates[i] * power) / power;
                    }
                    return coordinates;
                };
                if (transformed === geometry) {
                    transformed = transformed.clone();
                }
                transformed.applyTransform(transform);
            }
            return transformed;
        };


        ol.format.MVT = function(opt_options) {

            ol.format.Feature.call(this);

            var options = opt_options ? opt_options : {};

            /**
             * @type {ol.proj.Projection}
             */
    //        this.defaultDataProjection = new ol.proj.Projection({
    //            code: 'EPSG:3857',
    //            units: ol.proj.Units.TILE_PIXELS
    //        });

            /**
             * @private
             * @type {function((ol.geom.Geometry|Object.<string,*>)=)|
       *     function(ol.geom.GeometryType,Array.<number>,
       *         (Array.<number>|Array.<Array.<number>>),Object.<string,*>,number)}
             */
            this.featureClass_ = options.featureClass ?
                options.featureClass : ol.render.Feature;

            /**
             * @private
             * @type {string|undefined}
             */
            this.geometryName_ = options.geometryName;

            /**
             * @private
             * @type {string}
             */
            this.layerName_ = options.layerName ? options.layerName : 'layer';

            /**
             * @private
             * @type {Array.<string>}
             */
            this.layers_ = options.layers ? options.layers : null;

            /**
             * @private
             * @type {ol.Extent}
             */
            this.extent_ = null;

        };
        ol.inherits(ol.format.MVT, ol.format.Feature);

        ol.format.MVT.pbfReaders_ = {
            layers: function (tag, layers, pbf) {
                if (tag === 3) {
                    var layer = {
                        keys: [],
                        values: [],
                        features: []
                    };
                    var end = pbf.readVarint() + pbf.pos;
                    pbf.readFields(ol.format.MVT.pbfReaders_.layer, layer, end);
                    layer.length = layer.features.length;
                    if (layer.length) {
                        layers[layer.name] = layer;
                    }
                }
            },
            layer: function (tag, layer, pbf) {
                if (tag === 15) {
                    layer.version = pbf.readVarint();
                } else if (tag === 1) {
                    layer.name = pbf.readString();
                } else if (tag === 5) {
                    layer.extent = pbf.readVarint();
                } else if (tag === 2) {
                    layer.features.push(pbf.pos);
                } else if (tag === 3) {
                    layer.keys.push(pbf.readString());
                } else if (tag === 4) {
                    var value = null;
                    var end = pbf.readVarint() + pbf.pos;
                    while (pbf.pos < end) {
                        tag = pbf.readVarint() >> 3;
                        value = tag === 1 ? pbf.readString() :
                            tag === 2 ? pbf.readFloat() :
                                tag === 3 ? pbf.readDouble() :
                                    tag === 4 ? pbf.readVarint64() :
                                        tag === 5 ? pbf.readVarint() :
                                            tag === 6 ? pbf.readSVarint() :
                                                tag === 7 ? pbf.readBoolean() : null;
                    }
                    layer.values.push(value);
                }
            },
            feature: function (tag, feature, pbf) {
                if (tag == 1) {
                    feature.id = pbf.readVarint();
                } else if (tag == 2) {
                    var end = pbf.readVarint() + pbf.pos;
                    while (pbf.pos < end) {
                        var key = feature.layer.keys[pbf.readVarint()];
                        var value = feature.layer.values[pbf.readVarint()];
                        feature.properties[key] = value;
                    }
                } else if (tag == 3) {
                    feature.type = pbf.readVarint();
                } else if (tag == 4) {
                    feature.geometry = pbf.pos;
                }
            }
        };

        /**
         * Read a raw feature from the pbf offset stored at index `i` in the raw layer.
         * @suppress {missingProperties}
         * @private
         * @param {ol.ext.PBF} pbf PBF.
         * @param {Object} layer Raw layer.
         * @param {number} i Index of the feature in the raw layer's `features` array.
         * @return {Object} Raw feature.
         */
        ol.format.MVT.readRawFeature_ = function (pbf, layer, i) {
            pbf.pos = layer.features[i];
            var end = pbf.readVarint() + pbf.pos;

            var feature = {
                layer: layer,
                type: 0,
                properties: {}
            };
            pbf.readFields(ol.format.MVT.pbfReaders_.feature, feature, end);
            return feature;
        };

        /**
         * Read the raw geometry from the pbf offset stored in a raw feature's geometry
         * proeprty.
         * @suppress {missingProperties}
         * @private
         * @param {ol.ext.PBF} pbf PBF.
         * @param {Object} feature Raw feature.
         * @param {Array.<number>} flatCoordinates Array to store flat coordinates in.
         * @param {Array.<number>} ends Array to store ends in.
         */
        ol.format.MVT.readRawGeometry_ = function (pbf, feature, flatCoordinates, ends) {
            pbf.pos = feature.geometry;

            var end = pbf.readVarint() + pbf.pos;
            var cmd = 1;
            var length = 0;
            var x = 0;
            var y = 0;
            var coordsLen = 0;
            var currentEnd = 0;

            while (pbf.pos < end) {
                if (!length) {
                    var cmdLen = pbf.readVarint();
                    cmd = cmdLen & 0x7;
                    length = cmdLen >> 3;
                }

                length--;

                if (cmd === 1 || cmd === 2) {
                    x += pbf.readSVarint();
                    y += pbf.readSVarint();

                    if (cmd === 1) { // moveTo
                        if (coordsLen > currentEnd) {
                            ends.push(coordsLen);
                            currentEnd = coordsLen;
                        }
                    }

                    flatCoordinates.push(x, y);
                    coordsLen += 2;

                } else if (cmd === 7) {

                    if (coordsLen > currentEnd) {
                        // close polygon
                        flatCoordinates.push(
                            flatCoordinates[currentEnd], flatCoordinates[currentEnd + 1]);
                        coordsLen += 2;
                    }

                } else {
                    ol.asserts.assert(false, 59); // Invalid command found in the PBF
                }
            }

            if (coordsLen > currentEnd) {
                ends.push(coordsLen);
                currentEnd = coordsLen;
            }

        };

        /**
         * @suppress {missingProperties}
         * @private
         * @param {number} type The raw feature's geometry type
         * @param {number} numEnds Number of ends of the flat coordinates of the
         * geometry.
         * @return {ol.geom.GeometryType} The geometry type.
         */
        ol.format.MVT.getGeometryType_ = function (type, numEnds) {
            /** @type {ol.geom.GeometryType} */
            var geometryType;
            if (type === 1) {
                geometryType = numEnds === 1 ?
                    ol.geom.GeometryType.POINT : ol.geom.GeometryType.MULTI_POINT;
            } else if (type === 2) {
                geometryType = numEnds === 1 ?
                    ol.geom.GeometryType.LINE_STRING :
                    ol.geom.GeometryType.MULTI_LINE_STRING;
            } else if (type === 3) {
                geometryType = ol.geom.GeometryType.POLYGON;
                // MultiPolygon not relevant for rendering - winding order determines
                // outer rings of polygons.
            }
            return geometryType;
        };

        /**
         * @private
         * @param {ol.ext.PBF} pbf PBF
         * @param {Object} rawFeature Raw Mapbox feature.
         * @param {olx.format.ReadOptions=} opt_options Read options.
         * @return {ol.Feature|ol.render.Feature} Feature.
         */
        ol.format.MVT.prototype.createFeature_ = function (pbf, rawFeature, opt_options) {
            var type = rawFeature.type;
            if (type === 0) {
                return null;
            }

            var feature;
            var id = rawFeature.id;
            var values = rawFeature.properties;
            values[this.layerName_] = rawFeature.layer.name;

            var flatCoordinates = [];
            var ends = [];
            ol.format.MVT.readRawGeometry_(pbf, rawFeature, flatCoordinates, ends);

            var geometryType = ol.format.MVT.getGeometryType_(type, ends.length);

            var geom;
            if (geometryType == ol.geom.GeometryType.POLYGON) {
                var endss = [];
                var offset = 0;
                var prevEndIndex = 0;
                for (var i = 0, ii = ends.length; i < ii; ++i) {
                    var end = ends[i];
                    if (!ol.geom.flat.orient.linearRingIsClockwise(flatCoordinates, offset, end, 2)) {
                        endss.push(ends.slice(prevEndIndex, i + 1));
                        prevEndIndex = i + 1;
                    }
                    offset = end;
                }
                if (endss.length > 1) {
                    ends = endss;
                    geom = new ol.geom.MultiPolygon(null);
                } else {
                    geom = new ol.geom.Polygon(null);
                }
            } else {
                geom = geometryType === ol.geom.GeometryType.POINT ? new ol.geom.Point(null) :
                    geometryType === ol.geom.GeometryType.LINE_STRING ? new ol.geom.LineString(null) :
                        geometryType === ol.geom.GeometryType.POLYGON ? new ol.geom.Polygon(null) :
                            geometryType === ol.geom.GeometryType.MULTI_POINT ? new ol.geom.MultiPoint(null) :
                                geometryType === ol.geom.GeometryType.MULTI_LINE_STRING ? new ol.geom.MultiLineString(null) :
                                    null;
            }
            geom.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates, ends);
            feature = new this.featureClass_();
            if (this.geometryName_) {
                feature.setGeometryName(this.geometryName_);
            }
            var geometry = ol.format.Feature.transformWithOptions(geom, false, this.adaptOptions(opt_options));
            feature.setGeometry(geometry);
            feature.setId(id);
            feature.setProperties(values);

            return feature;
        };

        ol.format.MVT.prototype.readFeatures = function (source, opt_options) {
            var layers = this.layers_;

            var pbf$1 = new pbf.Protobuf(/** @type {ArrayBuffer} */ (source));
            var pbfLayers = pbf$1.readFields(ol.format.MVT.pbfReaders_.layers, {});
            /** @type {Array.<ol.Feature|ol.render.Feature>} */
            var features = [];
            var pbfLayer;
            for (var name in pbfLayers) {
                if (layers && layers.indexOf(name) == -1) {
                    continue;
                }

                if (opt_options !== undefined) {
                    var needSourceLayerNames = opt_options.needSourceLayerNames;
                    if (needSourceLayerNames !== undefined && needSourceLayerNames[name] === undefined) {
                        continue;
                    }
                }

                pbfLayer = pbfLayers[name];

                var rawFeature;
                for (var i = 0, ii = pbfLayer.length; i < ii; ++i) {
                    rawFeature = ol.format.MVT.readRawFeature_(pbf$1, pbfLayer, i);
                    features.push(this.createFeature_(pbf$1, rawFeature));
                }
                this.extent_ = pbfLayer ? [0, 0, pbfLayer.extent, pbfLayer.extent] : null;
            }

            return features;
        };

        ol.style = {};

        /**
         * Singleton class. Available through {@link ol.style.iconImageCache}.
         * @constructor
         */
        ol.style.IconImageCache = function() {
            this.cache_ = {};
            this.cacheSize_ = 0;
            this.maxCacheSize_ = 32;
        };

        ol.style.IconImageCache.getKey = function(src, crossOrigin, color) {
            var colorString = color ? ol.color.asString(color) : 'null';
            return crossOrigin + ':' + src + ':' + colorString;
        };


        /**
         * FIXME empty description for jsdoc
         */
        ol.style.IconImageCache.prototype.clear = function() {
            this.cache_ = {};
            this.cacheSize_ = 0;
        };


        /**
         * FIXME empty description for jsdoc
         */
        ol.style.IconImageCache.prototype.expire = function() {
            if (this.cacheSize_ > this.maxCacheSize_) {
                var i = 0;
                var key, iconImage;
                for (key in this.cache_) {
                    iconImage = this.cache_[key];
                    if ((i++ & 3) === 0 && !iconImage.hasListener()) {
                        delete this.cache_[key];
                        --this.cacheSize_;
                    }
                }
            }
        };

        ol.style.IconImageCache.prototype.get = function(src, crossOrigin, color) {
            var key = ol.style.IconImageCache.getKey(src, crossOrigin, color);
            return key in this.cache_ ? this.cache_[key] : null;
        };

        ol.style.IconImageCache.prototype.set = function(src, crossOrigin, color, iconImage) {
            var key = ol.style.IconImageCache.getKey(src, crossOrigin, color);
            this.cache_[key] = iconImage;
            ++this.cacheSize_;
        };

        ol.style.IconImageCache.prototype.setSize = function(maxCacheSize) {
            this.maxCacheSize_ = maxCacheSize;
            this.expire();
        };

        /**
         * The {@link ol.style.IconImageCache} for {@link ol.style.Icon} images.
         * @api
         */
        ol.style.iconImageCache = new ol.style.IconImageCache();

        ol.style.Image = function(options) {

            /**
             * @private
             * @type {number}
             */
            this.opacity_ = options.opacity;

            /**
             * @private
             * @type {boolean}
             */
            this.rotateWithView_ = options.rotateWithView;

            /**
             * @private
             * @type {number}
             */
            this.rotation_ = options.rotation;

            /**
             * @private
             * @type {number}
             */
            this.scale_ = options.scale;

            /**
             * @private
             * @type {boolean}
             */
            this.snapToPixel_ = options.snapToPixel;

        };

        ol.style.Image.prototype.getOpacity = function() {
            return this.opacity_;
        };

        ol.style.Image.prototype.getRotateWithView = function() {
            return this.rotateWithView_;
        };

        ol.style.Image.prototype.getRotation = function() {
            return this.rotation_;
        };

        ol.style.Image.prototype.getScale = function() {
            return this.scale_;
        };

        ol.style.Image.prototype.getSnapToPixel = function() {
            return this.snapToPixel_;
        };

        ol.style.Image.prototype.getAnchor = function() {};

        ol.style.Image.prototype.getImage = function(pixelRatio) {};

        ol.style.Image.prototype.getHitDetectionImage = function(pixelRatio) {};

        ol.style.Image.prototype.getImageState = function() {};

        ol.style.Image.prototype.getImageSize = function() {};

        ol.style.Image.prototype.getHitDetectionImageSize = function() {};

        ol.style.Image.prototype.getOrigin = function() {};

        ol.style.Image.prototype.getSize = function() {};

        ol.style.Image.prototype.setOpacity = function(opacity) {
            this.opacity_ = opacity;
        };

        ol.style.Image.prototype.setRotateWithView = function(rotateWithView) {
            this.rotateWithView_ = rotateWithView;
        };

        ol.style.Image.prototype.setRotation = function(rotation) {
            this.rotation_ = rotation;
        };

        ol.style.Image.prototype.setScale = function(scale) {
            this.scale_ = scale;
        };

        ol.style.Image.prototype.setSnapToPixel = function(snapToPixel) {
            this.snapToPixel_ = snapToPixel;
        };

        ol.style.Image.prototype.listenImageChange = function(listener, thisArg) {};

        ol.style.Image.prototype.load = function() {};

        ol.style.Image.prototype.unlistenImageChange = function(listener, thisArg) {};

        /**
         * @classdesc
         * Set regular shape style for vector features. The resulting shape will be
         * a regular polygon when `radius` is provided, or a star when `radius1` and
         * `radius2` are provided.
         *
         * @constructor
         * @param {olx.style.RegularShapeOptions} options Options.
         * @extends {ol.style.Image}
         * @api
         */
        ol.style.RegularShape = function(options) {
            /**
             * @private
             * @type {Array.<string>}
             */
            this.checksums_ = null;

            /**
             * @private
             * @type {HTMLCanvasElement}
             */
            this.canvas_ = null;

            /**
             * @private
             * @type {HTMLCanvasElement}
             */
            this.hitDetectionCanvas_ = null;

            /**
             * @private
             * @type {ol.style.Fill}
             */
            this.fill_ = options.fill !== undefined ? options.fill : null;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.origin_ = [0, 0];

            /**
             * @private
             * @type {number}
             */
            this.points_ = options.points;

            /**
             * @protected
             * @type {number}
             */
            this.radius_ = /** @type {number} */ (options.radius !== undefined ?
                options.radius : options.radius1);

            /**
             * @private
             * @type {number|undefined}
             */
            this.radius2_ = options.radius2;

            /**
             * @private
             * @type {number}
             */
            this.angle_ = options.angle !== undefined ? options.angle : 0;

            /**
             * @private
             * @type {ol.style.Stroke}
             */
            this.stroke_ = options.stroke !== undefined ? options.stroke : null;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.anchor_ = null;

            /**
             * @private
             * @type {ol.Size}
             */
            this.size_ = null;

            /**
             * @private
             * @type {ol.Size}
             */
            this.imageSize_ = null;

            /**
             * @private
             * @type {ol.Size}
             */
            this.hitDetectionImageSize_ = null;

            /**
             * @protected
             * @type {ol.style.AtlasManager|undefined}
             */
            this.atlasManager_ = options.atlasManager;

            this.render_(this.atlasManager_);

            /**
             * @type {boolean}
             */
            var snapToPixel = options.snapToPixel !== undefined ?
                options.snapToPixel : true;

            /**
             * @type {boolean}
             */
            var rotateWithView = options.rotateWithView !== undefined ?
                options.rotateWithView : false;

            ol.style.Image.call(this, {
                opacity: 1,
                rotateWithView: rotateWithView,
                rotation: options.rotation !== undefined ? options.rotation : 0,
                scale: 1,
                snapToPixel: snapToPixel
            });
        };
        ol.inherits(ol.style.RegularShape, ol.style.Image);

        ol.style.RegularShape.prototype.clone = function() {
            var style = new ol.style.RegularShape({
                fill: this.getFill() ? this.getFill().clone() : undefined,
                points: this.getPoints(),
                radius: this.getRadius(),
                radius2: this.getRadius2(),
                angle: this.getAngle(),
                snapToPixel: this.getSnapToPixel(),
                stroke: this.getStroke() ?  this.getStroke().clone() : undefined,
                rotation: this.getRotation(),
                rotateWithView: this.getRotateWithView(),
                atlasManager: this.atlasManager_
            });
            style.setOpacity(this.getOpacity());
            style.setScale(this.getScale());
            return style;
        };

        /**
         * @inheritDoc
         * @api
         */
        ol.style.RegularShape.prototype.getAnchor = function() {
            return this.anchor_;
        };


        /**
         * Get the angle used in generating the shape.
         * @return {number} Shape's rotation in radians.
         * @api
         */
        ol.style.RegularShape.prototype.getAngle = function() {
            return this.angle_;
        };

        /**
         * Get the fill style for the shape.
         * @return {ol.style.Fill} Fill style.
         * @api
         */
        ol.style.RegularShape.prototype.getFill = function() {
            return this.fill_;
        };

        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.getHitDetectionImage = function(pixelRatio) {
            return this.hitDetectionCanvas_;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.style.RegularShape.prototype.getImage = function(pixelRatio) {
            return this.canvas_;
        };

        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.getImageSize = function() {
            return this.imageSize_;
        };


        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.getHitDetectionImageSize = function() {
            return this.hitDetectionImageSize_;
        };


        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.getImageState = function() {
            return ol.ImageState.LOADED;
        };

        /**
         * @inheritDoc
         * @api
         */
        ol.style.RegularShape.prototype.getOrigin = function() {
            return this.origin_;
        };


        /**
         * Get the number of points for generating the shape.
         * @return {number} Number of points for stars and regular polygons.
         * @api
         */
        ol.style.RegularShape.prototype.getPoints = function() {
            return this.points_;
        };


        /**
         * Get the (primary) radius for the shape.
         * @return {number} Radius.
         * @api
         */
        ol.style.RegularShape.prototype.getRadius = function() {
            return this.radius_;
        };


        /**
         * Get the secondary radius for the shape.
         * @return {number|undefined} Radius2.
         * @api
         */
        ol.style.RegularShape.prototype.getRadius2 = function() {
            return this.radius2_;
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.style.RegularShape.prototype.getSize = function() {
            return this.size_;
        };


        /**
         * Get the stroke style for the shape.
         * @return {ol.style.Stroke} Stroke style.
         * @api
         */
        ol.style.RegularShape.prototype.getStroke = function() {
            return this.stroke_;
        };


        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.listenImageChange = function(listener, thisArg) {};


        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.load = function() {};


        /**
         * @inheritDoc
         */
        ol.style.RegularShape.prototype.unlistenImageChange = function(listener, thisArg) {};


        /**
         * @protected
         * @param {ol.style.AtlasManager|undefined} atlasManager An atlas manager.
         */
        ol.style.RegularShape.prototype.render_ = function(atlasManager) {
            var imageSize;
            var lineCap = '';
            var lineJoin = '';
            var miterLimit = 0;
            var lineDash = null;
            var lineDashOffset = 0;
            var strokeStyle;
            var strokeWidth = 0;

            if (this.stroke_) {
                strokeStyle = this.stroke_.getColor();
                if (strokeStyle === null) {
                    strokeStyle = ol.render.canvas.defaultStrokeStyle;
                }
                strokeStyle = ol.colorlike.asColorLike(strokeStyle);
                strokeWidth = this.stroke_.getWidth();
                if (strokeWidth === undefined) {
                    strokeWidth = ol.render.canvas.defaultLineWidth;
                }
                lineDash = this.stroke_.getLineDash();
                lineDashOffset = this.stroke_.getLineDashOffset();
                if (!ol.has.CANVAS_LINE_DASH) {
                    lineDash = null;
                    lineDashOffset = 0;
                }
                lineJoin = this.stroke_.getLineJoin();
                if (lineJoin === undefined) {
                    lineJoin = ol.render.canvas.defaultLineJoin;
                }
                lineCap = this.stroke_.getLineCap();
                if (lineCap === undefined) {
                    lineCap = ol.render.canvas.defaultLineCap;
                }
                miterLimit = this.stroke_.getMiterLimit();
                if (miterLimit === undefined) {
                    miterLimit = ol.render.canvas.defaultMiterLimit;
                }
            }

            var size = 2 * (this.radius_ + strokeWidth) + 1;

            /** @type {ol.RegularShapeRenderOptions} */
            var renderOptions = {
                strokeStyle: strokeStyle,
                strokeWidth: strokeWidth,
                size: size,
                lineCap: lineCap,
                lineDash: lineDash,
                lineDashOffset: lineDashOffset,
                lineJoin: lineJoin,
                miterLimit: miterLimit
            };

            if (atlasManager === undefined) {
                // no atlas manager is used, create a new canvas
                var context = ol.dom.createCanvasContext2D(size, size);
                this.canvas_ = context.canvas;

                // canvas.width and height are rounded to the closest integer
                size = this.canvas_.width;
                imageSize = size;

                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 = !this.fill_;
                var renderHitDetectionCallback;
                if (hasCustomHitDetectionImage) {
                    // render the hit-detection image into a separate atlas image
                    renderHitDetectionCallback =
                        this.drawHitDetectionCanvas_.bind(this, renderOptions);
                }

                var id = this.getChecksum();
                var info = atlasManager.add(
                    id, size, size, this.draw_.bind(this, renderOptions),
                    renderHitDetectionCallback);

                this.canvas_ = info.image;
                this.origin_ = [info.offsetX, info.offsetY];
                imageSize = info.image.width;

                if (hasCustomHitDetectionImage) {
                    this.hitDetectionCanvas_ = info.hitImage;
                    this.hitDetectionImageSize_ =
                        [info.hitImage.width, info.hitImage.height];
                } else {
                    this.hitDetectionCanvas_ = this.canvas_;
                    this.hitDetectionImageSize_ = [imageSize, imageSize];
                }
            }

            this.anchor_ = [size / 2, size / 2];
            this.size_ = [size, size];
            this.imageSize_ = [imageSize, imageSize];
        };


        /**
         * @private
         * @param {ol.RegularShapeRenderOptions} renderOptions Render options.
         * @param {CanvasRenderingContext2D} context The rendering 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();

            var points = this.points_;
            if (points === Infinity) {
                context.arc(
                    renderOptions.size / 2, renderOptions.size / 2,
                    this.radius_, 0, 2 * Math.PI, true);
            } else {
                var radius2 = (this.radius2_ !== undefined) ? this.radius2_
                    : this.radius_;
                if (radius2 !== this.radius_) {
                    points = 2 * points;
                }
                for (i = 0; i <= points; i++) {
                    angle0 = i * 2 * Math.PI / points - Math.PI / 2 + this.angle_;
                    radiusC = i % 2 === 0 ? this.radius_ : radius2;
                    context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
                        renderOptions.size / 2 + radiusC * Math.sin(angle0));
                }
            }


            if (this.fill_) {
                var color = this.fill_.getColor();
                if (color === null) {
                    color = ol.render.canvas.defaultFillStyle;
                }
                context.fillStyle = ol.colorlike.asColorLike(color);
                context.fill();
            }
            if (this.stroke_) {
                context.strokeStyle = renderOptions.strokeStyle;
                context.lineWidth = renderOptions.strokeWidth;
                if (renderOptions.lineDash) {
                    context.setLineDash(renderOptions.lineDash);
                    context.lineDashOffset = renderOptions.lineDashOffset;
                }
                context.lineCap = renderOptions.lineCap;
                context.lineJoin = renderOptions.lineJoin;
                context.miterLimit = renderOptions.miterLimit;
                context.stroke();
            }
            context.closePath();
        };


        /**
         * @private
         * @param {ol.RegularShapeRenderOptions} renderOptions Render options.
         */
        ol.style.RegularShape.prototype.createHitDetectionCanvas_ = function(renderOptions) {
            this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
            if (this.fill_) {
                this.hitDetectionCanvas_ = this.canvas_;
                return;
            }

            // if no fill style is set, create an extra hit-detection image with a
            // default fill style
            var context = ol.dom.createCanvasContext2D(renderOptions.size, renderOptions.size);
            this.hitDetectionCanvas_ = context.canvas;

            this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
        };


        /**
         * @private
         * @param {ol.RegularShapeRenderOptions} renderOptions Render options.
         * @param {CanvasRenderingContext2D} context The 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();

            var points = this.points_;
            if (points === Infinity) {
                context.arc(
                    renderOptions.size / 2, renderOptions.size / 2,
                    this.radius_, 0, 2 * Math.PI, true);
            } else {
                var radius2 = (this.radius2_ !== undefined) ? this.radius2_
                    : this.radius_;
                if (radius2 !== this.radius_) {
                    points = 2 * points;
                }
                var i, radiusC, angle0;
                for (i = 0; i <= points; i++) {
                    angle0 = i * 2 * Math.PI / points - Math.PI / 2 + this.angle_;
                    radiusC = i % 2 === 0 ? this.radius_ : 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 (this.stroke_) {
                context.strokeStyle = renderOptions.strokeStyle;
                context.lineWidth = renderOptions.strokeWidth;
                if (renderOptions.lineDash) {
                    context.setLineDash(renderOptions.lineDash);
                    context.lineDashOffset = renderOptions.lineDashOffset;
                }
                context.stroke();
            }
            context.closePath();
        };


        /**
         * @return {string} The checksum.
         */
        ol.style.RegularShape.prototype.getChecksum = function() {
            var strokeChecksum = this.stroke_ ?
                this.stroke_.getChecksum() : '-';
            var fillChecksum = this.fill_ ?
                this.fill_.getChecksum() : '-';

            var recalculate = !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 +
                    (this.radius_ !== undefined ? this.radius_.toString() : '-') +
                    (this.radius2_ !== undefined ? this.radius2_.toString() : '-') +
                    (this.angle_ !== undefined ? this.angle_.toString() : '-') +
                    (this.points_ !== undefined ? this.points_.toString() : '-');
                this.checksums_ = [checksum, strokeChecksum, fillChecksum,
                    this.radius_, this.radius2_, this.angle_, this.points_];
            }

            return this.checksums_[0];
        };

        /**
         * @classdesc
         * Set circle style for vector features.
         *
         * @constructor
         * @param {olx.style.CircleOptions=} opt_options Options.
         * @extends {ol.style.RegularShape}
         * @api
         */
        ol.style.Circle = function(opt_options) {

            var options = opt_options || {};

            ol.style.RegularShape.call(this, {
                points: Infinity,
                fill: options.fill,
                radius: options.radius,
                snapToPixel: options.snapToPixel,
                stroke: options.stroke,
                atlasManager: options.atlasManager
            });

        };
        ol.inherits(ol.style.Circle, ol.style.RegularShape);


        /**
         * Clones the style.  If an atlasmanager was provided to the original style it will be used in the cloned style, too.
         * @return {ol.style.Circle} The cloned style.
         * @override
         * @api
         */
        ol.style.Circle.prototype.clone = function() {
            var style = new ol.style.Circle({
                fill: this.getFill() ? this.getFill().clone() : undefined,
                stroke: this.getStroke() ? this.getStroke().clone() : undefined,
                radius: this.getRadius(),
                snapToPixel: this.getSnapToPixel(),
                atlasManager: this.atlasManager_
            });
            style.setOpacity(this.getOpacity());
            style.setScale(this.getScale());
            return style;
        };


        /**
         * Set the circle radius.
         *
         * @param {number} radius Circle radius.
         * @api
         */
        ol.style.Circle.prototype.setRadius = function(radius) {
            this.radius_ = radius;
            this.render_(this.atlasManager_);
        };

        /**
         * @classdesc
         * Set fill style for vector features.
         *
         * @constructor
         * @param {olx.style.FillOptions=} opt_options Options.
         * @api
         */
        ol.style.Fill = function(opt_options) {

            var options = opt_options || {};

            /**
             * @private
             * @type {ol.Color|ol.ColorLike}
             */
            this.color_ = options.color !== undefined ? options.color : null;

            /**
             * @private
             * @type {string|undefined}
             */
            this.checksum_ = undefined;
        };


        /**
         * Clones the style. The color is not cloned if it is an {@link ol.ColorLike}.
         * @return {ol.style.Fill} The cloned style.
         * @api
         */
        ol.style.Fill.prototype.clone = function() {
            var color = this.getColor();
            return new ol.style.Fill({
                color: (color && color.slice) ? color.slice() : color || undefined
            });
        };


        /**
         * Get the fill color.
         * @return {ol.Color|ol.ColorLike} Color.
         * @api
         */
        ol.style.Fill.prototype.getColor = function() {
            return this.color_;
        };


        /**
         * Set the color.
         *
         * @param {ol.Color|ol.ColorLike} color Color.
         * @api
         */
        ol.style.Fill.prototype.setColor = function(color) {
            this.color_ = color;
            this.checksum_ = undefined;
        };


        /**
         * @return {string} The checksum.
         */
        ol.style.Fill.prototype.getChecksum = function() {
            if (this.checksum_ === undefined) {
                if (
                    this.color_ instanceof CanvasPattern ||
                        this.color_ instanceof CanvasGradient
                    ) {
                    this.checksum_ = ol.getUid(this.color_).toString();
                } else {
                    this.checksum_ = 'f' + (this.color_ ?
                        ol.color.asString(this.color_) : '-');
                }
            }

            return this.checksum_;
        };

        /**
         * @classdesc
         * Set stroke style for vector features.
         * Note that the defaults given are the Canvas defaults, which will be used if
         * option is not defined. The `get` functions return whatever was entered in
         * the options; they will not return the default.
         *
         * @constructor
         * @param {olx.style.StrokeOptions=} opt_options Options.
         * @api
         */
        ol.style.Stroke = function(opt_options) {

            var options = opt_options || {};

            /**
             * @private
             * @type {ol.Color|ol.ColorLike}
             */
            this.color_ = options.color !== undefined ? options.color : null;

            /**
             * @private
             * @type {string|undefined}
             */
            this.lineCap_ = options.lineCap;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.lineDash_ = options.lineDash !== undefined ? options.lineDash : null;

            /**
             * @private
             * @type {number|undefined}
             */
            this.lineDashOffset_ = options.lineDashOffset;

            /**
             * @private
             * @type {string|undefined}
             */
            this.lineJoin_ = options.lineJoin;

            /**
             * @private
             * @type {number|undefined}
             */
            this.miterLimit_ = options.miterLimit;

            /**
             * @private
             * @type {number|undefined}
             */
            this.width_ = options.width;

            /**
             * @private
             * @type {string|undefined}
             */
            this.checksum_ = undefined;
        };


        /**
         * Clones the style.
         * @return {ol.style.Stroke} The cloned style.
         * @api
         */
        ol.style.Stroke.prototype.clone = function() {
            var color = this.getColor();
            return new ol.style.Stroke({
                color: (color && color.slice) ? color.slice() : color || undefined,
                lineCap: this.getLineCap(),
                lineDash: this.getLineDash() ? this.getLineDash().slice() : undefined,
                lineDashOffset: this.getLineDashOffset(),
                lineJoin: this.getLineJoin(),
                miterLimit: this.getMiterLimit(),
                width: this.getWidth()
            });
        };


        /**
         * Get the stroke color.
         * @return {ol.Color|ol.ColorLike} Color.
         * @api
         */
        ol.style.Stroke.prototype.getColor = function() {
            return this.color_;
        };


        /**
         * Get the line cap type for the stroke.
         * @return {string|undefined} Line cap.
         * @api
         */
        ol.style.Stroke.prototype.getLineCap = function() {
            return this.lineCap_;
        };


        /**
         * Get the line dash style for the stroke.
         * @return {Array.<number>} Line dash.
         * @api
         */
        ol.style.Stroke.prototype.getLineDash = function() {
            return this.lineDash_;
        };


        /**
         * Get the line dash offset for the stroke.
         * @return {number|undefined} Line dash offset.
         * @api
         */
        ol.style.Stroke.prototype.getLineDashOffset = function() {
            return this.lineDashOffset_;
        };


        /**
         * Get the line join type for the stroke.
         * @return {string|undefined} Line join.
         * @api
         */
        ol.style.Stroke.prototype.getLineJoin = function() {
            return this.lineJoin_;
        };


        /**
         * Get the miter limit for the stroke.
         * @return {number|undefined} Miter limit.
         * @api
         */
        ol.style.Stroke.prototype.getMiterLimit = function() {
            return this.miterLimit_;
        };


        /**
         * Get the stroke width.
         * @return {number|undefined} Width.
         * @api
         */
        ol.style.Stroke.prototype.getWidth = function() {
            return this.width_;
        };


        /**
         * Set the color.
         *
         * @param {ol.Color|ol.ColorLike} color Color.
         * @api
         */
        ol.style.Stroke.prototype.setColor = function(color) {
            this.color_ = color;
            this.checksum_ = undefined;
        };


        /**
         * Set the line cap.
         *
         * @param {string|undefined} lineCap Line cap.
         * @api
         */
        ol.style.Stroke.prototype.setLineCap = function(lineCap) {
            this.lineCap_ = lineCap;
            this.checksum_ = undefined;
        };


        /**
         * Set the line dash.
         *
         * Please note that Internet Explorer 10 and lower [do not support][mdn] the
         * `setLineDash` method on the `CanvasRenderingContext2D` and therefore this
         * property will have no visual effect in these browsers.
         *
         * [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility
         *
         * @param {Array.<number>} lineDash Line dash.
         * @api
         */
        ol.style.Stroke.prototype.setLineDash = function(lineDash) {
            this.lineDash_ = lineDash;
            this.checksum_ = undefined;
        };


        /**
         * Set the line dash offset.
         *
         * @param {number|undefined} lineDashOffset Line dash offset.
         * @api
         */
        ol.style.Stroke.prototype.setLineDashOffset = function(lineDashOffset) {
            this.lineDashOffset_ = lineDashOffset;
            this.checksum_ = undefined;
        };


        /**
         * Set the line join.
         *
         * @param {string|undefined} lineJoin Line join.
         * @api
         */
        ol.style.Stroke.prototype.setLineJoin = function(lineJoin) {
            this.lineJoin_ = lineJoin;
            this.checksum_ = undefined;
        };


        /**
         * Set the miter limit.
         *
         * @param {number|undefined} miterLimit Miter limit.
         * @api
         */
        ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) {
            this.miterLimit_ = miterLimit;
            this.checksum_ = undefined;
        };


        /**
         * Set the width.
         *
         * @param {number|undefined} width Width.
         * @api
         */
        ol.style.Stroke.prototype.setWidth = function(width) {
            this.width_ = width;
            this.checksum_ = undefined;
        };


        /**
         * @return {string} The checksum.
         */
        ol.style.Stroke.prototype.getChecksum = function() {
            if (this.checksum_ === undefined) {
                this.checksum_ = 's';
                if (this.color_) {
                    if (typeof this.color_ === 'string') {
                        this.checksum_ += this.color_;
                    } else {
                        this.checksum_ += ol.getUid(this.color_).toString();
                    }
                } else {
                    this.checksum_ += '-';
                }
                this.checksum_ += ',' +
                    (this.lineCap_ !== undefined ?
                        this.lineCap_.toString() : '-') + ',' +
                    (this.lineDash_ ?
                        this.lineDash_.toString() : '-') + ',' +
                    (this.lineDashOffset_ !== undefined ?
                        this.lineDashOffset_ : '-') + ',' +
                    (this.lineJoin_ !== undefined ?
                        this.lineJoin_ : '-') + ',' +
                    (this.miterLimit_ !== undefined ?
                        this.miterLimit_.toString() : '-') + ',' +
                    (this.width_ !== undefined ?
                        this.width_.toString() : '-');
            }

            return this.checksum_;
        };

        /**
         * Icon anchor units. One of 'fraction', 'pixels'.
         * @enum {string}
         */
        ol.style.IconAnchorUnits = {
            FRACTION: 'fraction',
            PIXELS: 'pixels'
        };

        /**
         * @constructor
         * @param {Image|HTMLCanvasElement} image Image.
         * @param {string|undefined} src Src.
         * @param {ol.Size} size Size.
         * @param {?string} crossOrigin Cross origin.
         * @param {ol.ImageState} imageState Image state.
         * @param {ol.Color} color Color.
         * @extends {ol.events.EventTarget}
         */
        ol.style.IconImage = function(image, src, size, crossOrigin, imageState,
                                      color) {

            ol.events.EventTarget.call(this);

            /**
             * @private
             * @type {Image|HTMLCanvasElement}
             */
            this.hitDetectionImage_ = null;

            /**
             * @private
             * @type {Image|HTMLCanvasElement}
             */
            this.image_ = !image ? new Image() : image;

            if (crossOrigin !== null) {
                this.image_.crossOrigin = crossOrigin;
            }

            /**
             * @private
             * @type {HTMLCanvasElement}
             */
            this.canvas_ = color ?
            /** @type {HTMLCanvasElement} */ (document.createElement('CANVAS')) :
                null;

            /**
             * @private
             * @type {ol.Color}
             */
            this.color_ = color;

            /**
             * @private
             * @type {Array.<ol.EventsKey>}
             */
            this.imageListenerKeys_ = null;

            /**
             * @private
             * @type {ol.ImageState}
             */
            this.imageState_ = imageState;

            /**
             * @private
             * @type {ol.Size}
             */
            this.size_ = size;

            /**
             * @private
             * @type {string|undefined}
             */
            this.src_ = src;

            /**
             * @private
             * @type {boolean}
             */
            this.tainting_ = false;
            if (this.imageState_ == ol.ImageState.LOADED) {
                this.determineTainting_();
            }

        };
        ol.inherits(ol.style.IconImage, ol.events.EventTarget);


        /**
         * @param {Image|HTMLCanvasElement} image Image.
         * @param {string} src Src.
         * @param {ol.Size} size Size.
         * @param {?string} crossOrigin Cross origin.
         * @param {ol.ImageState} imageState Image state.
         * @param {ol.Color} color Color.
         * @return {ol.style.IconImage} Icon image.
         */
        ol.style.IconImage.get = function(image, src, size, crossOrigin, imageState,
                                          color) {
            var iconImageCache = ol.style.iconImageCache;
            var iconImage = iconImageCache.get(src, crossOrigin, color);
            if (!iconImage) {
                iconImage = new ol.style.IconImage(
                    image, src, size, crossOrigin, imageState, color);
                iconImageCache.set(src, crossOrigin, color, iconImage);
            }
            return iconImage;
        };


        /**
         * @private
         */
        ol.style.IconImage.prototype.determineTainting_ = function() {
            var context = ol.dom.createCanvasContext2D(1, 1);
            try {
                context.drawImage(this.image_, 0, 0);
                context.getImageData(0, 0, 1, 1);
            } catch (e) {
                this.tainting_ = true;
            }
        };


        /**
         * @private
         */
        ol.style.IconImage.prototype.dispatchChangeEvent_ = function() {
            this.dispatchEvent(ol.events.EventType.CHANGE);
        };


        /**
         * @private
         */
        ol.style.IconImage.prototype.handleImageError_ = function() {
            this.imageState_ = ol.ImageState.ERROR;
            this.unlistenImage_();
            this.dispatchChangeEvent_();
        };


        /**
         * @private
         */
        ol.style.IconImage.prototype.handleImageLoad_ = function() {
            this.imageState_ = ol.ImageState.LOADED;
            if (this.size_) {
                this.image_.width = this.size_[0];
                this.image_.height = this.size_[1];
            }
            this.size_ = [this.image_.width, this.image_.height];
            this.unlistenImage_();
            this.determineTainting_();
            this.replaceColor_();
            this.dispatchChangeEvent_();
        };


        /**
         * @param {number} pixelRatio Pixel ratio.
         * @return {Image|HTMLCanvasElement} Image or Canvas element.
         */
        ol.style.IconImage.prototype.getImage = function(pixelRatio) {
            return this.canvas_ ? this.canvas_ : this.image_;
        };


        /**
         * @return {ol.ImageState} Image state.
         */
        ol.style.IconImage.prototype.getImageState = function() {
            return this.imageState_;
        };


        /**
         * @param {number} pixelRatio Pixel ratio.
         * @return {Image|HTMLCanvasElement} Image element.
         */
        ol.style.IconImage.prototype.getHitDetectionImage = function(pixelRatio) {
            if (!this.hitDetectionImage_) {
                if (this.tainting_) {
                    var width = this.size_[0];
                    var height = this.size_[1];
                    var context = ol.dom.createCanvasContext2D(width, height);
                    context.fillRect(0, 0, width, height);
                    this.hitDetectionImage_ = context.canvas;
                } else {
                    this.hitDetectionImage_ = this.image_;
                }
            }
            return this.hitDetectionImage_;
        };


        /**
         * @return {ol.Size} Image size.
         */
        ol.style.IconImage.prototype.getSize = function() {
            return this.size_;
        };


        /**
         * @return {string|undefined} Image src.
         */
        ol.style.IconImage.prototype.getSrc = function() {
            return this.src_;
        };


        /**
         * Load not yet loaded URI.
         */
        ol.style.IconImage.prototype.load = function() {
            if (this.imageState_ == ol.ImageState.IDLE) {
                this.imageState_ = ol.ImageState.LOADING;
                this.imageListenerKeys_ = [
                    ol.events.listenOnce(this.image_, ol.events.EventType.ERROR,
                        this.handleImageError_, this),
                    ol.events.listenOnce(this.image_, ol.events.EventType.LOAD,
                        this.handleImageLoad_, this)
                ];
                try {
                    this.image_.src = this.src_;
                } catch (e) {
                    this.handleImageError_();
                }
            }
        };


        /**
         * @private
         */
        ol.style.IconImage.prototype.replaceColor_ = function() {
            if (this.tainting_ || this.color_ === null) {
                return;
            }

            this.canvas_.width = this.image_.width;
            this.canvas_.height = this.image_.height;

            var ctx = this.canvas_.getContext('2d');
            ctx.drawImage(this.image_, 0, 0);

            var imgData = ctx.getImageData(0, 0, this.image_.width, this.image_.height);
            var data = imgData.data;
            var r = this.color_[0] / 255.0;
            var g = this.color_[1] / 255.0;
            var b = this.color_[2] / 255.0;

            for (var i = 0, ii = data.length; i < ii; i += 4) {
                data[i] *= r;
                data[i + 1] *= g;
                data[i + 2] *= b;
            }
            ctx.putImageData(imgData, 0, 0);
        };


        /**
         * Discards event handlers which listen for load completion or errors.
         *
         * @private
         */
        ol.style.IconImage.prototype.unlistenImage_ = function() {
            this.imageListenerKeys_.forEach(ol.events.unlistenByKey);
            this.imageListenerKeys_ = null;
        };

        /**
         * Icon origin. One of 'bottom-left', 'bottom-right', 'top-left', 'top-right'.
         * @enum {string}
         */
        ol.style.IconOrigin = {
            BOTTOM_LEFT: 'bottom-left',
            BOTTOM_RIGHT: 'bottom-right',
            TOP_LEFT: 'top-left',
            TOP_RIGHT: 'top-right'
        };

        /**
         * @classdesc
         * Set icon style for vector features.
         *
         * @constructor
         * @param {olx.style.IconOptions=} opt_options Options.
         * @extends {ol.style.Image}
         * @api
         */
        ol.style.Icon = function(opt_options) {

            var options = opt_options || {};

            /**
             * @private
             * @type {Array.<number>}
             */
            this.anchor_ = options.anchor !== undefined ? options.anchor : [0.5, 0.5];

            /**
             * @private
             * @type {Array.<number>}
             */
            this.normalizedAnchor_ = null;

            /**
             * @private
             * @type {ol.style.IconOrigin}
             */
            this.anchorOrigin_ = options.anchorOrigin !== undefined ?
                options.anchorOrigin : ol.style.IconOrigin.TOP_LEFT;

            /**
             * @private
             * @type {ol.style.IconAnchorUnits}
             */
            this.anchorXUnits_ = options.anchorXUnits !== undefined ?
                options.anchorXUnits : ol.style.IconAnchorUnits.FRACTION;

            /**
             * @private
             * @type {ol.style.IconAnchorUnits}
             */
            this.anchorYUnits_ = options.anchorYUnits !== undefined ?
                options.anchorYUnits : ol.style.IconAnchorUnits.FRACTION;

            /**
             * @private
             * @type {?string}
             */
            this.crossOrigin_ =
                options.crossOrigin !== undefined ? options.crossOrigin : null;

            /**
             * @type {Image|HTMLCanvasElement}
             */
            var image = options.img !== undefined ? options.img : null;

            /**
             * @type {ol.Size}
             */
            var imgSize = options.imgSize !== undefined ? options.imgSize : null;

            /**
             * @type {string|undefined}
             */
            var src = options.src;

    //        ol.asserts.assert(!(src !== undefined && image),
    //            4); // `image` and `src` cannot be provided at the same time
    //        ol.asserts.assert(!image || (image && imgSize),
    //            5); // `imgSize` must be set when `image` is provided

            if ((src === undefined || src.length === 0) && image) {
                src = image.src || ol.getUid(image).toString();
            }
    //        ol.asserts.assert(src !== undefined && src.length > 0,
    //            6); // A defined and non-empty `src` or `image` must be provided

            /**
             * @type {ol.ImageState}
             */
            var imageState = options.src !== undefined ?
                ol.ImageState.IDLE : ol.ImageState.LOADED;

            /**
             * @private
             * @type {ol.Color}
             */
            this.color_ = options.color !== undefined ? ol.color.asArray(options.color) :
                null;

            /**
             * @private
             * @type {ol.style.IconImage}
             */
            this.iconImage_ = ol.style.IconImage.get(
                image, /** @type {string} */ (src), imgSize, this.crossOrigin_, imageState, this.color_);

            /**
             * @private
             * @type {Array.<number>}
             */
            this.offset_ = options.offset !== undefined ? options.offset : [0, 0];

            /**
             * @private
             * @type {ol.style.IconOrigin}
             */
            this.offsetOrigin_ = options.offsetOrigin !== undefined ?
                options.offsetOrigin : ol.style.IconOrigin.TOP_LEFT;

            /**
             * @private
             * @type {Array.<number>}
             */
            this.origin_ = null;

            /**
             * @private
             * @type {ol.Size}
             */
            this.size_ = options.size !== undefined ? options.size : null;

            /**
             * @type {number}
             */
            var opacity = options.opacity !== undefined ? options.opacity : 1;

            /**
             * @type {boolean}
             */
            var rotateWithView = options.rotateWithView !== undefined ?
                options.rotateWithView : false;

            /**
             * @type {number}
             */
            var rotation = options.rotation !== undefined ? options.rotation : 0;

            /**
             * @type {number}
             */
            var scale = options.scale !== undefined ? options.scale : 1;

            /**
             * @type {boolean}
             */
            var snapToPixel = options.snapToPixel !== undefined ?
                options.snapToPixel : true;

            ol.style.Image.call(this, {
                opacity: opacity,
                rotation: rotation,
                scale: scale,
                snapToPixel: snapToPixel,
                rotateWithView: rotateWithView
            });

        };
        ol.inherits(ol.style.Icon, ol.style.Image);


        /**
         * Clones the style. The underlying Image/HTMLCanvasElement is not cloned.
         * @return {ol.style.Icon} The cloned style.
         * @api
         */
        ol.style.Icon.prototype.clone = function() {
            return new ol.style.Icon({
                anchor: this.anchor_.slice(),
                anchorOrigin: this.anchorOrigin_,
                anchorXUnits: this.anchorXUnits_,
                anchorYUnits: this.anchorYUnits_,
                crossOrigin: this.crossOrigin_,
                color: (this.color_ && this.color_.slice) ? this.color_.slice() : this.color_ || undefined,
                src: this.getSrc(),
                offset: this.offset_.slice(),
                offsetOrigin: this.offsetOrigin_,
                size: this.size_ !== null ? this.size_.slice() : undefined,
                opacity: this.getOpacity(),
                scale: this.getScale(),
                snapToPixel: this.getSnapToPixel(),
                rotation: this.getRotation(),
                rotateWithView: this.getRotateWithView()
            });
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.style.Icon.prototype.getAnchor = function() {
            if (this.normalizedAnchor_) {
                return this.normalizedAnchor_;
            }
            var anchor = this.anchor_;
            var size = this.getSize();
            if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION ||
                this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
                if (!size) {
                    return null;
                }
                anchor = this.anchor_.slice();
                if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION) {
                    anchor[0] *= size[0];
                }
                if (this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
                    anchor[1] *= size[1];
                }
            }

            if (this.anchorOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
                if (!size) {
                    return null;
                }
                if (anchor === this.anchor_) {
                    anchor = this.anchor_.slice();
                }
                if (this.anchorOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
                    this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
                    anchor[0] = -anchor[0] + size[0];
                }
                if (this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
                    this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
                    anchor[1] = -anchor[1] + size[1];
                }
            }
            this.normalizedAnchor_ = anchor;
            return this.normalizedAnchor_;
        };


        /**
         * Get the icon color.
         * @return {ol.Color} Color.
         * @api
         */
        ol.style.Icon.prototype.getColor = function() {
            return this.color_;
        };


        /**
         * Get the image icon.
         * @param {number} pixelRatio Pixel ratio.
         * @return {Image|HTMLCanvasElement} Image or Canvas element.
         * @override
         * @api
         */
        ol.style.Icon.prototype.getImage = function(pixelRatio) {
            return this.iconImage_.getImage(pixelRatio);
        };


        /**
         * @override
         */
        ol.style.Icon.prototype.getImageSize = function() {
            return this.iconImage_.getSize();
        };


        /**
         * @override
         */
        ol.style.Icon.prototype.getHitDetectionImageSize = function() {
            return this.getImageSize();
        };


        /**
         * @override
         */
        ol.style.Icon.prototype.getImageState = function() {
            return this.iconImage_.getImageState();
        };


        /**
         * @override
         */
        ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) {
            return this.iconImage_.getHitDetectionImage(pixelRatio);
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.style.Icon.prototype.getOrigin = function() {
            if (this.origin_) {
                return this.origin_;
            }
            var offset = this.offset_;

            if (this.offsetOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
                var size = this.getSize();
                var iconImageSize = this.iconImage_.getSize();
                if (!size || !iconImageSize) {
                    return null;
                }
                offset = offset.slice();
                if (this.offsetOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
                    this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
                    offset[0] = iconImageSize[0] - size[0] - offset[0];
                }
                if (this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
                    this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
                    offset[1] = iconImageSize[1] - size[1] - offset[1];
                }
            }
            this.origin_ = offset;
            return this.origin_;
        };


        /**
         * Get the image URL.
         * @return {string|undefined} Image src.
         * @api
         */
        ol.style.Icon.prototype.getSrc = function() {
            return this.iconImage_.getSrc();
        };


        /**
         * @inheritDoc
         * @api
         */
        ol.style.Icon.prototype.getSize = function() {
            return !this.size_ ? this.iconImage_.getSize() : this.size_;
        };


        /**
         * @override
         */
        ol.style.Icon.prototype.listenImageChange = function(listener, thisArg) {
            return ol.events.listen(this.iconImage_, ol.events.EventType.CHANGE,
                listener, thisArg);
        };


        /**
         * Load not yet loaded URI.
         * When rendering a feature with an icon style, the vector renderer will
         * automatically call this method. However, you might want to call this
         * method yourself for preloading or other purposes.
         * @override
         * @api
         */
        ol.style.Icon.prototype.load = function() {
            this.iconImage_.load();
        };


        /**
         * @override
         */
        ol.style.Icon.prototype.unlistenImageChange = function(listener, thisArg) {
            ol.events.unlisten(this.iconImage_, ol.events.EventType.CHANGE,
                listener, thisArg);
        };

        ol.style.Text = function(opt_options) {
            var options = opt_options || {};
            this.font_ = options.font;
            this.rotation_ = options.rotation;
            this.rotateWithView_ = options.rotateWithView;
            this.scale_ = options.scale;
            this.text_ = options.text;
            this.textAlign_ = options.textAlign;
            this.textBaseline_ = options.textBaseline;
            this.fill_ = options.fill !== undefined ? options.fill :
                new ol.style.Fill({color: ol.style.Text.DEFAULT_FILL_COLOR_});
            this.maxAngle_ = options.maxAngle !== undefined ? options.maxAngle : Math.PI / 4;
            this.placement_ = options.placement !== undefined ? options.placement : ol.style.TextPlacement.POINT;

            //TODO Use options.overflow directly after removing @deprecated exceedLength
            var overflow = options.overflow === undefined ? options.exceedLength : options.overflow;

            this.overflow_ = overflow !== undefined ? overflow : false;
            this.stroke_ = options.stroke !== undefined ? options.stroke : null;
            this.offsetX_ = options.offsetX !== undefined ? options.offsetX : 0;
            this.offsetY_ = options.offsetY !== undefined ? options.offsetY : 0;
            this.backgroundFill_ = options.backgroundFill ? options.backgroundFill : null;
            this.backgroundStroke_ = options.backgroundStroke ? options.backgroundStroke : null;
            this.padding_ = options.padding === undefined ? null : options.padding;
        };

        ol.style.Text.DEFAULT_FILL_COLOR_ = '#333';
        ol.style.Text.prototype.clone = function() {
            return new ol.style.Text({
                font: this.getFont(),
                placement: this.getPlacement(),
                maxAngle: this.getMaxAngle(),
                overflow: this.getOverflow(),
                rotation: this.getRotation(),
                rotateWithView: this.getRotateWithView(),
                scale: this.getScale(),
                text: this.getText(),
                textAlign: this.getTextAlign(),
                textBaseline: this.getTextBaseline(),
                fill: this.getFill() ? this.getFill().clone() : undefined,
                stroke: this.getStroke() ? this.getStroke().clone() : undefined,
                offsetX: this.getOffsetX(),
                offsetY: this.getOffsetY()
            });
        };

        ol.style.Text.prototype.getOverflow = function() {
            return this.overflow_;
        };

        ol.style.Text.prototype.getFont = function() {
            return this.font_;
        };

        ol.style.Text.prototype.getMaxAngle = function() {
            return this.maxAngle_;
        };

        ol.style.Text.prototype.getPlacement = function() {
            return this.placement_;
        };

        ol.style.Text.prototype.getOffsetX = function() {
            return this.offsetX_;
        };

        ol.style.Text.prototype.getOffsetY = function() {
            return this.offsetY_;
        };

        ol.style.Text.prototype.getFill = function() {
            return this.fill_;
        };

        ol.style.Text.prototype.getRotateWithView = function() {
            return this.rotateWithView_;
        };

        ol.style.Text.prototype.getRotation = function() {
            return this.rotation_;
        };

        ol.style.Text.prototype.getScale = function() {
            return this.scale_;
        };

        ol.style.Text.prototype.getStroke = function() {
            return this.stroke_;
        };

        ol.style.Text.prototype.getText = function() {
            return this.text_;
        };

        ol.style.Text.prototype.getTextAlign = function() {
            return this.textAlign_;
        };

        ol.style.Text.prototype.getTextBaseline = function() {
            return this.textBaseline_;
        };

        ol.style.Text.prototype.getBackgroundFill = function() {
            return this.backgroundFill_;
        };

        ol.style.Text.prototype.getBackgroundStroke = function() {
            return this.backgroundStroke_;
        };

        ol.style.Text.prototype.getPadding = function() {
            return this.padding_;
        };

        ol.style.Text.prototype.setOverflow = function(overflow) {
            this.overflow_ = overflow;
        };

        ol.style.Text.prototype.setFont = function(font) {
            this.font_ = font;
        };

        ol.style.Text.prototype.setMaxAngle = function(maxAngle) {
            this.maxAngle_ = maxAngle;
        };

        ol.style.Text.prototype.setOffsetX = function(offsetX) {
            this.offsetX_ = offsetX;
        };

        ol.style.Text.prototype.setOffsetY = function(offsetY) {
            this.offsetY_ = offsetY;
        };

        ol.style.Text.prototype.setPlacement = function(placement) {
            this.placement_ = placement;
        };

        ol.style.Text.prototype.setFill = function(fill) {
            this.fill_ = fill;
        };

        ol.style.Text.prototype.setRotation = function(rotation) {
            this.rotation_ = rotation;
        };

        ol.style.Text.prototype.setScale = function(scale) {
            this.scale_ = scale;
        };

        ol.style.Text.prototype.setStroke = function(stroke) {
            this.stroke_ = stroke;
        };

        ol.style.Text.prototype.setText = function(text) {
            this.text_ = text;
        };

        ol.style.Text.prototype.setTextAlign = function(textAlign) {
            this.textAlign_ = textAlign;
        };

        ol.style.Text.prototype.setTextBaseline = function(textBaseline) {
            this.textBaseline_ = textBaseline;
        };

        ol.style.Text.prototype.setBackgroundFill = function(fill) {
            this.backgroundFill_ = fill;
        };

        ol.style.Text.prototype.setBackgroundStroke = function(stroke) {
            this.backgroundStroke_ = stroke;
        };

        ol.style.Text.prototype.setPadding = function(padding) {
            this.padding_ = padding;
        };

        ol.style.Style = function(opt_options) {

            var options = opt_options || {};

            /**
             * @private
             * @type {string|ol.geom.Geometry|ol.StyleGeometryFunction}
             */
            this.geometry_ = null;

            /**
             * @private
             * @type {!ol.StyleGeometryFunction}
             */
            this.geometryFunction_ = ol.style.Style.defaultGeometryFunction;

            if (options.geometry !== undefined) {
                this.setGeometry(options.geometry);
            }

            /**
             * @private
             * @type {ol.style.Fill}
             */
            this.fill_ = options.fill !== undefined ? options.fill : null;

            /**
             * @private
             * @type {ol.style.Image}
             */
            this.image_ = options.image !== undefined ? options.image : null;

            /**
             * @private
             * @type {ol.StyleRenderFunction|null}
             */
            this.renderer_ = options.renderer !== undefined ? options.renderer : null;

            /**
             * @private
             * @type {ol.style.Stroke}
             */
            this.stroke_ = options.stroke !== undefined ? options.stroke : null;

            /**
             * @private
             * @type {ol.style.Text}
             */
            this.text_ = options.text !== undefined ? options.text : null;

            /**
             * @private
             * @type {number|undefined}
             */
            this.zIndex_ = options.zIndex;

        };


        /**
         * Clones the style.
         * @return {ol.style.Style} The cloned style.
         * @api
         */
        ol.style.Style.prototype.clone = function() {
            var geometry = this.getGeometry();
            if (geometry && geometry.clone) {
                geometry = geometry.clone();
            }
            return new ol.style.Style({
                geometry: geometry,
                fill: this.getFill() ? this.getFill().clone() : undefined,
                image: this.getImage() ? this.getImage().clone() : undefined,
                stroke: this.getStroke() ? this.getStroke().clone() : undefined,
                text: this.getText() ? this.getText().clone() : undefined,
                zIndex: this.getZIndex()
            });
        };


        /**
         * Get the custom renderer function that was configured with
         * {@link #setRenderer} or the `renderer` constructor option.
         * @return {ol.StyleRenderFunction|null} Custom renderer function.
         * @api
         */
        ol.style.Style.prototype.getRenderer = function() {
            return this.renderer_;
        };


        /**
         * Sets a custom renderer function for this style. When set, `fill`, `stroke`
         * and `image` options of the style will be ignored.
         * @param {ol.StyleRenderFunction|null} renderer Custom renderer function.
         * @api
         */
        ol.style.Style.prototype.setRenderer = function(renderer) {
            this.renderer_ = renderer;
        };


        /**
         * Get the geometry to be rendered.
         * @return {string|ol.geom.Geometry|ol.StyleGeometryFunction}
         * Feature property or geometry or function that returns the geometry that will
         * be rendered with this style.
         * @api
         */
        ol.style.Style.prototype.getGeometry = function() {
            return this.geometry_;
        };


        /**
         * Get the function used to generate a geometry for rendering.
         * @return {!ol.StyleGeometryFunction} Function that is called with a feature
         * and returns the geometry to render instead of the feature's geometry.
         * @api
         */
        ol.style.Style.prototype.getGeometryFunction = function() {
            return this.geometryFunction_;
        };


        /**
         * Get the fill style.
         * @return {ol.style.Fill} Fill style.
         * @api
         */
        ol.style.Style.prototype.getFill = function() {
            return this.fill_;
        };


        /**
         * Set the fill style.
         * @param {ol.style.Fill} fill Fill style.
         * @api
         */
        ol.style.Style.prototype.setFill = function(fill) {
            this.fill_ = fill;
        };


        /**
         * Get the image style.
         * @return {ol.style.Image} Image style.
         * @api
         */
        ol.style.Style.prototype.getImage = function() {
            return this.image_;
        };


        /**
         * Set the image style.
         * @param {ol.style.Image} image Image style.
         * @api
         */
        ol.style.Style.prototype.setImage = function(image) {
            this.image_ = image;
        };


        /**
         * Get the stroke style.
         * @return {ol.style.Stroke} Stroke style.
         * @api
         */
        ol.style.Style.prototype.getStroke = function() {
            return this.stroke_;
        };


        /**
         * Set the stroke style.
         * @param {ol.style.Stroke} stroke Stroke style.
         * @api
         */
        ol.style.Style.prototype.setStroke = function(stroke) {
            this.stroke_ = stroke;
        };


        /**
         * Get the text style.
         * @return {ol.style.Text} Text style.
         * @api
         */
        ol.style.Style.prototype.getText = function() {
            return this.text_;
        };


        /**
         * Set the text style.
         * @param {ol.style.Text} text Text style.
         * @api
         */
        ol.style.Style.prototype.setText = function(text) {
            this.text_ = text;
        };


        /**
         * Get the z-index for the style.
         * @return {number|undefined} ZIndex.
         * @api
         */
        ol.style.Style.prototype.getZIndex = function() {
            return this.zIndex_;
        };


        /**
         * Set a geometry that is rendered instead of the feature's geometry.
         *
         * @param {string|ol.geom.Geometry|ol.StyleGeometryFunction} geometry
         *     Feature property or geometry or function returning a geometry to render
         *     for this style.
         * @api
         */
        ol.style.Style.prototype.setGeometry = function(geometry) {
            if (typeof geometry === 'function') {
                this.geometryFunction_ = geometry;
            } else if (typeof geometry === 'string') {
                this.geometryFunction_ = function(feature) {
                    return /** @type {ol.geom.Geometry} */ (feature.get(geometry));
                };
            } else if (!geometry) {
                this.geometryFunction_ = ol.style.Style.defaultGeometryFunction;
            } else if (geometry !== undefined) {
                this.geometryFunction_ = function() {
                    return /** @type {ol.geom.Geometry} */ (geometry);
                };
            }
            this.geometry_ = geometry;
        };


        /**
         * Set the z-index.
         *
         * @param {number|undefined} zIndex ZIndex.
         * @api
         */
        ol.style.Style.prototype.setZIndex = function(zIndex) {
            this.zIndex_ = zIndex;
        };


        /**
         * Convert the provided object into a style function.  Functions passed through
         * unchanged.  Arrays of ol.style.Style or single style objects wrapped in a
         * new style function.
         * @param {ol.StyleFunction|Array.<ol.style.Style>|ol.style.Style} obj
         *     A style function, a single style, or an array of styles.
         * @return {ol.StyleFunction} A style function.
         */
        ol.style.Style.createFunction = function(obj) {
            var styleFunction;

            if (typeof obj === 'function') {
                styleFunction = obj;
            } else {
                /**
                 * @type {Array.<ol.style.Style>}
                 */
                var styles;
                if (Array.isArray(obj)) {
                    styles = obj;
                } else {
                    ol.asserts.assert(obj instanceof ol.style.Style,
                        41); // Expected an `ol.style.Style` or an array of `ol.style.Style`
                    styles = [obj];
                }
                styleFunction = function() {
                    return styles;
                };
            }
            return styleFunction;
        };


        /**
         * @type {Array.<ol.style.Style>}
         * @private
         */
        ol.style.Style.default_ = null;


        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @param {number} resolution Resolution.
         * @return {Array.<ol.style.Style>} Style.
         */
        ol.style.Style.defaultFunction = function(feature, resolution) {
            // We don't use an immediately-invoked function
            // and a closure so we don't get an error at script evaluation time in
            // browsers that do not support Canvas. (ol.style.Circle does
            // canvas.getContext('2d') at construction time, which will cause an.error
            // in such browsers.)
            if (!ol.style.Style.default_) {
                var fill = new ol.style.Fill({
                    color: 'rgba(255,255,255,0.4)'
                });
                var stroke = new ol.style.Stroke({
                    color: '#3399CC',
                    width: 1.25
                });
                ol.style.Style.default_ = [
                    new ol.style.Style({
                        image: new ol.style.Circle({
                            fill: fill,
                            stroke: stroke,
                            radius: 5
                        }),
                        fill: fill,
                        stroke: stroke
                    })
                ];
            }
            return ol.style.Style.default_;
        };


        /**
         * Default styles for editing features.
         * @return {Object.<ol.geom.GeometryType, Array.<ol.style.Style>>} Styles
         */
        ol.style.Style.createDefaultEditing = function() {
            /** @type {Object.<ol.geom.GeometryType, Array.<ol.style.Style>>} */
            var styles = {};
            var white = [255, 255, 255, 1];
            var blue = [0, 153, 255, 1];
            var width = 3;
            styles[ol.geom.GeometryType.POLYGON] = [
                new ol.style.Style({
                    fill: new ol.style.Fill({
                        color: [255, 255, 255, 0.5]
                    })
                })
            ];
            styles[ol.geom.GeometryType.MULTI_POLYGON] =
                styles[ol.geom.GeometryType.POLYGON];

            styles[ol.geom.GeometryType.LINE_STRING] = [
                new ol.style.Style({
                    stroke: new ol.style.Stroke({
                        color: white,
                        width: width + 2
                    })
                }),
                new ol.style.Style({
                    stroke: new ol.style.Stroke({
                        color: blue,
                        width: width
                    })
                })
            ];
            styles[ol.geom.GeometryType.MULTI_LINE_STRING] =
                styles[ol.geom.GeometryType.LINE_STRING];

            styles[ol.geom.GeometryType.CIRCLE] =
                styles[ol.geom.GeometryType.POLYGON].concat(
                    styles[ol.geom.GeometryType.LINE_STRING]
                );


            styles[ol.geom.GeometryType.POINT] = [
                new ol.style.Style({
                    image: new ol.style.Circle({
                        radius: width * 2,
                        fill: new ol.style.Fill({
                            color: blue
                        }),
                        stroke: new ol.style.Stroke({
                            color: white,
                            width: width / 2
                        })
                    }),
                    zIndex: Infinity
                })
            ];
            styles[ol.geom.GeometryType.MULTI_POINT] =
                styles[ol.geom.GeometryType.POINT];

            styles[ol.geom.GeometryType.GEOMETRY_COLLECTION] =
                styles[ol.geom.GeometryType.POLYGON].concat(
                    styles[ol.geom.GeometryType.LINE_STRING],
                    styles[ol.geom.GeometryType.POINT]
                );

            return styles;
        };


        /**
         * Function that is called with a feature and returns its default geometry.
         * @param {ol.Feature|ol.render.Feature} feature Feature to get the geometry
         *     for.
         * @return {ol.geom.Geometry|ol.render.Feature|undefined} Geometry to render.
         */
        ol.style.Style.defaultGeometryFunction = function(feature) {
            return feature.getGeometry();
        };


        ol.ext = {};
        ol.ext.rbush = function() {};

        (function() {(function (exports) {

            var quickselect_1 = quickselect;
            var default_1 = quickselect;
            function quickselect(arr, k, left, right, compare) {
                quickselectStep(arr, k, left || 0, right || (arr.length - 1), compare || defaultCompare);
            }
            function quickselectStep(arr, k, left, right, compare) {
                while (right > left) {
                    if (right - left > 600) {
                        var n = right - left + 1;
                        var m = k - left + 1;
                        var z = Math.log(n);
                        var s = 0.5 * Math.exp(2 * z / 3);
                        var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
                        var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
                        var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
                        quickselectStep(arr, k, newLeft, newRight, compare);
                    }
                    var t = arr[k];
                    var i = left;
                    var j = right;
                    swap(arr, left, k);
                    if (compare(arr[right], t) > 0) swap(arr, left, right);
                    while (i < j) {
                        swap(arr, i, j);
                        i++;
                        j--;
                        while (compare(arr[i], t) < 0) i++;
                        while (compare(arr[j], t) > 0) j--;
                    }
                    if (compare(arr[left], t) === 0) swap(arr, left, j);
                    else {
                        j++;
                        swap(arr, j, right);
                    }
                    if (j <= k) left = j + 1;
                    if (k <= j) right = j - 1;
                }
            }
            function swap(arr, i, j) {
                var tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
            }
            function defaultCompare(a, b) {
                return a < b ? -1 : a > b ? 1 : 0;
            }
            quickselect_1.default = default_1;

            var rbush_1 = rbush;
            function rbush(maxEntries, format) {
                if (!(this instanceof rbush)) return new rbush(maxEntries, format);
                this._maxEntries = Math.max(4, maxEntries || 9);
                this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));
                if (format) {
                    this._initFormat(format);
                }
                this.clear();
            }
            rbush.prototype = {
                all: function () {
                    return this._all(this.data, []);
                },
                search: function (bbox) {
                    var node = this.data,
                        result = [],
                        toBBox = this.toBBox;
                    if (!intersects(bbox, node)) return result;
                    var nodesToSearch = [],
                        i, len, child, childBBox;
                    while (node) {
                        for (i = 0, len = node.children.length; i < len; i++) {
                            child = node.children[i];
                            childBBox = node.leaf ? toBBox(child) : child;
                            if (intersects(bbox, childBBox)) {
                                if (node.leaf) result.push(child);
                                else if (contains(bbox, childBBox)) this._all(child, result);
                                else nodesToSearch.push(child);
                            }
                        }
                        node = nodesToSearch.pop();
                    }
                    return result;
                },
                collides: function (bbox) {
                    var node = this.data,
                        toBBox = this.toBBox;
                    if (!intersects(bbox, node)) return false;
                    var nodesToSearch = [],
                        i, len, child, childBBox;
                    while (node) {
                        for (i = 0, len = node.children.length; i < len; i++) {
                            child = node.children[i];
                            childBBox = node.leaf ? toBBox(child) : child;
                            if (intersects(bbox, childBBox)) {
                                if (node.leaf || contains(bbox, childBBox)) return true;
                                nodesToSearch.push(child);
                            }
                        }
                        node = nodesToSearch.pop();
                    }
                    return false;
                },
                load: function (data) {
                    if (!(data && data.length)) return this;
                    if (data.length < this._minEntries) {
                        for (var i = 0, len = data.length; i < len; i++) {
                            this.insert(data[i]);
                        }
                        return this;
                    }
                    var node = this._build(data.slice(), 0, data.length - 1, 0);
                    if (!this.data.children.length) {
                        this.data = node;
                    } else if (this.data.height === node.height) {
                        this._splitRoot(this.data, node);
                    } else {
                        if (this.data.height < node.height) {
                            var tmpNode = this.data;
                            this.data = node;
                            node = tmpNode;
                        }
                        this._insert(node, this.data.height - node.height - 1, true);
                    }
                    return this;
                },
                insert: function (item) {
                    if (item) this._insert(item, this.data.height - 1);
                    return this;
                },
                clear: function () {
                    this.data = createNode([]);
                    return this;
                },
                remove: function (item, equalsFn) {
                    if (!item) return this;
                    var node = this.data,
                        bbox = this.toBBox(item),
                        path = [],
                        indexes = [],
                        i, parent, index, goingUp;
                    while (node || path.length) {
                        if (!node) {
                            node = path.pop();
                            parent = path[path.length - 1];
                            i = indexes.pop();
                            goingUp = true;
                        }
                        if (node.leaf) {
                            index = findItem(item, node.children, equalsFn);
                            if (index !== -1) {
                                node.children.splice(index, 1);
                                path.push(node);
                                this._condense(path);
                                return this;
                            }
                        }
                        if (!goingUp && !node.leaf && contains(node, bbox)) {
                            path.push(node);
                            indexes.push(i);
                            i = 0;
                            parent = node;
                            node = node.children[0];
                        } else if (parent) {
                            i++;
                            node = parent.children[i];
                            goingUp = false;
                        } else node = null;
                    }
                    return this;
                },
                toBBox: function (item) { return item; },
                compareMinX: compareNodeMinX,
                compareMinY: compareNodeMinY,
                toJSON: function () { return this.data; },
                fromJSON: function (data) {
                    this.data = data;
                    return this;
                },
                _all: function (node, result) {
                    var nodesToSearch = [];
                    while (node) {
                        if (node.leaf) result.push.apply(result, node.children);
                        else nodesToSearch.push.apply(nodesToSearch, node.children);
                        node = nodesToSearch.pop();
                    }
                    return result;
                },
                _build: function (items, left, right, height) {
                    var N = right - left + 1,
                        M = this._maxEntries,
                        node;
                    if (N <= M) {
                        node = createNode(items.slice(left, right + 1));
                        calcBBox(node, this.toBBox);
                        return node;
                    }
                    if (!height) {
                        height = Math.ceil(Math.log(N) / Math.log(M));
                        M = Math.ceil(N / Math.pow(M, height - 1));
                    }
                    node = createNode([]);
                    node.leaf = false;
                    node.height = height;
                    var N2 = Math.ceil(N / M),
                        N1 = N2 * Math.ceil(Math.sqrt(M)),
                        i, j, right2, right3;
                    multiSelect(items, left, right, N1, this.compareMinX);
                    for (i = left; i <= right; i += N1) {
                        right2 = Math.min(i + N1 - 1, right);
                        multiSelect(items, i, right2, N2, this.compareMinY);
                        for (j = i; j <= right2; j += N2) {
                            right3 = Math.min(j + N2 - 1, right2);
                            node.children.push(this._build(items, j, right3, height - 1));
                        }
                    }
                    calcBBox(node, this.toBBox);
                    return node;
                },
                _chooseSubtree: function (bbox, node, level, path) {
                    var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;
                    while (true) {
                        path.push(node);
                        if (node.leaf || path.length - 1 === level) break;
                        minArea = minEnlargement = Infinity;
                        for (i = 0, len = node.children.length; i < len; i++) {
                            child = node.children[i];
                            area = bboxArea(child);
                            enlargement = enlargedArea(bbox, child) - area;
                            if (enlargement < minEnlargement) {
                                minEnlargement = enlargement;
                                minArea = area < minArea ? area : minArea;
                                targetNode = child;
                            } else if (enlargement === minEnlargement) {
                                if (area < minArea) {
                                    minArea = area;
                                    targetNode = child;
                                }
                            }
                        }
                        node = targetNode || node.children[0];
                    }
                    return node;
                },
                _insert: function (item, level, isNode) {
                    var toBBox = this.toBBox,
                        bbox = isNode ? item : toBBox(item),
                        insertPath = [];
                    var node = this._chooseSubtree(bbox, this.data, level, insertPath);
                    node.children.push(item);
                    extend(node, bbox);
                    while (level >= 0) {
                        if (insertPath[level].children.length > this._maxEntries) {
                            this._split(insertPath, level);
                            level--;
                        } else break;
                    }
                    this._adjustParentBBoxes(bbox, insertPath, level);
                },
                _split: function (insertPath, level) {
                    var node = insertPath[level],
                        M = node.children.length,
                        m = this._minEntries;
                    this._chooseSplitAxis(node, m, M);
                    var splitIndex = this._chooseSplitIndex(node, m, M);
                    var newNode = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
                    newNode.height = node.height;
                    newNode.leaf = node.leaf;
                    calcBBox(node, this.toBBox);
                    calcBBox(newNode, this.toBBox);
                    if (level) insertPath[level - 1].children.push(newNode);
                    else this._splitRoot(node, newNode);
                },
                _splitRoot: function (node, newNode) {
                    this.data = createNode([node, newNode]);
                    this.data.height = node.height + 1;
                    this.data.leaf = false;
                    calcBBox(this.data, this.toBBox);
                },
                _chooseSplitIndex: function (node, m, M) {
                    var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;
                    minOverlap = minArea = Infinity;
                    for (i = m; i <= M - m; i++) {
                        bbox1 = distBBox(node, 0, i, this.toBBox);
                        bbox2 = distBBox(node, i, M, this.toBBox);
                        overlap = intersectionArea(bbox1, bbox2);
                        area = bboxArea(bbox1) + bboxArea(bbox2);
                        if (overlap < minOverlap) {
                            minOverlap = overlap;
                            index = i;
                            minArea = area < minArea ? area : minArea;
                        } else if (overlap === minOverlap) {
                            if (area < minArea) {
                                minArea = area;
                                index = i;
                            }
                        }
                    }
                    return index;
                },
                _chooseSplitAxis: function (node, m, M) {
                    var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
                        compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
                        xMargin = this._allDistMargin(node, m, M, compareMinX),
                        yMargin = this._allDistMargin(node, m, M, compareMinY);
                    if (xMargin < yMargin) node.children.sort(compareMinX);
                },
                _allDistMargin: function (node, m, M, compare) {
                    node.children.sort(compare);
                    var toBBox = this.toBBox,
                        leftBBox = distBBox(node, 0, m, toBBox),
                        rightBBox = distBBox(node, M - m, M, toBBox),
                        margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
                        i, child;
                    for (i = m; i < M - m; i++) {
                        child = node.children[i];
                        extend(leftBBox, node.leaf ? toBBox(child) : child);
                        margin += bboxMargin(leftBBox);
                    }
                    for (i = M - m - 1; i >= m; i--) {
                        child = node.children[i];
                        extend(rightBBox, node.leaf ? toBBox(child) : child);
                        margin += bboxMargin(rightBBox);
                    }
                    return margin;
                },
                _adjustParentBBoxes: function (bbox, path, level) {
                    for (var i = level; i >= 0; i--) {
                        extend(path[i], bbox);
                    }
                },
                _condense: function (path) {
                    for (var i = path.length - 1, siblings; i >= 0; i--) {
                        if (path[i].children.length === 0) {
                            if (i > 0) {
                                siblings = path[i - 1].children;
                                siblings.splice(siblings.indexOf(path[i]), 1);
                            } else this.clear();
                        } else calcBBox(path[i], this.toBBox);
                    }
                },
                _initFormat: function (format) {
                    var compareArr = ['return a', ' - b', ';'];
                    this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));
                    this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));
                    this.toBBox = new Function('a',
                        'return {minX: a' + format[0] +
                            ', minY: a' + format[1] +
                            ', maxX: a' + format[2] +
                            ', maxY: a' + format[3] + '};');
                }
            };
            function findItem(item, items, equalsFn) {
                if (!equalsFn) return items.indexOf(item);
                for (var i = 0; i < items.length; i++) {
                    if (equalsFn(item, items[i])) return i;
                }
                return -1;
            }
            function calcBBox(node, toBBox) {
                distBBox(node, 0, node.children.length, toBBox, node);
            }
            function distBBox(node, k, p, toBBox, destNode) {
                if (!destNode) destNode = createNode(null);
                destNode.minX = Infinity;
                destNode.minY = Infinity;
                destNode.maxX = -Infinity;
                destNode.maxY = -Infinity;
                for (var i = k, child; i < p; i++) {
                    child = node.children[i];
                    extend(destNode, node.leaf ? toBBox(child) : child);
                }
                return destNode;
            }
            function extend(a, b) {
                a.minX = Math.min(a.minX, b.minX);
                a.minY = Math.min(a.minY, b.minY);
                a.maxX = Math.max(a.maxX, b.maxX);
                a.maxY = Math.max(a.maxY, b.maxY);
                return a;
            }
            function compareNodeMinX(a, b) { return a.minX - b.minX; }
            function compareNodeMinY(a, b) { return a.minY - b.minY; }
            function bboxArea(a)   { return (a.maxX - a.minX) * (a.maxY - a.minY); }
            function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
            function enlargedArea(a, b) {
                return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
                    (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
            }
            function intersectionArea(a, b) {
                var minX = Math.max(a.minX, b.minX),
                    minY = Math.max(a.minY, b.minY),
                    maxX = Math.min(a.maxX, b.maxX),
                    maxY = Math.min(a.maxY, b.maxY);
                return Math.max(0, maxX - minX) *
                    Math.max(0, maxY - minY);
            }
            function contains(a, b) {
                return a.minX <= b.minX &&
                    a.minY <= b.minY &&
                    b.maxX <= a.maxX &&
                    b.maxY <= a.maxY;
            }
            function intersects(a, b) {
                return b.minX <= a.maxX &&
                    b.minY <= a.maxY &&
                    b.maxX >= a.minX &&
                    b.maxY >= a.minY;
            }
            function createNode(children) {
                return {
                    children: children,
                    height: 1,
                    leaf: true,
                    minX: Infinity,
                    minY: Infinity,
                    maxX: -Infinity,
                    maxY: -Infinity
                };
            }
            function multiSelect(arr, left, right, n, compare) {
                var stack = [left, right],
                    mid;
                while (stack.length) {
                    right = stack.pop();
                    left = stack.pop();
                    if (right - left <= n) continue;
                    mid = left + Math.ceil((right - left) / n / 2) * n;
                    quickselect_1(arr, mid, left, right, compare);
                    stack.push(left, mid, mid, right);
                }
            }

            exports['default'] = rbush_1;

        }((this.rbush = this.rbush || {})));}).call(ol.ext);
        ol.ext.rbush = ol.ext.rbush.default;

        ol.render = {};

        /**
         * Context for drawing geometries.  A vector context is available on render
         * events and does not need to be constructed directly.
         * @constructor
         * @abstract
         * @struct
         * @api
         */
        ol.render.VectorContext = function() {
        };

        ol.render.VectorContext.prototype.drawCustom = function(geometry, feature, renderer) {};
        ol.render.VectorContext.prototype.drawGeometry = function(geometry) {};
        ol.render.VectorContext.prototype.setStyle = function(style) {};
        ol.render.VectorContext.prototype.drawCircle = function(circleGeometry, feature) {};
        ol.render.VectorContext.prototype.drawFeature = function(feature, style) {};
        ol.render.VectorContext.prototype.drawGeometryCollection = function(geometryCollectionGeometry, feature) {};
        ol.render.VectorContext.prototype.drawLineString = function(lineStringGeometry, feature) {};
        ol.render.VectorContext.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {};
        ol.render.VectorContext.prototype.drawMultiPoint = function(multiPointGeometry, feature) {};
        ol.render.VectorContext.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) {};
        ol.render.VectorContext.prototype.drawPoint = function(pointGeometry, feature) {};
        ol.render.VectorContext.prototype.drawPolygon = function(polygonGeometry, feature) {};
        ol.render.VectorContext.prototype.drawText = function(geometry, feature) {};
        ol.render.VectorContext.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {};
        ol.render.VectorContext.prototype.setImageStyle = function(imageStyle, opt_declutterGroup) {};
        ol.render.VectorContext.prototype.setTextStyle = function(textStyle, opt_declutterGroup) {};

        ol.render.ReplayGroup = {};

        ol.render.ReplayGroup = function() {};

        ol.render.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {};

        ol.render.ReplayGroup.prototype.isEmpty = function() {};

        ol.render.ReplayType = {
            CIRCLE: 'Circle',
            DEFAULT: 'Default',
            IMAGE: 'Image',
            LINE_STRING: 'LineString',
            POLYGON: 'Polygon',
            TEXT: 'Text'
        };

        ol.geom.flat.length = {};

        ol.geom.flat.length.lineString = function(flatCoordinates, offset, end, stride) {
            var x1 = flatCoordinates[offset];
            var y1 = flatCoordinates[offset + 1];
            var length = 0;
            var i;
            for (i = offset + stride; i < end; i += stride) {
                var x2 = flatCoordinates[i];
                var y2 = flatCoordinates[i + 1];
                length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
                x1 = x2;
                y1 = y2;
            }
            return length;
        };

        ol.geom.flat.length.linearRing = function(flatCoordinates, offset, end, stride) {
            var perimeter =
                ol.geom.flat.length.lineString(flatCoordinates, offset, end, stride);
            var dx = flatCoordinates[end - stride] - flatCoordinates[offset];
            var dy = flatCoordinates[end - stride + 1] - flatCoordinates[offset + 1];
            perimeter += Math.sqrt(dx * dx + dy * dy);
            return perimeter;
        };

        ol.geom.flat.textpath = {};

        ol.geom.flat.textpath.lineString = function(
            flatCoordinates, offset, end, stride, text, measure, startM, maxAngle) {
            var result = [];

            // Keep text upright
            var reverse = flatCoordinates[offset] > flatCoordinates[end - stride];

            var numChars = text.length;

            var x1 = flatCoordinates[offset];
            var y1 = flatCoordinates[offset + 1];
            offset += stride;
            var x2 = flatCoordinates[offset];
            var y2 = flatCoordinates[offset + 1];
            var segmentM = 0;
            var segmentLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));

            var chunk = '';
            var chunkLength = 0;
            var data, index, previousAngle;
            for (var i = 0; i < numChars; ++i) {
                index = reverse ? numChars - i - 1 : i;
                var char = text.charAt(index);
                chunk = reverse ? char + chunk : chunk + char;
                var charLength = measure(chunk) - chunkLength;
                chunkLength += charLength;
                var charM = startM + charLength / 2;
                while (offset < end - stride && segmentM + segmentLength < charM) {
                    x1 = x2;
                    y1 = y2;
                    offset += stride;
                    x2 = flatCoordinates[offset];
                    y2 = flatCoordinates[offset + 1];
                    segmentM += segmentLength;
                    segmentLength = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
                }
                var segmentPos = charM - segmentM;
                var angle = Math.atan2(y2 - y1, x2 - x1);
                if (reverse) {
                    angle += angle > 0 ? -Math.PI : Math.PI;
                }
                if (previousAngle !== undefined) {
                    var delta = angle - previousAngle;
                    delta += (delta > Math.PI) ? -2 * Math.PI : (delta < -Math.PI) ? 2 * Math.PI : 0;
                    if (Math.abs(delta) > maxAngle) {
                        return null;
                    }
                }
                var interpolate = segmentPos / segmentLength;
                var x = ol.math.lerp(x1, x2, interpolate);
                var y = ol.math.lerp(y1, y2, interpolate);
                if (previousAngle == angle) {
                    if (reverse) {
                        data[0] = x;
                        data[1] = y;
                        data[2] = charLength / 2;
                    }
                    data[4] = chunk;
                } else {
                    chunk = char;
                    chunkLength = charLength;
                    data = [x, y, charLength / 2, angle, chunk];
                    if (reverse) {
                        result.unshift(data);
                    } else {
                        result.push(data);
                    }
                    previousAngle = angle;
                }
                startM += charLength;
            }
            return result;
        };


        ol.structs.LRUCache = function(opt_highWaterMark) {
            ol.events.EventTarget.call(this);
            this.highWaterMark = opt_highWaterMark !== undefined ? opt_highWaterMark : 2048;
            this.count_ = 0;
            this.entries_ = {};
            this.oldest_ = null;
            this.newest_ = null;
        };

        ol.inherits(ol.structs.LRUCache, ol.events.EventTarget);

        ol.structs.LRUCache.prototype.canExpireCache = function() {
            return this.getCount() > this.highWaterMark;
        };

        ol.structs.LRUCache.prototype.clear = function() {
            this.count_ = 0;
            this.entries_ = {};
            this.oldest_ = null;
            this.newest_ = null;
            this.dispatchEvent(ol.events.EventType.CLEAR);
        };

        ol.structs.LRUCache.prototype.containsKey = function(key) {
            return this.entries_.hasOwnProperty(key);
        };

        ol.structs.LRUCache.prototype.forEach = function(f, opt_this) {
            var entry = this.oldest_;
            while (entry) {
                f.call(opt_this, entry.value_, entry.key_, this);
                entry = entry.newer;
            }
        };

        ol.structs.LRUCache.prototype.get = function(key) {
            var entry = this.entries_[key];
            ol.asserts.assert(entry !== undefined,
                15); // Tried to get a value for a key that does not exist in the cache
            if (entry === this.newest_) {
                return entry.value_;
            } else if (entry === this.oldest_) {
                this.oldest_ = /** @type {ol.LRUCacheEntry} */ (this.oldest_.newer);
                this.oldest_.older = null;
            } else {
                entry.newer.older = entry.older;
                entry.older.newer = entry.newer;
            }
            entry.newer = null;
            entry.older = this.newest_;
            this.newest_.newer = entry;
            this.newest_ = entry;
            return entry.value_;
        };

        ol.structs.LRUCache.prototype.remove = function(key) {
            var entry = this.entries_[key];
            ol.asserts.assert(entry !== undefined, 15); // Tried to get a value for a key that does not exist in the cache
            if (entry === this.newest_) {
                this.newest_ = /** @type {ol.LRUCacheEntry} */ (entry.older);
                if (this.newest_) {
                    this.newest_.newer = null;
                }
            } else if (entry === this.oldest_) {
                this.oldest_ = /** @type {ol.LRUCacheEntry} */ (entry.newer);
                if (this.oldest_) {
                    this.oldest_.older = null;
                }
            } else {
                entry.newer.older = entry.older;
                entry.older.newer = entry.newer;
            }
            delete this.entries_[key];
            --this.count_;
            return entry.value_;
        };

        ol.structs.LRUCache.prototype.getCount = function() {
            return this.count_;
        };

        ol.structs.LRUCache.prototype.getKeys = function() {
            var keys = new Array(this.count_);
            var i = 0;
            var entry;
            for (entry = this.newest_; entry; entry = entry.older) {
                keys[i++] = entry.key_;
            }
            return keys;
        };

        ol.structs.LRUCache.prototype.getValues = function() {
            var values = new Array(this.count_);
            var i = 0;
            var entry;
            for (entry = this.newest_; entry; entry = entry.older) {
                values[i++] = entry.value_;
            }
            return values;
        };

        ol.structs.LRUCache.prototype.peekLast = function() {
            return this.oldest_.value_;
        };

        ol.structs.LRUCache.prototype.peekLastKey = function() {
            return this.oldest_.key_;
        };

        ol.structs.LRUCache.prototype.peekFirstKey = function() {
            return this.newest_.key_;
        };

        ol.structs.LRUCache.prototype.pop = function() {
            var entry = this.oldest_;
            delete this.entries_[entry.key_];
            if (entry.newer) {
                entry.newer.older = null;
            }
            this.oldest_ = /** @type {ol.LRUCacheEntry} */ (entry.newer);
            if (!this.oldest_) {
                this.newest_ = null;
            }
            --this.count_;
            return entry.value_;
        };

        ol.structs.LRUCache.prototype.replace = function(key, value) {
            this.get(key);  // update `newest_`
            this.entries_[key].value_ = value;
        };

        ol.structs.LRUCache.prototype.set = function(key, value) {
            ol.asserts.assert(!(key in this.entries_),
                16); // Tried to set a value for a key that is used already
            var entry = /** @type {ol.LRUCacheEntry} */ ({
                key_: key,
                newer: null,
                older: this.newest_,
                value_: value
            });
            if (!this.newest_) {
                this.oldest_ = entry;
            } else {
                this.newest_.newer = entry;
            }
            this.newest_ = entry;
            this.entries_[key] = entry;
            ++this.count_;
        };

        ol.structs.LRUCache.prototype.prune = function() {
            while (this.canExpireCache()) {
                this.pop();
            }
        };

        ol.render.canvas = {};
        ol.render.canvas.defaultFont = '10px sans-serif';
        ol.render.canvas.defaultFillStyle = [0, 0, 0, 1];
        ol.render.canvas.defaultLineCap = 'round';
        ol.render.canvas.defaultLineDash = [];
        ol.render.canvas.defaultLineDashOffset = 0;
        ol.render.canvas.defaultLineJoin = 'round';
        ol.render.canvas.defaultMiterLimit = 10;
        ol.render.canvas.defaultStrokeStyle = [0, 0, 0, 1];
        ol.render.canvas.defaultTextAlign = 'center';
        ol.render.canvas.defaultTextBaseline = 'middle';
        ol.render.canvas.defaultPadding = [0, 0, 0, 0];
        ol.render.canvas.defaultLineWidth = 1;
        ol.render.canvas.labelCache = new ol.structs.LRUCache();
        ol.render.canvas.checkedFonts_ = {};
        ol.render.canvas.measureContext_ = null;
        ol.render.canvas.textHeights_ = {};

        /**
         * Clears the label cache when a font becomes available.
         * @param {string} fontSpec CSS font spec.
         */
        ol.render.canvas.checkFont = (function() {
            var retries = 60;
            var checked = ol.render.canvas.checkedFonts_;
            var labelCache = ol.render.canvas.labelCache;
            var font = '32px monospace';
            var text = 'wmytzilWMYTZIL@#/&?$%10';
            var interval, referenceWidth;

            function isAvailable(fontFamily) {
                var context = ol.render.canvas.getMeasureContext();
                context.font = font;
                referenceWidth = context.measureText(text).width;
                var available = true;
                if (fontFamily != 'monospace') {
                    context.font = '32px ' + fontFamily + ',monospace';
                    var width = context.measureText(text).width;
                    // If width and referenceWidth are the same, then the 'monospace'
                    // fallback was used instead of the font we wanted, so the font is not
                    // available.
                    available = width != referenceWidth;
                }
                return available;
            }

            function check() {
                var done = true;
                for (var font in checked) {
                    if (checked[font] < retries) {
                        if (isAvailable(font)) {
                            checked[font] = retries;
                            ol.obj.clear(ol.render.canvas.textHeights_);
                            // Make sure that loaded fonts are picked up by Safari
                            ol.render.canvas.measureContext_ = null;
                            labelCache.clear();
                        } else {
                            ++checked[font];
                            done = false;
                        }
                    }
                }
                if (done) {
                    window.clearInterval(interval);
                    interval = undefined;
                }
            }

            return function(fontSpec) {
                var fontFamilies = ol.css.getFontFamilies(fontSpec);
                if (!fontFamilies) {
                    return;
                }
                for (var i = 0, ii = fontFamilies.length; i < ii; ++i) {
                    var fontFamily = fontFamilies[i];
                    if (!(fontFamily in checked)) {
                        checked[fontFamily] = retries;
                        if (!isAvailable(fontFamily)) {
                            checked[fontFamily] = 0;
                            if (interval === undefined) {
                                interval = window.setInterval(check, 32);
                            }
                        }
                    }
                }
            };
        })();


        /**
         * @return {CanvasRenderingContext2D} Measure context.
         */
        ol.render.canvas.getMeasureContext = function() {
            var context = ol.render.canvas.measureContext_;
            if (!context) {
                context = ol.render.canvas.measureContext_ = ol.dom.createCanvasContext2D(1, 1);
            }
            return context;
        };


        /**
         * @param {string} font Font to use for measuring.
         * @return {ol.Size} Measurement.
         */
        ol.render.canvas.measureTextHeight = (function() {
            var heights = ol.render.canvas.textHeights_;
            return function(font) {
                var height = heights[font];
                if (height == undefined) {
    //                if (!span) {
    //                    span = document.createElement('span');
    //                    span.textContent = 'M';
    //                    span.style.margin = span.style.padding = '0 !important';
    //                    span.style.position = 'absolute !important';
    //                    span.style.left = '-99999px !important';
    //                }
    //                span.style.font = font;
    //                document.body.appendChild(span);
    //                height = heights[font] = span.offsetHeight;
    //                document.body.removeChild(span);
                    height = heights[font] = 19; // FIXME sunyl
                }
                return height;
            };
        })();


        /**
         * @param {string} font Font.
         * @param {string} text Text.
         * @return {number} Width.
         */
        ol.render.canvas.measureTextWidth = function(font, text) {
            var measureContext = ol.render.canvas.getMeasureContext();
            if (font != measureContext.font) {
                measureContext.font = font;
            }
            return measureContext.measureText(text).width;
        };


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {number} rotation Rotation.
         * @param {number} offsetX X offset.
         * @param {number} offsetY Y offset.
         */
        ol.render.canvas.rotateAtOffset = function(context, rotation, offsetX, offsetY) {
            if (rotation !== 0) {
                context.translate(offsetX, offsetY);
                context.rotate(rotation);
                context.translate(-offsetX, -offsetY);
            }
        };


        ol.render.canvas.resetTransform_ = ol.transform.create();


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Transform|null} transform Transform.
         * @param {number} opacity Opacity.
         * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
         * @param {number} originX Origin X.
         * @param {number} originY Origin Y.
         * @param {number} w Width.
         * @param {number} h Height.
         * @param {number} x X.
         * @param {number} y Y.
         * @param {number} scale Scale.
         */
        ol.render.canvas.drawImage = function(context,
                                              transform, opacity, image, originX, originY, w, h, x, y, scale) {
            var alpha;
            if (opacity != 1) {
                alpha = context.globalAlpha;
                context.globalAlpha = alpha * opacity;
            }
            if (transform) {
                context.setTransform.apply(context, transform);
            }

            context.drawImage(image, originX, originY, w, h, x, y, w * scale, h * scale);

            if (alpha) {
                context.globalAlpha = alpha;
            }
            if (transform) {
                context.setTransform.apply(context, ol.render.canvas.resetTransform_);
            }
        };


        /**
         * @enum {number}
         */
        ol.render.canvas.Instruction = {
            BEGIN_GEOMETRY: 0,
            BEGIN_PATH: 1,
            CIRCLE: 2,
            CLOSE_PATH: 3,
            CUSTOM: 4,
            DRAW_CHARS: 5,
            DRAW_IMAGE: 6,
            END_GEOMETRY: 7,
            FILL: 8,
            MOVE_TO_LINE_TO: 9,
            SET_FILL_STYLE: 10,
            SET_STROKE_STYLE: 11,
            STROKE: 12
        };

        ol.render.replay = {};

        ol.render.replay.ORDER = [
            ol.render.ReplayType.POLYGON,
            ol.render.ReplayType.CIRCLE,
            ol.render.ReplayType.LINE_STRING,
            ol.render.ReplayType.IMAGE,
            ol.render.ReplayType.TEXT,
            ol.render.ReplayType.DEFAULT
        ];

        ol.render.replay.TEXT_ALIGN = {};
        ol.render.replay.TEXT_ALIGN['left'] = 0;
        ol.render.replay.TEXT_ALIGN['end'] = 0;
        ol.render.replay.TEXT_ALIGN['center'] = 0.5;
        ol.render.replay.TEXT_ALIGN['right'] = 1;
        ol.render.replay.TEXT_ALIGN['start'] = 1;
        ol.render.replay.TEXT_ALIGN['top'] = 0;
        ol.render.replay.TEXT_ALIGN['middle'] = 0.5;
        ol.render.replay.TEXT_ALIGN['hanging'] = 0.2;
        ol.render.replay.TEXT_ALIGN['alphabetic'] = 0.8;
        ol.render.replay.TEXT_ALIGN['ideographic'] = 0.8;
        ol.render.replay.TEXT_ALIGN['bottom'] = 1;


        ol.render.canvas.Replay = function(tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
            ol.render.VectorContext.call(this);

            /**
             * @type {?}
             */
            this.declutterTree = declutterTree;

            /**
             * @private
             * @type {ol.Extent}
             */
            this.tmpExtent_ = ol.extent.createEmpty();

            /**
             * @protected
             * @type {number}
             */
            this.tolerance = tolerance;

            /**
             * @protected
             * @const
             * @type {ol.Extent}
             */
            this.maxExtent = maxExtent;

            /**
             * @protected
             * @type {boolean}
             */
            this.overlaps = overlaps;

            /**
             * @protected
             * @type {number}
             */
            this.pixelRatio = pixelRatio;

            /**
             * @protected
             * @type {number}
             */
            this.maxLineWidth = 0;

            /**
             * @protected
             * @const
             * @type {number}
             */
            this.resolution = resolution;

            /**
             * @private
             * @type {ol.Coordinate}
             */
            this.fillOrigin_;

            /**
             * @private
             * @type {Array.<*>}
             */
            this.beginGeometryInstruction1_ = null;

            /**
             * @private
             * @type {Array.<*>}
             */
            this.beginGeometryInstruction2_ = null;

            /**
             * @private
             * @type {ol.Extent}
             */
            this.bufferedMaxExtent_ = null;

            /**
             * @protected
             * @type {Array.<*>}
             */
            this.instructions = [];

            /**
             * @protected
             * @type {Array.<number>}
             */
            this.coordinates = [];

            /**
             * @private
             * @type {Object.<number,ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>>}
             */
            this.coordinateCache_ = {};

            /**
             * @private
             * @type {!ol.Transform}
             */
            this.renderedTransform_ = ol.transform.create();

            /**
             * @protected
             * @type {Array.<*>}
             */
            this.hitDetectionInstructions = [];

            /**
             * @private
             * @type {Array.<number>}
             */
            this.pixelCoordinates_ = null;

            /**
             * @protected
             * @type {ol.CanvasFillStrokeState}
             */
            this.state = /** @type {ol.CanvasFillStrokeState} */ ({});

            /**
             * @private
             * @type {number}
             */
            this.viewRotation_ = 0;

            /**
             * @private
             * @type {!ol.Transform}
             */
            this.tmpLocalTransform_ = ol.transform.create();

            /**
             * @private
             * @type {!ol.Transform}
             */
            this.resetTransform_ = ol.transform.create();
        };
        ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext);


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Coordinate} p1 1st point of the background box.
         * @param {ol.Coordinate} p2 2nd point of the background box.
         * @param {ol.Coordinate} p3 3rd point of the background box.
         * @param {ol.Coordinate} p4 4th point of the background box.
         * @param {Array.<*>} fillInstruction Fill instruction.
         * @param {Array.<*>} strokeInstruction Stroke instruction.
         */
        ol.render.canvas.Replay.prototype.replayTextBackground_ = function(context, p1, p2, p3, p4,
                                                                           fillInstruction, strokeInstruction) {
            context.beginPath();
            context.moveTo.apply(context, p1);
            context.lineTo.apply(context, p2);
            context.lineTo.apply(context, p3);
            context.lineTo.apply(context, p4);
            context.lineTo.apply(context, p1);
            if (fillInstruction) {
                this.fillOrigin_ = /** @type {Array.<number>} */ (fillInstruction[2]);
                this.fill_(context);
            }
            if (strokeInstruction) {
                this.setStrokeStyle_(context, /** @type {Array.<*>} */ (strokeInstruction));
                context.stroke();
            }
        };


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {number} x X.
         * @param {number} y Y.
         * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} image Image.
         * @param {number} anchorX Anchor X.
         * @param {number} anchorY Anchor Y.
         * @param {ol.DeclutterGroup} declutterGroup Declutter group.
         * @param {number} height Height.
         * @param {number} opacity Opacity.
         * @param {number} originX Origin X.
         * @param {number} originY Origin Y.
         * @param {number} rotation Rotation.
         * @param {number} scale Scale.
         * @param {boolean} snapToPixel Snap to pixel.
         * @param {number} width Width.
         * @param {Array.<number>} padding Padding.
         * @param {Array.<*>} fillInstruction Fill instruction.
         * @param {Array.<*>} strokeInstruction Stroke instruction.
         */
        ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image,
                                                                  anchorX, anchorY, declutterGroup, height, opacity, originX, originY,
                                                                  rotation, scale, snapToPixel, width, padding, fillInstruction, strokeInstruction) {
            var fillStroke = fillInstruction || strokeInstruction;
            var localTransform = this.tmpLocalTransform_;
            anchorX *= scale;
            anchorY *= scale;
            x -= anchorX;
            y -= anchorY;
            if (snapToPixel) {
                x = Math.round(x);
                y = Math.round(y);
            }

            var w = (width + originX > image.width) ? image.width - originX : width;
            var h = (height + originY > image.height) ? image.height - originY : height;
            var box = this.tmpExtent_;
            var boxW = padding[3] + w * scale + padding[1];
            var boxH = padding[0] + h * scale + padding[2];
            var boxX = x - padding[3];
            var boxY = y - padding[0];

            /** @type {ol.Coordinate} */
            var p1;
            /** @type {ol.Coordinate} */
            var p2;
            /** @type {ol.Coordinate} */
            var p3;
            /** @type {ol.Coordinate} */
            var p4;
            if (fillStroke || rotation !== 0) {
                p1 = [boxX, boxY];
                p2 = [boxX + boxW, boxY];
                p3 = [boxX + boxW, boxY + boxH];
                p4 = [boxX, boxY + boxH];
            }

            var transform = null;
            if (rotation !== 0) {
                var centerX = x + anchorX;
                var centerY = y + anchorY;
                transform = ol.transform.compose(localTransform,
                    centerX, centerY, 1, 1, rotation, -centerX, -centerY);

                ol.extent.createOrUpdateEmpty(box);
                ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, p1));
                ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, p2));
                ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, p3));
                ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, p4));
            } else {
                ol.extent.createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, box);
            }
            var canvas = context.canvas;
            var intersects = box[0] <= canvas.width && box[2] >= 0 && box[1] <= canvas.height && box[3] >= 0;
            if (declutterGroup) {
                if (!intersects && declutterGroup[4] == 1) {
                    return;
                }
                ol.extent.extend(declutterGroup, box);
                var declutterArgs = intersects ?
                    [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] :
                    null;
                if (declutterArgs && fillStroke) {
                    declutterArgs.push(fillInstruction, strokeInstruction, p1, p2, p3, p4);
                }
                declutterGroup.push(declutterArgs);
            } else if (intersects) {
                if (fillStroke) {
                    this.replayTextBackground_(context, p1, p2, p3, p4,
                        /** @type {Array.<*>} */ (fillInstruction),
                        /** @type {Array.<*>} */ (strokeInstruction));
                }
                ol.render.canvas.drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale);
            }
        };


        /**
         * @protected
         * @param {Array.<number>} dashArray Dash array.
         * @return {Array.<number>} Dash array with pixel ratio applied
         */
        ol.render.canvas.Replay.prototype.applyPixelRatio = function(dashArray) {
            var pixelRatio = this.pixelRatio;
            return pixelRatio == 1 ? dashArray : dashArray.map(function(dash) {
                return dash * pixelRatio;
            });
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @param {boolean} closed Last input coordinate equals first.
         * @param {boolean} skipFirst Skip first coordinate.
         * @protected
         * @return {number} My end.
         */
        ol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, closed, skipFirst) {

            var myEnd = this.coordinates.length;
            var extent = this.getBufferedMaxExtent();
            if (skipFirst) {
                offset += stride;
            }
            var lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]];
            var nextCoord = [NaN, NaN];
            var skipped = true;

            var i, lastRel, nextRel;
            for (i = offset + stride; i < end; i += stride) {
                nextCoord[0] = flatCoordinates[i];
                nextCoord[1] = flatCoordinates[i + 1];
                nextRel = ol.extent.coordinateRelationship(extent, nextCoord);
                if (nextRel !== lastRel) {
                    if (skipped) {
                        this.coordinates[myEnd++] = lastCoord[0];
                        this.coordinates[myEnd++] = lastCoord[1];
                    }
                    this.coordinates[myEnd++] = nextCoord[0];
                    this.coordinates[myEnd++] = nextCoord[1];
                    skipped = false;
                } else if (nextRel === ol.extent.Relationship.INTERSECTING) {
                    this.coordinates[myEnd++] = nextCoord[0];
                    this.coordinates[myEnd++] = nextCoord[1];
                    skipped = false;
                } else {
                    skipped = true;
                }
                lastCoord[0] = nextCoord[0];
                lastCoord[1] = nextCoord[1];
                lastRel = nextRel;
            }

            // Last coordinate equals first or only one point to append:
            if ((closed && skipped) || i === offset + stride) {
                this.coordinates[myEnd++] = lastCoord[0];
                this.coordinates[myEnd++] = lastCoord[1];
            }
            return myEnd;
        };


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<number>} ends Ends.
         * @param {number} stride Stride.
         * @param {Array.<number>} replayEnds Replay ends.
         * @return {number} Offset.
         */
        ol.render.canvas.Replay.prototype.drawCustomCoordinates_ = function(flatCoordinates, offset, ends, stride, replayEnds) {
            for (var i = 0, ii = ends.length; i < ii; ++i) {
                var end = ends[i];
                var replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false);
                replayEnds.push(replayEnd);
                offset = end;
            }
            return offset;
        };


        /**
         * @inheritDoc.
         */
        ol.render.canvas.Replay.prototype.drawCustom = function(geometry, feature, renderer) {
            this.beginGeometry(geometry, feature);
            var type = geometry.getType();
            var stride = geometry.getStride();
            var replayBegin = this.coordinates.length;
            var flatCoordinates, replayEnd, replayEnds, replayEndss;
            var offset;
            if (type == ol.geom.GeometryType.MULTI_POLYGON) {
                geometry = /** @type {ol.geom.MultiPolygon} */ (geometry);
                flatCoordinates = geometry.getOrientedFlatCoordinates();
                replayEndss = [];
                var endss = geometry.getEndss();
                offset = 0;
                for (var i = 0, ii = endss.length; i < ii; ++i) {
                    var myEnds = [];
                    offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds);
                    replayEndss.push(myEnds);
                }
                this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
                    replayBegin, replayEndss, geometry, renderer, ol.geom.flat.inflate.coordinatesss]);
            } else if (type == ol.geom.GeometryType.POLYGON || type == ol.geom.GeometryType.MULTI_LINE_STRING) {
                replayEnds = [];
                flatCoordinates = (type == ol.geom.GeometryType.POLYGON) ?
                /** @type {ol.geom.Polygon} */ (geometry).getOrientedFlatCoordinates() :
                    geometry.getFlatCoordinates();
                offset = this.drawCustomCoordinates_(flatCoordinates, 0,
                    /** @type {ol.geom.Polygon|ol.geom.MultiLineString} */ (geometry).getEnds(),
                    stride, replayEnds);
                this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
                    replayBegin, replayEnds, geometry, renderer, ol.geom.flat.inflate.coordinatess]);
            } else if (type == ol.geom.GeometryType.LINE_STRING || type == ol.geom.GeometryType.MULTI_POINT) {
                flatCoordinates = geometry.getFlatCoordinates();
                replayEnd = this.appendFlatCoordinates(
                    flatCoordinates, 0, flatCoordinates.length, stride, false, false);
                this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
                    replayBegin, replayEnd, geometry, renderer, ol.geom.flat.inflate.coordinates]);
            } else if (type == ol.geom.GeometryType.POINT) {
                flatCoordinates = geometry.getFlatCoordinates();
                this.coordinates.push(flatCoordinates[0], flatCoordinates[1]);
                replayEnd = this.coordinates.length;
                this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
                    replayBegin, replayEnd, geometry, renderer]);
            }
            this.endGeometry(geometry, feature);
        };


        /**
         * @protected
         * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         */
        ol.render.canvas.Replay.prototype.beginGeometry = function(geometry, feature) {
            this.beginGeometryInstruction1_ =
                [ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
            this.instructions.push(this.beginGeometryInstruction1_);
            this.beginGeometryInstruction2_ =
                [ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
            this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
        };


        /**
         * @private
         * @param {CanvasRenderingContext2D} context Context.
         */
        ol.render.canvas.Replay.prototype.fill_ = function(context) {
            if (this.fillOrigin_) {
                var origin = ol.transform.apply(this.renderedTransform_, this.fillOrigin_.slice());
                context.translate(origin[0], origin[1]);
                context.rotate(this.viewRotation_);
            }
            context.fill();
            if (this.fillOrigin_) {
                context.setTransform.apply(context, ol.render.canvas.resetTransform_);
            }
        };


        /**
         * @private
         * @param {CanvasRenderingContext2D} context Context.
         * @param {Array.<*>} instruction Instruction.
         */
        ol.render.canvas.Replay.prototype.setStrokeStyle_ = function(context, instruction) {
            context.strokeStyle = /** @type {ol.ColorLike} */ (instruction[1]);
            context.lineWidth = /** @type {number} */ (instruction[2]);
            context.lineCap = /** @type {string} */ (instruction[3]);
            context.lineJoin = /** @type {string} */ (instruction[4]);
            context.miterLimit = /** @type {number} */ (instruction[5]);
            if (ol.has.CANVAS_LINE_DASH) {
                context.lineDashOffset = /** @type {number} */ (instruction[7]);
                context.setLineDash(/** @type {Array.<number>} */ (instruction[6]));
            }
        };


        /**
         * @param {ol.DeclutterGroup} declutterGroup Declutter group.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         */
        ol.render.canvas.Replay.prototype.renderDeclutter_ = function(declutterGroup, feature) {
            if (declutterGroup && declutterGroup.length > 5) {
                var groupCount = declutterGroup[4];
                if (groupCount == 1 || groupCount == declutterGroup.length - 5) {
                    /** @type {ol.RBushEntry} */
                    var box = {
                        minX: /** @type {number} */ (declutterGroup[0]),
                        minY: /** @type {number} */ (declutterGroup[1]),
                        maxX: /** @type {number} */ (declutterGroup[2]),
                        maxY: /** @type {number} */ (declutterGroup[3]),
                        value: feature
                    };
                    if (!this.declutterTree.collides(box)) {
                        this.declutterTree.insert(box);
                        var drawImage = ol.render.canvas.drawImage;
                        for (var j = 5, jj = declutterGroup.length; j < jj; ++j) {
                            var declutterData = /** @type {Array} */ (declutterGroup[j]);
                            if (declutterData) {
                                if (declutterData.length > 11) {
                                    this.replayTextBackground_(declutterData[0],
                                        declutterData[13], declutterData[14], declutterData[15], declutterData[16],
                                        declutterData[11], declutterData[12]);
                                }
                                drawImage.apply(undefined, declutterData);
                            }
                        }
                    }
                    declutterGroup.length = 5;
                    ol.extent.createOrUpdateEmpty(declutterGroup);
                }
            }
        };


        /**
         * @private
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Transform} transform Transform.
         * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
         *     to skip.
         * @param {Array.<*>} instructions Instructions array.
         * @param {function((ol.Feature|ol.render.Feature)): T|undefined}
         *     featureCallback Feature callback.
         * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
         *     extent.
         * @return {T|undefined} Callback result.
         * @template T
         */
        ol.render.canvas.Replay.prototype.replay_ = function(
            context, transform, skippedFeaturesHash,
            instructions, featureCallback, opt_hitExtent) {
            /** @type {Array.<number>} */
            var pixelCoordinates;
            if (this.pixelCoordinates_ && ol.array.equals(transform, this.renderedTransform_)) {
                pixelCoordinates = this.pixelCoordinates_;
            } else {
                if (!this.pixelCoordinates_) {
                    this.pixelCoordinates_ = [];
                }
                pixelCoordinates = ol.geom.flat.transform.transform2D(
                    this.coordinates, 0, this.coordinates.length, 2,
                    transform, this.pixelCoordinates_);
                ol.transform.setFromArray(this.renderedTransform_, transform);
            }
            var skipFeatures = !ol.obj.isEmpty(skippedFeaturesHash);
            var i = 0; // instruction index
            var ii = instructions.length; // end of instructions
            var d = 0; // data index
            var dd; // end of per-instruction data
            var anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image;
            var pendingFill = 0;
            var pendingStroke = 0;
            var lastFillInstruction = null;
            var lastStrokeInstruction = null;
            var coordinateCache = this.coordinateCache_;
            var viewRotation = this.viewRotation_;

            var state = /** @type {olx.render.State} */ ({
                context: context,
                pixelRatio: this.pixelRatio,
                resolution: this.resolution,
                rotation: viewRotation
            });

            // When the batch size gets too big, performance decreases. 200 is a good
            // balance between batch size and number of fill/stroke instructions.
            var batchSize =
                this.instructions != instructions || this.overlaps ? 0 : 200;
            while (i < ii) {
                var instruction = instructions[i];
                var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
                var /** @type {ol.Feature|ol.render.Feature} */ feature, x, y;
                switch (type) {
                    case ol.render.canvas.Instruction.BEGIN_GEOMETRY:
                        feature = /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
                        if ((skipFeatures &&
                            skippedFeaturesHash[ol.getUid(feature).toString()]) ||
                            !feature.getGeometry()) {
                            i = /** @type {number} */ (instruction[2]);
                        } else if (opt_hitExtent !== undefined && !ol.extent.intersects(
                            opt_hitExtent, feature.getGeometry().getExtent())) {
                            i = /** @type {number} */ (instruction[2]) + 1;
                        } else {
                            ++i;
                        }
                        break;
                    case ol.render.canvas.Instruction.BEGIN_PATH:
                        if (pendingFill > batchSize) {
                            this.fill_(context);
                            pendingFill = 0;
                        }
                        if (pendingStroke > batchSize) {
                            context.stroke();
                            pendingStroke = 0;
                        }
                        if (!pendingFill && !pendingStroke) {
                            context.beginPath();
                            prevX = prevY = NaN;
                        }
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.CIRCLE:
                        d = /** @type {number} */ (instruction[1]);
                        var x1 = pixelCoordinates[d];
                        var y1 = pixelCoordinates[d + 1];
                        var x2 = pixelCoordinates[d + 2];
                        var y2 = pixelCoordinates[d + 3];
                        var dx = x2 - x1;
                        var dy = y2 - y1;
                        var r = Math.sqrt(dx * dx + dy * dy);
                        context.moveTo(x1 + r, y1);
                        context.arc(x1, y1, r, 0, 2 * Math.PI, true);
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.CLOSE_PATH:
                        context.closePath();
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.CUSTOM:
                        d = /** @type {number} */ (instruction[1]);
                        dd = instruction[2];
                        var geometry = /** @type {ol.geom.SimpleGeometry} */ (instruction[3]);
                        var renderer = instruction[4];
                        var fn = instruction.length == 6 ? instruction[5] : undefined;
                        state.geometry = geometry;
                        state.feature = feature;
                        if (!(i in coordinateCache)) {
                            coordinateCache[i] = [];
                        }
                        var coords = coordinateCache[i];
                        if (fn) {
                            fn(pixelCoordinates, d, dd, 2, coords);
                        } else {
                            coords[0] = pixelCoordinates[d];
                            coords[1] = pixelCoordinates[d + 1];
                            coords.length = 2;
                        }
                        renderer(coords, state);
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.DRAW_IMAGE:
                        d = /** @type {number} */ (instruction[1]);
                        dd = /** @type {number} */ (instruction[2]);
                        image =  /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */
                            (instruction[3]);
                        // Remaining arguments in DRAW_IMAGE are in alphabetical order
                        anchorX = /** @type {number} */ (instruction[4]);
                        anchorY = /** @type {number} */ (instruction[5]);
                        declutterGroup = featureCallback ? null : /** @type {ol.DeclutterGroup} */ (instruction[6]);
                        var height = /** @type {number} */ (instruction[7]);
                        var opacity = /** @type {number} */ (instruction[8]);
                        var originX = /** @type {number} */ (instruction[9]);
                        var originY = /** @type {number} */ (instruction[10]);
                        var rotateWithView = /** @type {boolean} */ (instruction[11]);
                        var rotation = /** @type {number} */ (instruction[12]);
                        var scale = /** @type {number} */ (instruction[13]);
                        var snapToPixel = /** @type {boolean} */ (instruction[14]);
                        var width = /** @type {number} */ (instruction[15]);

                        var padding, backgroundFill, backgroundStroke;
                        if (instruction.length > 16) {
                            padding = /** @type {Array.<number>} */ (instruction[16]);
                            backgroundFill = /** @type {boolean} */ (instruction[17]);
                            backgroundStroke = /** @type {boolean} */ (instruction[18]);
                        } else {
                            padding = ol.render.canvas.defaultPadding;
                            backgroundFill = backgroundStroke = false;
                        }

                        if (rotateWithView) {
                            rotation += viewRotation;
                        }
                        for (; d < dd; d += 2) {
                            this.replayImage_(context,
                                pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY,
                                declutterGroup, height, opacity, originX, originY, rotation, scale,
                                snapToPixel, width, padding,
                                backgroundFill ? /** @type {Array.<*>} */ (lastFillInstruction) : null,
                                backgroundStroke ? /** @type {Array.<*>} */ (lastStrokeInstruction) : null);
                        }
                        this.renderDeclutter_(declutterGroup, feature);
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.DRAW_CHARS:
                        var begin = /** @type {number} */ (instruction[1]);
                        var end = /** @type {number} */ (instruction[2]);
                        var baseline = /** @type {number} */ (instruction[3]);
                        declutterGroup = featureCallback ? null : /** @type {ol.DeclutterGroup} */ (instruction[4]);
                        var overflow = /** @type {number} */ (instruction[5]);
                        var fillKey = /** @type {string} */ (instruction[6]);
                        var maxAngle = /** @type {number} */ (instruction[7]);
                        var measure = /** @type {function(string):number} */ (instruction[8]);
                        var offsetY = /** @type {number} */ (instruction[9]);
                        var strokeKey = /** @type {string} */ (instruction[10]);
                        var strokeWidth =  /** @type {number} */ (instruction[11]);
                        var text = /** @type {string} */ (instruction[12]);
                        var textKey = /** @type {string} */ (instruction[13]);
                        var textScale = /** @type {number} */ (instruction[14]);

                        var pathLength = ol.geom.flat.length.lineString(pixelCoordinates, begin, end, 2);
                        var textLength = measure(text);
                        if (overflow || textLength <= pathLength) {
                            var textAlign = /** @type {ol.render.canvas.TextReplay} */ (this).textStates[textKey].textAlign;
                            var startM = (pathLength - textLength) * ol.render.replay.TEXT_ALIGN[textAlign];
                            var parts = ol.geom.flat.textpath.lineString(
                                pixelCoordinates, begin, end, 2, text, measure, startM, maxAngle);
                            if (parts) {
                                var c, cc, chars, label, part;
                                if (strokeKey) {
                                    for (c = 0, cc = parts.length; c < cc; ++c) {
                                        part = parts[c]; // x, y, anchorX, rotation, chunk
                                        chars = /** @type {string} */ (part[4]);
                                        label = /** @type {ol.render.canvas.TextReplay} */ (this).getImage(chars, textKey, '', strokeKey);
                                        anchorX = /** @type {number} */ (part[2]) + strokeWidth;
                                        anchorY = baseline * label.height + (0.5 - baseline) * 2 * strokeWidth - offsetY;
                                        this.replayImage_(context,
                                            /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label,
                                            anchorX, anchorY, declutterGroup, label.height, 1, 0, 0,
                                            /** @type {number} */ (part[3]), textScale, false, label.width,
                                            ol.render.canvas.defaultPadding, null, null);
                                    }
                                }
                                if (fillKey) {
                                    for (c = 0, cc = parts.length; c < cc; ++c) {
                                        part = parts[c]; // x, y, anchorX, rotation, chunk
                                        chars = /** @type {string} */ (part[4]);
                                        label = /** @type {ol.render.canvas.TextReplay} */ (this).getImage(chars, textKey, fillKey, '');
                                        anchorX = /** @type {number} */ (part[2]);
                                        anchorY = baseline * label.height - offsetY;
                                        this.replayImage_(context,
                                            /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label,
                                            anchorX, anchorY, declutterGroup, label.height, 1, 0, 0,
                                            /** @type {number} */ (part[3]), textScale, false, label.width,
                                            ol.render.canvas.defaultPadding, null, null);
                                    }
                                }
                            }
                        }
                        this.renderDeclutter_(declutterGroup, feature);
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.END_GEOMETRY:
                        if (featureCallback !== undefined) {
                            feature = /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
                            var result = featureCallback(feature);
                            if (result) {
                                return result;
                            }
                        }
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.FILL:
                        if (batchSize) {
                            pendingFill++;
                        } else {
                            this.fill_(context);
                        }
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.MOVE_TO_LINE_TO:
                        d = /** @type {number} */ (instruction[1]);
                        dd = /** @type {number} */ (instruction[2]);
                        x = pixelCoordinates[d];
                        y = pixelCoordinates[d + 1];
                        roundX = (x + 0.5) | 0;
                        roundY = (y + 0.5) | 0;
                        if (roundX !== prevX || roundY !== prevY) {
                            context.moveTo(x, y);
                            prevX = roundX;
                            prevY = roundY;
                        }
                        for (d += 2; d < dd; d += 2) {
                            x = pixelCoordinates[d];
                            y = pixelCoordinates[d + 1];
                            roundX = (x + 0.5) | 0;
                            roundY = (y + 0.5) | 0;
                            if (d == dd - 2 || roundX !== prevX || roundY !== prevY) {
                                context.lineTo(x, y);
                                prevX = roundX;
                                prevY = roundY;
                            }
                        }
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.SET_FILL_STYLE:
                        lastFillInstruction = instruction;
                        this.fillOrigin_ = instruction[2];

                        if (pendingFill) {
                            this.fill_(context);
                            pendingFill = 0;
                            if (pendingStroke) {
                                context.stroke();
                                pendingStroke = 0;
                            }
                        }

                        context.fillStyle = /** @type {ol.ColorLike} */ (instruction[1]);
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.SET_STROKE_STYLE:
                        lastStrokeInstruction = instruction;
                        if (pendingStroke) {
                            context.stroke();
                            pendingStroke = 0;
                        }
                        this.setStrokeStyle_(context, /** @type {Array.<*>} */ (instruction));
                        ++i;
                        break;
                    case ol.render.canvas.Instruction.STROKE:
                        if (batchSize) {
                            pendingStroke++;
                        } else {
                            context.stroke();
                        }
                        ++i;
                        break;
                    default:
                        ++i; // consume the instruction anyway, to avoid an infinite loop
                        break;
                }
            }
            if (pendingFill) {
                this.fill_(context);
            }
            if (pendingStroke) {
                context.stroke();
            }
            return undefined;
        };


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Transform} transform Transform.
         * @param {number} viewRotation View rotation.
         * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
         *     to skip.
         */
        ol.render.canvas.Replay.prototype.replay = function(
            context, transform, viewRotation, skippedFeaturesHash) {
            this.viewRotation_ = viewRotation;
            this.replay_(context, transform,
                skippedFeaturesHash, this.instructions, undefined, undefined);
        };


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Transform} transform Transform.
         * @param {number} viewRotation View rotation.
         * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
         *     to skip.
         * @param {function((ol.Feature|ol.render.Feature)): T=} opt_featureCallback
         *     Feature callback.
         * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
         *     extent.
         * @return {T|undefined} Callback result.
         * @template T
         */
        ol.render.canvas.Replay.prototype.replayHitDetection = function(
            context, transform, viewRotation, skippedFeaturesHash,
            opt_featureCallback, opt_hitExtent) {
            this.viewRotation_ = viewRotation;
            return this.replay_(context, transform, skippedFeaturesHash,
                this.hitDetectionInstructions, opt_featureCallback, opt_hitExtent);
        };


        /**
         * Reverse the hit detection instructions.
         */
        ol.render.canvas.Replay.prototype.reverseHitDetectionInstructions = function() {
            var hitDetectionInstructions = this.hitDetectionInstructions;
            // step 1 - reverse array
            hitDetectionInstructions.reverse();
            // step 2 - reverse instructions within geometry blocks
            var i;
            var n = hitDetectionInstructions.length;
            var instruction;
            var type;
            var begin = -1;
            for (i = 0; i < n; ++i) {
                instruction = hitDetectionInstructions[i];
                type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
                if (type == ol.render.canvas.Instruction.END_GEOMETRY) {
                    begin = i;
                } else if (type == ol.render.canvas.Instruction.BEGIN_GEOMETRY) {
                    instruction[2] = i;
                    ol.array.reverseSubArray(this.hitDetectionInstructions, begin, i);
                    begin = -1;
                }
            }
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.Replay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
            var state = this.state;
            if (fillStyle) {
                var fillStyleColor = fillStyle.getColor();
                state.fillStyle = ol.colorlike.asColorLike(fillStyleColor ?
                    fillStyleColor : ol.render.canvas.defaultFillStyle);
            } else {
                state.fillStyle = undefined;
            }
            if (strokeStyle) {
                var strokeStyleColor = strokeStyle.getColor();
                state.strokeStyle = ol.colorlike.asColorLike(strokeStyleColor ?
                    strokeStyleColor : ol.render.canvas.defaultStrokeStyle);
                var strokeStyleLineCap = strokeStyle.getLineCap();
                state.lineCap = strokeStyleLineCap !== undefined ?
                    strokeStyleLineCap : ol.render.canvas.defaultLineCap;
                var strokeStyleLineDash = strokeStyle.getLineDash();
                state.lineDash = strokeStyleLineDash ?
                    strokeStyleLineDash.slice() : ol.render.canvas.defaultLineDash;
                var strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
                state.lineDashOffset = strokeStyleLineDashOffset ?
                    strokeStyleLineDashOffset : ol.render.canvas.defaultLineDashOffset;
                var strokeStyleLineJoin = strokeStyle.getLineJoin();
                state.lineJoin = strokeStyleLineJoin !== undefined ?
                    strokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
                var strokeStyleWidth = strokeStyle.getWidth();
                state.lineWidth = strokeStyleWidth !== undefined ?
                    strokeStyleWidth : ol.render.canvas.defaultLineWidth;
                var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
                state.miterLimit = strokeStyleMiterLimit !== undefined ?
                    strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;

                if (state.lineWidth > this.maxLineWidth) {
                    this.maxLineWidth = state.lineWidth;
                    // invalidate the buffered max extent cache
                    this.bufferedMaxExtent_ = null;
                }
            } else {
                state.strokeStyle = undefined;
                state.lineCap = undefined;
                state.lineDash = null;
                state.lineDashOffset = undefined;
                state.lineJoin = undefined;
                state.lineWidth = undefined;
                state.miterLimit = undefined;
            }
        };


        /**
         * @param {ol.CanvasFillStrokeState} state State.
         * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
         */
        ol.render.canvas.Replay.prototype.applyFill = function(state, geometry) {
            var fillStyle = state.fillStyle;
            var fillInstruction = [ol.render.canvas.Instruction.SET_FILL_STYLE, fillStyle];
            if (typeof fillStyle !== 'string') {
                var fillExtent = geometry.getExtent();
                fillInstruction.push([fillExtent[0], fillExtent[3]]);
            }
            this.instructions.push(fillInstruction);
        };


        /**
         * @param {ol.CanvasFillStrokeState} state State.
         */
        ol.render.canvas.Replay.prototype.applyStroke = function(state) {
            this.instructions.push([
                ol.render.canvas.Instruction.SET_STROKE_STYLE,
                state.strokeStyle, state.lineWidth * this.pixelRatio, state.lineCap,
                state.lineJoin, state.miterLimit,
                this.applyPixelRatio(state.lineDash), state.lineDashOffset * this.pixelRatio
            ]);
        };


        /**
         * @param {ol.CanvasFillStrokeState} state State.
         * @param {function(this:ol.render.canvas.Replay, ol.CanvasFillStrokeState, (ol.geom.Geometry|ol.render.Feature))} applyFill Apply fill.
         * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
         */
        ol.render.canvas.Replay.prototype.updateFillStyle = function(state, applyFill, geometry) {
            var fillStyle = state.fillStyle;
            if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) {
                applyFill.call(this, state, geometry);
                state.currentFillStyle = fillStyle;
            }
        };


        /**
         * @param {ol.CanvasFillStrokeState} state State.
         * @param {function(this:ol.render.canvas.Replay, ol.CanvasFillStrokeState)} applyStroke Apply stroke.
         */
        ol.render.canvas.Replay.prototype.updateStrokeStyle = function(state, applyStroke) {
            var strokeStyle = state.strokeStyle;
            var lineCap = state.lineCap;
            var lineDash = state.lineDash;
            var lineDashOffset = state.lineDashOffset;
            var lineJoin = state.lineJoin;
            var lineWidth = state.lineWidth;
            var miterLimit = state.miterLimit;
            if (state.currentStrokeStyle != strokeStyle ||
                state.currentLineCap != lineCap ||
                (lineDash != state.currentLineDash && !ol.array.equals(state.currentLineDash, lineDash)) ||
                state.currentLineDashOffset != lineDashOffset ||
                state.currentLineJoin != lineJoin ||
                state.currentLineWidth != lineWidth ||
                state.currentMiterLimit != miterLimit) {
                applyStroke.call(this, state);
                state.currentStrokeStyle = strokeStyle;
                state.currentLineCap = lineCap;
                state.currentLineDash = lineDash;
                state.currentLineDashOffset = lineDashOffset;
                state.currentLineJoin = lineJoin;
                state.currentLineWidth = lineWidth;
                state.currentMiterLimit = miterLimit;
            }
        };


        /**
         * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         */
        ol.render.canvas.Replay.prototype.endGeometry = function(geometry, feature) {
            this.beginGeometryInstruction1_[2] = this.instructions.length;
            this.beginGeometryInstruction1_ = null;
            this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
            this.beginGeometryInstruction2_ = null;
            var endGeometryInstruction =
                [ol.render.canvas.Instruction.END_GEOMETRY, feature];
            this.instructions.push(endGeometryInstruction);
            this.hitDetectionInstructions.push(endGeometryInstruction);
        };


        /**
         * FIXME empty description for jsdoc
         */
        ol.render.canvas.Replay.prototype.finish = ol.nullFunction;


        /**
         * Get the buffered rendering extent.  Rendering will be clipped to the extent
         * provided to the constructor.  To account for symbolizers that may intersect
         * this extent, we calculate a buffered extent (e.g. based on stroke width).
         * @return {ol.Extent} The buffered rendering extent.
         * @protected
         */
        ol.render.canvas.Replay.prototype.getBufferedMaxExtent = function() {
            if (!this.bufferedMaxExtent_) {
                this.bufferedMaxExtent_ = ol.extent.clone(this.maxExtent);
                if (this.maxLineWidth > 0) {
                    var width = this.resolution * (this.maxLineWidth + 1) / 2;
                    ol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
                }
            }
            return this.bufferedMaxExtent_;
        };

        /**
         * @constructor
         * @extends {ol.render.canvas.Replay}
         * @param {number} tolerance Tolerance.
         * @param {ol.Extent} maxExtent Maximum extent.
         * @param {number} resolution Resolution.
         * @param {number} pixelRatio Pixel ratio.
         * @param {boolean} overlaps The replay can have overlapping geometries.
         * @param {?} declutterTree Declutter tree.
         * @struct
         */
        ol.render.canvas.ImageReplay = function(
            tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
            ol.render.canvas.Replay.call(this,
                tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree);

            /**
             * @private
             * @type {ol.DeclutterGroup}
             */
            this.declutterGroup_ = null;

            /**
             * @private
             * @type {HTMLCanvasElement|HTMLVideoElement|Image}
             */
            this.hitDetectionImage_ = null;

            /**
             * @private
             * @type {HTMLCanvasElement|HTMLVideoElement|Image}
             */
            this.image_ = null;

            /**
             * @private
             * @type {number|undefined}
             */
            this.anchorX_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.anchorY_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.height_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.opacity_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.originX_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.originY_ = undefined;

            /**
             * @private
             * @type {boolean|undefined}
             */
            this.rotateWithView_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.rotation_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.scale_ = undefined;

            /**
             * @private
             * @type {boolean|undefined}
             */
            this.snapToPixel_ = undefined;

            /**
             * @private
             * @type {number|undefined}
             */
            this.width_ = undefined;

        };
        ol.inherits(ol.render.canvas.ImageReplay, ol.render.canvas.Replay);


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @private
         * @return {number} My end.
         */
        ol.render.canvas.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {
            return this.appendFlatCoordinates(
                flatCoordinates, offset, end, stride, false, false);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, feature) {
            if (!this.image_) {
                return;
            }
            this.beginGeometry(pointGeometry, feature);
            var flatCoordinates = pointGeometry.getFlatCoordinates();
            var stride = pointGeometry.getStride();
            var myBegin = this.coordinates.length;
            var myEnd = this.drawCoordinates_(
                flatCoordinates, 0, flatCoordinates.length, stride);
            this.instructions.push([
                ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
                // Remaining arguments to DRAW_IMAGE are in alphabetical order
                this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
                this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
                this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_
            ]);
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
                this.hitDetectionImage_,
                // Remaining arguments to DRAW_IMAGE are in alphabetical order
                this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
                this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
                this.scale_, this.snapToPixel_, this.width_
            ]);
            this.endGeometry(pointGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeometry, feature) {
            if (!this.image_) {
                return;
            }
            this.beginGeometry(multiPointGeometry, feature);
            var flatCoordinates = multiPointGeometry.getFlatCoordinates();
            var stride = multiPointGeometry.getStride();
            var myBegin = this.coordinates.length;
            var myEnd = this.drawCoordinates_(
                flatCoordinates, 0, flatCoordinates.length, stride);
            this.instructions.push([
                ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
                // Remaining arguments to DRAW_IMAGE are in alphabetical order
                this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
                this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
                this.scale_ * this.pixelRatio, this.snapToPixel_, this.width_
            ]);
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
                this.hitDetectionImage_,
                // Remaining arguments to DRAW_IMAGE are in alphabetical order
                this.anchorX_, this.anchorY_, this.declutterGroup_, this.height_, this.opacity_,
                this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
                this.scale_, this.snapToPixel_, this.width_
            ]);
            this.endGeometry(multiPointGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.ImageReplay.prototype.finish = function() {
            this.reverseHitDetectionInstructions();
            // FIXME this doesn't really protect us against further calls to draw*Geometry
            this.anchorX_ = undefined;
            this.anchorY_ = undefined;
            this.hitDetectionImage_ = null;
            this.image_ = null;
            this.height_ = undefined;
            this.scale_ = undefined;
            this.opacity_ = undefined;
            this.originX_ = undefined;
            this.originY_ = undefined;
            this.rotateWithView_ = undefined;
            this.rotation_ = undefined;
            this.snapToPixel_ = undefined;
            this.width_ = undefined;
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle, declutterGroup) {
            var anchor = imageStyle.getAnchor();
            var size = imageStyle.getSize();
            var hitDetectionImage = imageStyle.getHitDetectionImage(1);
            var image = imageStyle.getImage(1);
            var origin = imageStyle.getOrigin();
            this.anchorX_ = anchor[0];
            this.anchorY_ = anchor[1];
            this.declutterGroup_ = /** @type {ol.DeclutterGroup} */ (declutterGroup);
            this.hitDetectionImage_ = hitDetectionImage;
            this.image_ = image;
            this.height_ = size[1];
            this.opacity_ = imageStyle.getOpacity();
            this.originX_ = origin[0];
            this.originY_ = origin[1];
            this.rotateWithView_ = imageStyle.getRotateWithView();
            this.rotation_ = imageStyle.getRotation();
            this.scale_ = imageStyle.getScale();
            this.snapToPixel_ = imageStyle.getSnapToPixel();
            this.width_ = size[0];
        };

        /**
         * @constructor
         * @extends {ol.render.canvas.Replay}
         * @param {number} tolerance Tolerance.
         * @param {ol.Extent} maxExtent Maximum extent.
         * @param {number} resolution Resolution.
         * @param {number} pixelRatio Pixel ratio.
         * @param {boolean} overlaps The replay can have overlapping geometries.
         * @param {?} declutterTree Declutter tree.
         * @struct
         */
        ol.render.canvas.LineStringReplay = function(
            tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
            ol.render.canvas.Replay.call(this,
                tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree);
        };
        ol.inherits(ol.render.canvas.LineStringReplay, ol.render.canvas.Replay);


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {number} end End.
         * @param {number} stride Stride.
         * @private
         * @return {number} end.
         */
        ol.render.canvas.LineStringReplay.prototype.drawFlatCoordinates_ = function(flatCoordinates, offset, end, stride) {
            var myBegin = this.coordinates.length;
            var myEnd = this.appendFlatCoordinates(
                flatCoordinates, offset, end, stride, false, false);
            var moveToLineToInstruction =
                [ol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
            this.instructions.push(moveToLineToInstruction);
            this.hitDetectionInstructions.push(moveToLineToInstruction);
            return end;
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.LineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) {
            var state = this.state;
            var strokeStyle = state.strokeStyle;
            var lineWidth = state.lineWidth;
            if (strokeStyle === undefined || lineWidth === undefined) {
                return;
            }
            this.updateStrokeStyle(state, this.applyStroke);
            this.beginGeometry(lineStringGeometry, feature);
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.SET_STROKE_STYLE,
                state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
                state.miterLimit, state.lineDash, state.lineDashOffset
            ], [
                ol.render.canvas.Instruction.BEGIN_PATH
            ]);
            var flatCoordinates = lineStringGeometry.getFlatCoordinates();
            var stride = lineStringGeometry.getStride();
            this.drawFlatCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride);
            this.hitDetectionInstructions.push([ol.render.canvas.Instruction.STROKE]);
            this.endGeometry(lineStringGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.LineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {
            var state = this.state;
            var strokeStyle = state.strokeStyle;
            var lineWidth = state.lineWidth;
            if (strokeStyle === undefined || lineWidth === undefined) {
                return;
            }
            this.updateStrokeStyle(state, this.applyStroke);
            this.beginGeometry(multiLineStringGeometry, feature);
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.SET_STROKE_STYLE,
                state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
                state.miterLimit, state.lineDash, state.lineDashOffset
            ], [
                ol.render.canvas.Instruction.BEGIN_PATH
            ]);
            var ends = multiLineStringGeometry.getEnds();
            var flatCoordinates = multiLineStringGeometry.getFlatCoordinates();
            var stride = multiLineStringGeometry.getStride();
            var offset = 0;
            var i, ii;
            for (i = 0, ii = ends.length; i < ii; ++i) {
                offset = this.drawFlatCoordinates_(
                    flatCoordinates, offset, ends[i], stride);
            }
            this.hitDetectionInstructions.push([ol.render.canvas.Instruction.STROKE]);
            this.endGeometry(multiLineStringGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.LineStringReplay.prototype.finish = function() {
            var state = this.state;
            if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
                this.instructions.push([ol.render.canvas.Instruction.STROKE]);
            }
            this.reverseHitDetectionInstructions();
            this.state = null;
        };


        /**
         * @inheritDoc.
         */
        ol.render.canvas.LineStringReplay.prototype.applyStroke = function(state) {
            if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
                this.instructions.push([ol.render.canvas.Instruction.STROKE]);
                state.lastStroke = this.coordinates.length;
            }
            state.lastStroke = 0;
            ol.render.canvas.Replay.prototype.applyStroke.call(this, state);
            this.instructions.push([ol.render.canvas.Instruction.BEGIN_PATH]);
        };

        /**
         * @constructor
         * @extends {ol.render.canvas.Replay}
         * @param {number} tolerance Tolerance.
         * @param {ol.Extent} maxExtent Maximum extent.
         * @param {number} resolution Resolution.
         * @param {number} pixelRatio Pixel ratio.
         * @param {boolean} overlaps The replay can have overlapping geometries.
         * @param {?} declutterTree Declutter tree.
         * @struct
         */
        ol.render.canvas.PolygonReplay = function(
            tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
            ol.render.canvas.Replay.call(this,
                tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree);
        };
        ol.inherits(ol.render.canvas.PolygonReplay, ol.render.canvas.Replay);


        /**
         * @param {Array.<number>} flatCoordinates Flat coordinates.
         * @param {number} offset Offset.
         * @param {Array.<number>} ends Ends.
         * @param {number} stride Stride.
         * @private
         * @return {number} End.
         */
        ol.render.canvas.PolygonReplay.prototype.drawFlatCoordinatess_ = function(flatCoordinates, offset, ends, stride) {
            var state = this.state;
            var fill = state.fillStyle !== undefined;
            var stroke = state.strokeStyle != undefined;
            var numEnds = ends.length;
            var beginPathInstruction = [ol.render.canvas.Instruction.BEGIN_PATH];
            this.instructions.push(beginPathInstruction);
            this.hitDetectionInstructions.push(beginPathInstruction);
            for (var i = 0; i < numEnds; ++i) {
                var end = ends[i];
                var myBegin = this.coordinates.length;
                var myEnd = this.appendFlatCoordinates(
                    flatCoordinates, offset, end, stride, true, !stroke);
                var moveToLineToInstruction =
                    [ol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
                this.instructions.push(moveToLineToInstruction);
                this.hitDetectionInstructions.push(moveToLineToInstruction);
                if (stroke) {
                    // Performance optimization: only call closePath() when we have a stroke.
                    // Otherwise the ring is closed already (see appendFlatCoordinates above).
                    var closePathInstruction = [ol.render.canvas.Instruction.CLOSE_PATH];
                    this.instructions.push(closePathInstruction);
                    this.hitDetectionInstructions.push(closePathInstruction);
                }
                offset = end;
            }
            var fillInstruction = [ol.render.canvas.Instruction.FILL];
            this.hitDetectionInstructions.push(fillInstruction);
            if (fill) {
                this.instructions.push(fillInstruction);
            }
            if (stroke) {
                var strokeInstruction = [ol.render.canvas.Instruction.STROKE];
                this.instructions.push(strokeInstruction);
                this.hitDetectionInstructions.push(strokeInstruction);
            }
            return offset;
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.PolygonReplay.prototype.drawCircle = function(circleGeometry, feature) {
            var state = this.state;
            var fillStyle = state.fillStyle;
            var strokeStyle = state.strokeStyle;
            if (fillStyle === undefined && strokeStyle === undefined) {
                return;
            }
            this.setFillStrokeStyles_(circleGeometry);
            this.beginGeometry(circleGeometry, feature);
            // always fill the circle for hit detection
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.SET_FILL_STYLE,
                ol.color.asString(ol.render.canvas.defaultFillStyle)
            ]);
            if (state.strokeStyle !== undefined) {
                this.hitDetectionInstructions.push([
                    ol.render.canvas.Instruction.SET_STROKE_STYLE,
                    state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
                    state.miterLimit, state.lineDash, state.lineDashOffset
                ]);
            }
            var flatCoordinates = circleGeometry.getFlatCoordinates();
            var stride = circleGeometry.getStride();
            var myBegin = this.coordinates.length;
            this.appendFlatCoordinates(
                flatCoordinates, 0, flatCoordinates.length, stride, false, false);
            var beginPathInstruction = [ol.render.canvas.Instruction.BEGIN_PATH];
            var circleInstruction = [ol.render.canvas.Instruction.CIRCLE, myBegin];
            this.instructions.push(beginPathInstruction, circleInstruction);
            this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction);
            var fillInstruction = [ol.render.canvas.Instruction.FILL];
            this.hitDetectionInstructions.push(fillInstruction);
            if (state.fillStyle !== undefined) {
                this.instructions.push(fillInstruction);
            }
            if (state.strokeStyle !== undefined) {
                var strokeInstruction = [ol.render.canvas.Instruction.STROKE];
                this.instructions.push(strokeInstruction);
                this.hitDetectionInstructions.push(strokeInstruction);
            }
            this.endGeometry(circleGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.PolygonReplay.prototype.drawPolygon = function(polygonGeometry, feature) {
            var state = this.state;
            this.setFillStrokeStyles_(polygonGeometry);
            this.beginGeometry(polygonGeometry, feature);
            // always fill the polygon for hit detection
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.SET_FILL_STYLE,
                ol.color.asString(ol.render.canvas.defaultFillStyle)]
            );
            if (state.strokeStyle !== undefined) {
                this.hitDetectionInstructions.push([
                    ol.render.canvas.Instruction.SET_STROKE_STYLE,
                    state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
                    state.miterLimit, state.lineDash, state.lineDashOffset
                ]);
            }
            var ends = polygonGeometry.getEnds();
            var flatCoordinates = polygonGeometry.getOrientedFlatCoordinates();
            var stride = polygonGeometry.getStride();
            this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride);
            this.endGeometry(polygonGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.PolygonReplay.prototype.drawMultiPolygon = function(multiPolygonGeometry, feature) {
            var state = this.state;
            var fillStyle = state.fillStyle;
            var strokeStyle = state.strokeStyle;
            if (fillStyle === undefined && strokeStyle === undefined) {
                return;
            }
            this.setFillStrokeStyles_(multiPolygonGeometry);
            this.beginGeometry(multiPolygonGeometry, feature);
            // always fill the multi-polygon for hit detection
            this.hitDetectionInstructions.push([
                ol.render.canvas.Instruction.SET_FILL_STYLE,
                ol.color.asString(ol.render.canvas.defaultFillStyle)
            ]);
            if (state.strokeStyle !== undefined) {
                this.hitDetectionInstructions.push([
                    ol.render.canvas.Instruction.SET_STROKE_STYLE,
                    state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
                    state.miterLimit, state.lineDash, state.lineDashOffset
                ]);
            }
            var endss = multiPolygonGeometry.getEndss();
            var flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates();
            var stride = multiPolygonGeometry.getStride();
            var offset = 0;
            var i, ii;
            for (i = 0, ii = endss.length; i < ii; ++i) {
                offset = this.drawFlatCoordinatess_(
                    flatCoordinates, offset, endss[i], stride);
            }
            this.endGeometry(multiPolygonGeometry, feature);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.PolygonReplay.prototype.finish = function() {
            this.reverseHitDetectionInstructions();
            this.state = null;
            // We want to preserve topology when drawing polygons.  Polygons are
            // simplified using quantization and point elimination. However, we might
            // have received a mix of quantized and non-quantized geometries, so ensure
            // that all are quantized by quantizing all coordinates in the batch.
            var tolerance = this.tolerance;
            if (tolerance !== 0) {
                var coordinates = this.coordinates;
                var i, ii;
                for (i = 0, ii = coordinates.length; i < ii; ++i) {
                    coordinates[i] = ol.geom.flat.simplify.snap(coordinates[i], tolerance);
                }
            }
        };


        /**
         * @private
         * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
         */
        ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyles_ = function(geometry) {
            var state = this.state;
            var fillStyle = state.fillStyle;
            if (fillStyle !== undefined) {
                this.updateFillStyle(state, this.applyFill, geometry);
            }
            if (state.strokeStyle !== undefined) {
                this.updateStrokeStyle(state, this.applyStroke);
            }
        };

        ol.geom.flat.straightchunk = {};
        ol.geom.flat.straightchunk.lineString = function(maxAngle, flatCoordinates, offset, end, stride) {
            var chunkStart = offset;
            var chunkEnd = offset;
            var chunkM = 0;
            var m = 0;
            var start = offset;
            var acos, i, m12, m23, x1, y1, x12, y12, x23, y23;
            for (i = offset; i < end; i += stride) {
                var x2 = flatCoordinates[i];
                var y2 = flatCoordinates[i + 1];
                if (x1 !== undefined) {
                    x23 = x2 - x1;
                    y23 = y2 - y1;
                    m23 = Math.sqrt(x23 * x23 + y23 * y23);
                    if (x12 !== undefined) {
                        m += m12;
                        acos = Math.acos((x12 * x23 + y12 * y23) / (m12 * m23));
                        if (acos > maxAngle) {
                            if (m > chunkM) {
                                chunkM = m;
                                chunkStart = start;
                                chunkEnd = i;
                            }
                            m = 0;
                            start = i - stride;
                        }
                    }
                    m12 = m23;
                    x12 = x23;
                    y12 = y23;
                }
                x1 = x2;
                y1 = y2;
            }
            m += m23;
            return m > chunkM ? [start, i] : [chunkStart, chunkEnd];
        };

        ol.style.TextPlacement = {
            POINT: 'point',
            LINE: 'line'
        };

        ol.render.canvas.TextReplay = function(
            tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree) {
            ol.render.canvas.Replay.call(this,
                tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree);

            /**
             * @private
             * @type {ol.DeclutterGroup}
             */
            this.declutterGroup_;

            /**
             * @private
             * @type {Array.<HTMLCanvasElement>}
             */
            this.labels_ = null;

            /**
             * @private
             * @type {string}
             */
            this.text_ = '';

            /**
             * @private
             * @type {number}
             */
            this.textOffsetX_ = 0;

            /**
             * @private
             * @type {number}
             */
            this.textOffsetY_ = 0;

            /**
             * @private
             * @type {boolean|undefined}
             */
            this.textRotateWithView_ = undefined;

            /**
             * @private
             * @type {number}
             */
            this.textRotation_ = 0;

            /**
             * @private
             * @type {?ol.CanvasFillState}
             */
            this.textFillState_ = null;

            /**
             * @type {Object.<string, ol.CanvasFillState>}
             */
            this.fillStates = {};

            /**
             * @private
             * @type {?ol.CanvasStrokeState}
             */
            this.textStrokeState_ = null;

            /**
             * @type {Object.<string, ol.CanvasStrokeState>}
             */
            this.strokeStates = {};

            /**
             * @private
             * @type {ol.CanvasTextState}
             */
            this.textState_ = /** @type {ol.CanvasTextState} */ ({});

            /**
             * @type {Object.<string, ol.CanvasTextState>}
             */
            this.textStates = {};

            /**
             * @private
             * @type {string}
             */
            this.textKey_ = '';

            /**
             * @private
             * @type {string}
             */
            this.fillKey_ = '';

            /**
             * @private
             * @type {string}
             */
            this.strokeKey_ = '';

            /**
             * @private
             * @type {Object.<string, Object.<string, number>>}
             */
            this.widths_ = {};

            var labelCache = ol.render.canvas.labelCache;
            labelCache.prune();

        };
        ol.inherits(ol.render.canvas.TextReplay, ol.render.canvas.Replay);


        /**
         * @param {string} font Font to use for measuring.
         * @param {Array.<string>} lines Lines to measure.
         * @param {Array.<number>} widths Array will be populated with the widths of
         * each line.
         * @return {number} Width of the whole text.
         */
        ol.render.canvas.TextReplay.measureTextWidths = function(font, lines, widths) {
            var numLines = lines.length;
            var width = 0;
            var currentWidth, i;
            for (i = 0; i < numLines; ++i) {
                currentWidth = ol.render.canvas.measureTextWidth(font, lines[i]);
                width = Math.max(width, currentWidth);
                widths.push(currentWidth);
            }
            return width;
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) {
            var fillState = this.textFillState_;
            var strokeState = this.textStrokeState_;
            var textState = this.textState_;
            if (this.text_ === '' || !textState || (!fillState && !strokeState)) {
                return;
            }

            var begin = this.coordinates.length;

            var geometryType = geometry.getType();
            var flatCoordinates = null;
            var end = 2;
            var stride = 2;
            var i, ii;

            if (textState.placement === ol.style.TextPlacement.LINE) {
                if (!ol.extent.intersects(this.getBufferedMaxExtent(), geometry.getExtent())) {
                    return;
                }
                var ends;
                flatCoordinates = geometry.getFlatCoordinates();
                stride = geometry.getStride();
                if (geometryType == ol.geom.GeometryType.LINE_STRING) {
                    ends = [flatCoordinates.length];
                } else if (geometryType == ol.geom.GeometryType.MULTI_LINE_STRING) {
                    ends = geometry.getEnds();
                } else if (geometryType == ol.geom.GeometryType.POLYGON) {
                    ends = geometry.getEnds().slice(0, 1);
                } else if (geometryType == ol.geom.GeometryType.MULTI_POLYGON) {
                    var endss = geometry.getEndss();
                    ends = [];
                    for (i = 0, ii = endss.length; i < ii; ++i) {
                        ends.push(endss[i][0]);
                    }
                }
                this.beginGeometry(geometry, feature);
                var textAlign = textState.textAlign;
                var flatOffset = 0;
                var flatEnd;
                for (var o = 0, oo = ends.length; o < oo; ++o) {
                    if (textAlign == undefined) {
                        var range = ol.geom.flat.straightchunk.lineString(
                            textState.maxAngle, flatCoordinates, flatOffset, ends[o], stride);
                        flatOffset = range[0];
                        flatEnd = range[1];
                    } else {
                        flatEnd = ends[o];
                    }
                    for (i = flatOffset; i < flatEnd; i += stride) {
                        this.coordinates.push(flatCoordinates[i], flatCoordinates[i + 1]);
                    }
                    end = this.coordinates.length;
                    flatOffset = ends[o];
                    this.drawChars_(begin, end, this.declutterGroup_);
                    begin = end;
                }
                this.endGeometry(geometry, feature);

            } else {
                var label = this.getImage(this.text_, this.textKey_, this.fillKey_, this.strokeKey_);
                var width = label.width / this.pixelRatio;
                switch (geometryType) {
                    case ol.geom.GeometryType.POINT:
                    case ol.geom.GeometryType.MULTI_POINT:
                        flatCoordinates = geometry.getFlatCoordinates();
                        end = flatCoordinates.length;
                        break;
                    case ol.geom.GeometryType.LINE_STRING:
                        flatCoordinates = /** @type {ol.geom.LineString} */ (geometry).getFlatMidpoint();
                        break;
                    case ol.geom.GeometryType.CIRCLE:
                        flatCoordinates = /** @type {ol.geom.Circle} */ (geometry).getCenter();
                        break;
                    case ol.geom.GeometryType.MULTI_LINE_STRING:
                        flatCoordinates = /** @type {ol.geom.MultiLineString} */ (geometry).getFlatMidpoints();
                        end = flatCoordinates.length;
                        break;
                    case ol.geom.GeometryType.POLYGON:
                        flatCoordinates = /** @type {ol.geom.Polygon} */ (geometry).getFlatInteriorPoint();
                        if (!textState.overflow && flatCoordinates[2] / this.resolution < width) {
                            return;
                        }
                        stride = 3;
                        break;
                    case ol.geom.GeometryType.MULTI_POLYGON:
                        var interiorPoints = /** @type {ol.geom.MultiPolygon} */ (geometry).getFlatInteriorPoints();
                        flatCoordinates = [];
                        for (i = 0, ii = interiorPoints.length; i < ii; i += 3) {
                            if (textState.overflow || interiorPoints[i + 2] / this.resolution >= width) {
                                flatCoordinates.push(interiorPoints[i], interiorPoints[i + 1]);
                            }
                        }
                        end = flatCoordinates.length;
                        if (end == 0) {
                            return;
                        }
                        break;
                }
                end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false);
                this.beginGeometry(geometry, feature);
                if (textState.backgroundFill || textState.backgroundStroke) {
                    this.setFillStrokeStyle(textState.backgroundFill, textState.backgroundStroke);
                    this.updateFillStyle(this.state, this.applyFill, geometry);
                    this.updateStrokeStyle(this.state, this.applyStroke);
                }
                this.drawTextImage_(label, begin, end);
                this.endGeometry(geometry, feature);
            }
        };


        /**
         * @param {string} text Text.
         * @param {string} textKey Text style key.
         * @param {string} fillKey Fill style key.
         * @param {string} strokeKey Stroke style key.
         * @return {HTMLCanvasElement} Image.
         */
        ol.render.canvas.TextReplay.prototype.getImage = function(text, textKey, fillKey, strokeKey) {
            var label;
            var key = strokeKey + textKey + text + fillKey + this.pixelRatio;

            var labelCache = ol.render.canvas.labelCache;
            if (!labelCache.containsKey(key)) {
                var strokeState = strokeKey ? this.strokeStates[strokeKey] || this.textStrokeState_ : null;
                var fillState = fillKey ? this.fillStates[fillKey] || this.textFillState_ : null;
                var textState = this.textStates[textKey] || this.textState_;
                var pixelRatio = this.pixelRatio;
                var scale = textState.scale * pixelRatio;
                var align =  ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign];
                var strokeWidth = strokeKey && strokeState.lineWidth ? strokeState.lineWidth : 0;

                var lines = text.split('\n');
                var numLines = lines.length;
                var widths = [];
                var width = ol.render.canvas.TextReplay.measureTextWidths(textState.font, lines, widths);
                var lineHeight = ol.render.canvas.measureTextHeight(textState.font);
                var height = lineHeight * numLines;
                var renderWidth = (width + strokeWidth);
                var context = ol.dom.createCanvasContext2D(
                    Math.ceil(renderWidth * scale),
                    Math.ceil((height + strokeWidth) * scale));
                label = context.canvas;
                labelCache.set(key, label);
                if (scale != 1) {
                    context.scale(scale, scale);
                }
                context.font = textState.font;
                if (strokeKey) {
                    context.strokeStyle = strokeState.strokeStyle;
                    context.lineWidth = strokeWidth * (ol.has.SAFARI ? scale : 1);
                    context.lineCap = strokeState.lineCap;
                    context.lineJoin = strokeState.lineJoin;
                    context.miterLimit = strokeState.miterLimit;
                    if (ol.has.CANVAS_LINE_DASH && strokeState.lineDash.length) {
                        context.setLineDash(strokeState.lineDash);
                        context.lineDashOffset = strokeState.lineDashOffset;
                    }
                }
                if (fillKey) {
                    context.fillStyle = fillState.fillStyle;
                }
                context.textBaseline = 'middle';
                context.textAlign = 'center';
                var leftRight = (0.5 - align);
                var x = align * label.width / scale + leftRight * strokeWidth;
                var i;
                if (strokeKey) {
                    for (i = 0; i < numLines; ++i) {
                        context.strokeText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight);
                    }
                }
                if (fillKey) {
                    for (i = 0; i < numLines; ++i) {
                        context.fillText(lines[i], x + leftRight * widths[i], 0.5 * (strokeWidth + lineHeight) + i * lineHeight);
                    }
                }
            }
            return labelCache.get(key);
        };


        /**
         * @private
         * @param {HTMLCanvasElement} label Label.
         * @param {number} begin Begin.
         * @param {number} end End.
         */
        ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, end) {
            var textState = this.textState_;
            var strokeState = this.textStrokeState_;
            var pixelRatio = this.pixelRatio;
            var align = ol.render.replay.TEXT_ALIGN[textState.textAlign || ol.render.canvas.defaultTextAlign];
            var baseline = ol.render.replay.TEXT_ALIGN[textState.textBaseline];
            var strokeWidth = strokeState && strokeState.lineWidth ? strokeState.lineWidth : 0;

            var anchorX = align * label.width / pixelRatio + 2 * (0.5 - align) * strokeWidth;
            var anchorY = baseline * label.height / pixelRatio + 2 * (0.5 - baseline) * strokeWidth;
            this.instructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end,
                label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
                this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
                1, true, label.width,
                textState.padding == ol.render.canvas.defaultPadding ?
                    ol.render.canvas.defaultPadding : textState.padding.map(function(p) {
                    return p * pixelRatio;
                }),
                !!textState.backgroundFill, !!textState.backgroundStroke
            ]);
            this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end,
                label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio,
                this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_,
                1 / pixelRatio, true, label.width, textState.padding,
                !!textState.backgroundFill, !!textState.backgroundStroke
            ]);
        };


        /**
         * @private
         * @param {number} begin Begin.
         * @param {number} end End.
         * @param {ol.DeclutterGroup} declutterGroup Declutter group.
         */
        ol.render.canvas.TextReplay.prototype.drawChars_ = function(begin, end, declutterGroup) {
            var strokeState = this.textStrokeState_;
            var textState = this.textState_;
            var fillState = this.textFillState_;

            var strokeKey = this.strokeKey_;
            if (strokeState) {
                if (!(strokeKey in this.strokeStates)) {
                    this.strokeStates[strokeKey] = /** @type {ol.CanvasStrokeState} */ ({
                        strokeStyle: strokeState.strokeStyle,
                        lineCap: strokeState.lineCap,
                        lineDashOffset: strokeState.lineDashOffset,
                        lineWidth: strokeState.lineWidth,
                        lineJoin: strokeState.lineJoin,
                        miterLimit: strokeState.miterLimit,
                        lineDash: strokeState.lineDash
                    });
                }
            }
            var textKey = this.textKey_;
            if (!(this.textKey_ in this.textStates)) {
                this.textStates[this.textKey_] = /** @type {ol.CanvasTextState} */ ({
                    font: textState.font,
                    textAlign: textState.textAlign || ol.render.canvas.defaultTextAlign,
                    scale: textState.scale
                });
            }
            var fillKey = this.fillKey_;
            if (fillState) {
                if (!(fillKey in this.fillStates)) {
                    this.fillStates[fillKey] = /** @type {ol.CanvasFillState} */ ({
                        fillStyle: fillState.fillStyle
                    });
                }
            }

            var pixelRatio = this.pixelRatio;
            var baseline = ol.render.replay.TEXT_ALIGN[textState.textBaseline];

            var offsetY = this.textOffsetY_ * pixelRatio;
            var text = this.text_;
            var font = textState.font;
            var textScale = textState.scale;
            var strokeWidth = strokeState ? strokeState.lineWidth * textScale / 2 : 0;
            var widths = this.widths_[font];
            if (!widths) {
                this.widths_[font] = widths = {};
            }
            this.instructions.push([ol.render.canvas.Instruction.DRAW_CHARS,
                begin, end, baseline, declutterGroup,
                textState.overflow, fillKey, textState.maxAngle,
                function(text) {
                    var width = widths[text];
                    if (!width) {
                        width = widths[text] = ol.render.canvas.measureTextWidth(font, text);
                    }
                    return width * textScale * pixelRatio;
                },
                offsetY, strokeKey, strokeWidth * pixelRatio, text, textKey, 1
            ]);
            this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_CHARS,
                begin, end, baseline, declutterGroup,
                textState.overflow, fillKey, textState.maxAngle,
                function(text) {
                    var width = widths[text];
                    if (!width) {
                        width = widths[text] = ol.render.canvas.measureTextWidth(font, text);
                    }
                    return width * textScale;
                },
                offsetY, strokeKey, strokeWidth, text, textKey, 1 / pixelRatio
            ]);
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle, declutterGroup) {
            var textState, fillState, strokeState;
            if (!textStyle) {
                this.text_ = '';
            } else {
                this.declutterGroup_ = /** @type {ol.DeclutterGroup} */ (declutterGroup);

                var textFillStyle = textStyle.getFill();
                if (!textFillStyle) {
                    fillState = this.textFillState_ = null;
                } else {
                    fillState = this.textFillState_;
                    if (!fillState) {
                        fillState = this.textFillState_ = /** @type {ol.CanvasFillState} */ ({});
                    }
                    fillState.fillStyle = ol.colorlike.asColorLike(
                        textFillStyle.getColor() || ol.render.canvas.defaultFillStyle);
                }

                var textStrokeStyle = textStyle.getStroke();
                if (!textStrokeStyle) {
                    strokeState = this.textStrokeState_ = null;
                } else {
                    strokeState = this.textStrokeState_;
                    if (!strokeState) {
                        strokeState = this.textStrokeState_ = /** @type {ol.CanvasStrokeState} */ ({});
                    }
                    var lineDash = textStrokeStyle.getLineDash();
                    var lineDashOffset = textStrokeStyle.getLineDashOffset();
                    var lineWidth = textStrokeStyle.getWidth();
                    var miterLimit = textStrokeStyle.getMiterLimit();
                    strokeState.lineCap = textStrokeStyle.getLineCap() || ol.render.canvas.defaultLineCap;
                    strokeState.lineDash = lineDash ? lineDash.slice() : ol.render.canvas.defaultLineDash;
                    strokeState.lineDashOffset =
                        lineDashOffset === undefined ? ol.render.canvas.defaultLineDashOffset : lineDashOffset;
                    strokeState.lineJoin = textStrokeStyle.getLineJoin() || ol.render.canvas.defaultLineJoin;
                    strokeState.lineWidth =
                        lineWidth === undefined ? ol.render.canvas.defaultLineWidth : lineWidth;
                    strokeState.miterLimit =
                        miterLimit === undefined ? ol.render.canvas.defaultMiterLimit : miterLimit;
                    strokeState.strokeStyle = ol.colorlike.asColorLike(
                        textStrokeStyle.getColor() || ol.render.canvas.defaultStrokeStyle);
                }

                textState = this.textState_;
                var font = textStyle.getFont() || ol.render.canvas.defaultFont;
                //ol.render.canvas.checkFont(font);  // FIXME sunyl
                var textScale = textStyle.getScale();
                textState.overflow = textStyle.getOverflow();
                textState.font = font;
                textState.maxAngle = textStyle.getMaxAngle();
                textState.placement = textStyle.getPlacement();
                textState.textAlign = textStyle.getTextAlign();
                textState.textBaseline = textStyle.getTextBaseline() || ol.render.canvas.defaultTextBaseline;
                textState.backgroundFill = textStyle.getBackgroundFill();
                textState.backgroundStroke = textStyle.getBackgroundStroke();
                textState.padding = textStyle.getPadding() || ol.render.canvas.defaultPadding;
                textState.scale = textScale === undefined ? 1 : textScale;

                var textOffsetX = textStyle.getOffsetX();
                var textOffsetY = textStyle.getOffsetY();
                var textRotateWithView = textStyle.getRotateWithView();
                var textRotation = textStyle.getRotation();
                this.text_ = textStyle.getText() || '';
                this.textOffsetX_ = textOffsetX === undefined ? 0 : textOffsetX;
                this.textOffsetY_ = textOffsetY === undefined ? 0 : textOffsetY;
                this.textRotateWithView_ = textRotateWithView === undefined ? false : textRotateWithView;
                this.textRotation_ = textRotation === undefined ? 0 : textRotation;

                this.strokeKey_ = strokeState ?
                    (typeof strokeState.strokeStyle == 'string' ? strokeState.strokeStyle : ol.getUid(strokeState.strokeStyle)) +
                        strokeState.lineCap + strokeState.lineDashOffset + '|' + strokeState.lineWidth +
                        strokeState.lineJoin + strokeState.miterLimit + '[' + strokeState.lineDash.join() + ']' :
                    '';
                this.textKey_ = textState.font + textState.scale + (textState.textAlign || '?');
                this.fillKey_ = fillState ?
                    (typeof fillState.fillStyle == 'string' ? fillState.fillStyle : ('|' + ol.getUid(fillState.fillStyle))) :
                    '';
            }
        };

        /**
         * @constructor
         * @extends {ol.render.ReplayGroup}
         * @param {number} tolerance Tolerance.
         * @param {ol.Extent} maxExtent Max extent.
         * @param {number} resolution Resolution.
         * @param {number} pixelRatio Pixel ratio.
         * @param {boolean} overlaps The replay group can have overlapping geometries.
         * @param {?} declutterTree Declutter tree
         * for declutter processing in postrender.
         * @param {number=} opt_renderBuffer Optional rendering buffer.
         * @struct
         */
        ol.render.canvas.ReplayGroup = function(
            tolerance, maxExtent, resolution, pixelRatio, overlaps, declutterTree, opt_renderBuffer) {
            ol.render.ReplayGroup.call(this);

            /**
             * Declutter tree.
             * @private
             */
            this.declutterTree_ = declutterTree;

            /**
             * @type {ol.DeclutterGroup}
             * @private
             */
            this.declutterGroup_ = null;

            /**
             * @private
             * @type {number}
             */
            this.tolerance_ = tolerance;

            /**
             * @private
             * @type {ol.Extent}
             */
            this.maxExtent_ = maxExtent;

            /**
             * @private
             * @type {boolean}
             */
            this.overlaps_ = overlaps;

            /**
             * @private
             * @type {number}
             */
            this.pixelRatio_ = pixelRatio;

            /**
             * @private
             * @type {number}
             */
            this.resolution_ = resolution;

            /**
             * @private
             * @type {number|undefined}
             */
            this.renderBuffer_ = opt_renderBuffer;

            /**
             * @private
             * @type {!Object.<string,
       *        Object.<ol.render.ReplayType, ol.render.canvas.Replay>>}
             */
            this.replaysByZIndex_ = {};

            /**
             * @private
             * @type {CanvasRenderingContext2D}
             */
            // FIXME
            this.hitDetectionContext_ = null;

            /**
             * @private
             * @type {ol.Transform}
             */
            this.hitDetectionTransform_ = ol.transform.create();
        };
        ol.inherits(ol.render.canvas.ReplayGroup, ol.render.ReplayGroup);


        /**
         * This cache is used for storing calculated pixel circles for increasing performance.
         * It is a static property to allow each Replaygroup to access it.
         * @type {Object.<number, Array.<Array.<(boolean|undefined)>>>}
         * @private
         */
        ol.render.canvas.ReplayGroup.circleArrayCache_ = {
            0: [[true]]
        };


        /**
         * This method fills a row in the array from the given coordinate to the
         * middle with `true`.
         * @param {Array.<Array.<(boolean|undefined)>>} array The array that will be altered.
         * @param {number} x X coordinate.
         * @param {number} y Y coordinate.
         * @private
         */
        ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_ = function(array, x, y) {
            var i;
            var radius = Math.floor(array.length / 2);
            if (x >= radius) {
                for (i = radius; i < x; i++) {
                    array[i][y] = true;
                }
            } else if (x < radius) {
                for (i = x + 1; i < radius; i++) {
                    array[i][y] = true;
                }
            }
        };


        /**
         * This methods creates a circle inside a fitting array. Points inside the
         * circle are marked by true, points on the outside are undefined.
         * It uses the midpoint circle algorithm.
         * A cache is used to increase performance.
         * @param {number} radius Radius.
         * @returns {Array.<Array.<(boolean|undefined)>>} An array with marked circle points.
         * @private
         */
        ol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) {
            if (ol.render.canvas.ReplayGroup.circleArrayCache_[radius] !== undefined) {
                return ol.render.canvas.ReplayGroup.circleArrayCache_[radius];
            }

            var arraySize = radius * 2 + 1;
            var arr = new Array(arraySize);
            for (var i = 0; i < arraySize; i++) {
                arr[i] = new Array(arraySize);
            }

            var x = radius;
            var y = 0;
            var error = 0;

            while (x >= y) {
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + x, radius + y);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + y, radius + x);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - y, radius + x);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - x, radius + y);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - x, radius - y);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - y, radius - x);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + y, radius - x);
                ol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + x, radius - y);

                y++;
                error += 1 + 2 * y;
                if (2 * (error - x) + 1 > 0) {
                    x -= 1;
                    error += 1 - 2 * x;
                }
            }

            ol.render.canvas.ReplayGroup.circleArrayCache_[radius] = arr;
            return arr;
        };


        /**
         * @param {!Object.<string, Array.<*>>} declutterReplays Declutter replays.
         * @param {CanvasRenderingContext2D} context Context.
         * @param {number} rotation Rotation.
         */
        ol.render.canvas.ReplayGroup.replayDeclutter = function(declutterReplays, context, rotation) {
            var zs = Object.keys(declutterReplays).map(Number).sort(ol.array.numberSafeCompareFunction);
            var skippedFeatureUids = {};
            for (var z = 0, zz = zs.length; z < zz; ++z) {
                var replayData = declutterReplays[zs[z].toString()];
                for (var i = 0, ii = replayData.length; i < ii;) {
                    var replay = replayData[i++];
                    var transform = replayData[i++];
                    replay.replay(context, transform, rotation, skippedFeatureUids);
                }
            }
        };


        /**
         * @param {boolean} group Group with previous replay.
         * @return {ol.DeclutterGroup} Declutter instruction group.
         */
        ol.render.canvas.ReplayGroup.prototype.addDeclutter = function(group) {
            var declutter = null;
            if (this.declutterTree_) {
                if (group) {
                    declutter = this.declutterGroup_;
                    /** @type {number} */ (declutter[4])++;
                } else {
                    declutter = this.declutterGroup_ = ol.extent.createEmpty();
                    declutter.push(1);
                }
            }
            return declutter;
        };


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Transform} transform Transform.
         */
        ol.render.canvas.ReplayGroup.prototype.clip = function(context, transform) {
            var flatClipCoords = this.getClipCoords(transform);
            context.beginPath();
            context.moveTo(flatClipCoords[0], flatClipCoords[1]);
            context.lineTo(flatClipCoords[2], flatClipCoords[3]);
            context.lineTo(flatClipCoords[4], flatClipCoords[5]);
            context.lineTo(flatClipCoords[6], flatClipCoords[7]);
            context.clip();
        };


        /**
         * @param {Array.<ol.render.ReplayType>} replays Replays.
         * @return {boolean} Has replays of the provided types.
         */
        ol.render.canvas.ReplayGroup.prototype.hasReplays = function(replays) {
            for (var zIndex in this.replaysByZIndex_) {
                var candidates = this.replaysByZIndex_[zIndex];
                for (var i = 0, ii = replays.length; i < ii; ++i) {
                    if (replays[i] in candidates) {
                        return true;
                    }
                }
            }
            return false;
        };


        /**
         * FIXME empty description for jsdoc
         */
        ol.render.canvas.ReplayGroup.prototype.finish = function() {
            var zKey;
            for (zKey in this.replaysByZIndex_) {
                var replays = this.replaysByZIndex_[zKey];
                var replayKey;
                for (replayKey in replays) {
                    replays[replayKey].finish();
                }
            }
        };


        /**
         * @param {ol.Coordinate} coordinate Coordinate.
         * @param {number} resolution Resolution.
         * @param {number} rotation Rotation.
         * @param {number} hitTolerance Hit tolerance in pixels.
         * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
         *     to skip.
         * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
         *     callback.
         * @param {Object.<string, ol.DeclutterGroup>} declutterReplays Declutter
         *     replays.
         * @return {T|undefined} Callback result.
         * @template T
         */
        ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
            coordinate, resolution, rotation, hitTolerance, skippedFeaturesHash, callback, declutterReplays) {

            hitTolerance = Math.round(hitTolerance);
            var contextSize = hitTolerance * 2 + 1;
            var transform = ol.transform.compose(this.hitDetectionTransform_,
                hitTolerance + 0.5, hitTolerance + 0.5,
                1 / resolution, -1 / resolution,
                -rotation,
                -coordinate[0], -coordinate[1]);
            var context = this.hitDetectionContext_;

            if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) {
                context.canvas.width = contextSize;
                context.canvas.height = contextSize;
            } else {
                context.clearRect(0, 0, contextSize, contextSize);
            }

            /**
             * @type {ol.Extent}
             */
            var hitExtent;
            if (this.renderBuffer_ !== undefined) {
                hitExtent = ol.extent.createEmpty();
                ol.extent.extendCoordinate(hitExtent, coordinate);
                ol.extent.buffer(hitExtent, resolution * (this.renderBuffer_ + hitTolerance), hitExtent);
            }

            var mask = ol.render.canvas.ReplayGroup.getCircleArray_(hitTolerance);
            var declutteredFeatures;
            if (this.declutterTree_) {
                declutteredFeatures = this.declutterTree_.all().map(function(entry) {
                    return entry.value;
                });
            }

            var replayType;

            /**
             * @param {ol.Feature|ol.render.Feature} feature Feature.
             * @return {?} Callback result.
             */
            function featureCallback(feature) {
                var imageData = context.getImageData(0, 0, contextSize, contextSize).data;
                for (var i = 0; i < contextSize; i++) {
                    for (var j = 0; j < contextSize; j++) {
                        if (mask[i][j]) {
                            if (imageData[(j * contextSize + i) * 4 + 3] > 0) {
                                var result;
                                if (!(declutteredFeatures && (replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) ||
                                    declutteredFeatures.indexOf(feature) !== -1) {
                                    result = callback(feature);
                                }
                                if (result) {
                                    return result;
                                } else {
                                    context.clearRect(0, 0, contextSize, contextSize);
                                    return undefined;
                                }
                            }
                        }
                    }
                }
            }

            /** @type {Array.<number>} */
            var zs = Object.keys(this.replaysByZIndex_).map(Number);
            zs.sort(ol.array.numberSafeCompareFunction);

            var i, j, replays, replay, result;
            for (i = zs.length - 1; i >= 0; --i) {
                var zIndexKey = zs[i].toString();
                replays = this.replaysByZIndex_[zIndexKey];
                for (j = ol.render.replay.ORDER.length - 1; j >= 0; --j) {
                    replayType = ol.render.replay.ORDER[j];
                    replay = replays[replayType];
                    if (replay !== undefined) {
                        if (declutterReplays &&
                            (replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) {
                            var declutter = declutterReplays[zIndexKey];
                            if (!declutter) {
                                declutterReplays[zIndexKey] = [replay, transform.slice(0)];
                            } else {
                                declutter.push(replay, transform.slice(0));
                            }
                        } else {
                            result = replay.replayHitDetection(context, transform, rotation,
                                skippedFeaturesHash, featureCallback, hitExtent);
                            if (result) {
                                return result;
                            }
                        }
                    }
                }
            }
            return undefined;
        };


        /**
         * @param {ol.Transform} transform Transform.
         * @return {Array.<number>} Clip coordinates.
         */
        ol.render.canvas.ReplayGroup.prototype.getClipCoords = function(transform) {
            var maxExtent = this.maxExtent_;
            var minX = maxExtent[0];
            var minY = maxExtent[1];
            var maxX = maxExtent[2];
            var maxY = maxExtent[3];
            var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY];
            ol.geom.flat.transform.transform2D(
                flatClipCoords, 0, 8, 2, transform, flatClipCoords);
            return flatClipCoords;
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {
            var zIndexKey = zIndex !== undefined ? zIndex.toString() : '0';
            var replays = this.replaysByZIndex_[zIndexKey];
            if (replays === undefined) {
                replays = {};
                this.replaysByZIndex_[zIndexKey] = replays;
            }
            var replay = replays[replayType];
            if (replay === undefined) {
                var Constructor = ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_[replayType];
                replay = new Constructor(this.tolerance_, this.maxExtent_,
                    this.resolution_, this.pixelRatio_, this.overlaps_, this.declutterTree_);
                replays[replayType] = replay;
            }
            return replay;
        };


        /**
         * @return {Object.<string, Object.<ol.render.ReplayType, ol.render.canvas.Replay>>} Replays.
         */
        ol.render.canvas.ReplayGroup.prototype.getReplays = function() {
            return this.replaysByZIndex_;
        };


        /**
         * @inheritDoc
         */
        ol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
            return ol.obj.isEmpty(this.replaysByZIndex_);
        };


        /**
         * @param {CanvasRenderingContext2D} context Context.
         * @param {ol.Transform} transform Transform.
         * @param {number} viewRotation View rotation.
         * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
         *     to skip.
         * @param {Array.<ol.render.ReplayType>=} opt_replayTypes Ordered replay types
         *     to replay. Default is {@link ol.render.replay.ORDER}
         * @param {Object.<string, ol.DeclutterGroup>=} opt_declutterReplays Declutter
         *     replays.
         */
        ol.render.canvas.ReplayGroup.prototype.replay = function(context,
                                                                 transform, viewRotation, skippedFeaturesHash, opt_replayTypes, opt_declutterReplays) {

            /** @type {Array.<number>} */
            var zs = Object.keys(this.replaysByZIndex_).map(Number);
            zs.sort(ol.array.numberSafeCompareFunction);

            // setup clipping so that the parts of over-simplified geometries are not
            // visible outside the current extent when panning
            context.save();
            this.clip(context, transform);

            var replayTypes = opt_replayTypes ? opt_replayTypes : ol.render.replay.ORDER;
            var i, ii, j, jj, replays, replay;
            for (i = 0, ii = zs.length; i < ii; ++i) {
                var zIndexKey = zs[i].toString();
                replays = this.replaysByZIndex_[zIndexKey];
                for (j = 0, jj = replayTypes.length; j < jj; ++j) {
                    var replayType = replayTypes[j];
                    replay = replays[replayType];
                    if (replay !== undefined) {
                        if (opt_declutterReplays &&
                            (replayType == ol.render.ReplayType.IMAGE || replayType == ol.render.ReplayType.TEXT)) {
                            var declutter = opt_declutterReplays[zIndexKey];
                            if (!declutter) {
                                opt_declutterReplays[zIndexKey] = [replay, transform.slice(0)];
                            } else {
                                declutter.push(replay, transform.slice(0));
                            }
                        } else {
                            replay.replay(context, transform, viewRotation, skippedFeaturesHash);
                        }
                    }
                }
            }

            context.restore();
        };


        /**
         * @const
         * @private
         * @type {Object.<ol.render.ReplayType,
     *                function(new: ol.render.canvas.Replay, number, ol.Extent,
     *                number, number, boolean, Array.<ol.DeclutterGroup>)>}
         */
        ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = {
            'Circle': ol.render.canvas.PolygonReplay,
            'Default': ol.render.canvas.Replay,
            'Image': ol.render.canvas.ImageReplay,
            'LineString': ol.render.canvas.LineStringReplay,
            'Polygon': ol.render.canvas.PolygonReplay,
            'Text': ol.render.canvas.TextReplay
        };

        ol.renderer = {};
        ol.renderer.vector = {};

        /**
         * @param {ol.Feature|ol.render.Feature} feature1 Feature 1.
         * @param {ol.Feature|ol.render.Feature} feature2 Feature 2.
         * @return {number} Order.
         */
        ol.renderer.vector.defaultOrder = function(feature1, feature2) {
            return ol.getUid(feature1) - ol.getUid(feature2);
        };


        /**
         * @param {number} resolution Resolution.
         * @param {number} pixelRatio Pixel ratio.
         * @return {number} Squared pixel tolerance.
         */
        ol.renderer.vector.getSquaredTolerance = function(resolution, pixelRatio) {
            var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio);
            return tolerance * tolerance;
        };


        /**
         * @param {number} resolution Resolution.
         * @param {number} pixelRatio Pixel ratio.
         * @return {number} Pixel tolerance.
         */
        ol.renderer.vector.getTolerance = function(resolution, pixelRatio) {
            return ol.SIMPLIFY_TOLERANCE * resolution / pixelRatio;
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.Circle} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style, feature) {
            var fillStyle = style.getFill();
            var strokeStyle = style.getStroke();
            if (fillStyle || strokeStyle) {
                var circleReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.CIRCLE);
                circleReplay.setFillStrokeStyle(fillStyle, strokeStyle);
                circleReplay.drawCircle(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @param {ol.style.Style} style Style.
         * @param {number} squaredTolerance Squared tolerance.
         * @param {function(this: T, ol.events.Event)} listener Listener function.
         * @param {T} thisArg Value to use as `this` when executing `listener`.
         * @return {boolean} `true` if style is loading.
         * @template T
         */
        ol.renderer.vector.renderFeature = function(
            replayGroup, feature, style, squaredTolerance, listener, thisArg) {
            var loading = false;
            var imageStyle, imageState;
            imageStyle = style.getImage();
            if (imageStyle) {
                imageState = imageStyle.getImageState();
                if (imageState == ol.ImageState.LOADED ||
                    imageState == ol.ImageState.ERROR) {
                    imageStyle.unlistenImageChange(listener, thisArg);
                } else {
                    if (imageState == ol.ImageState.IDLE) {
                        imageStyle.load();
                    }
                    imageState = imageStyle.getImageState();
                    imageStyle.listenImageChange(listener, thisArg);
                    loading = true;
                }
            }
            ol.renderer.vector.renderFeature_(replayGroup, feature, style,
                squaredTolerance);

            return loading;
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @param {ol.style.Style} style Style.
         * @param {number} squaredTolerance Squared tolerance.
         * @private
         */
        ol.renderer.vector.renderFeature_ = function(
            replayGroup, feature, style, squaredTolerance) {
            var geometry = style.getGeometryFunction()(feature);
            if (!geometry) {
                return;
            }
            var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
            var renderer = style.getRenderer();
            if (renderer) {
                ol.renderer.vector.renderGeometry_(replayGroup, simplifiedGeometry, style, feature);
            } else {
                var geometryRenderer =
                    ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
                geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.Geometry} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderGeometry_ = function(replayGroup, geometry, style, feature) {
            if (geometry.getType() == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
                var geometries = /** @type {ol.geom.GeometryCollection} */ (geometry).getGeometries();
                for (var i = 0, ii = geometries.length; i < ii; ++i) {
                    ol.renderer.vector.renderGeometry_(replayGroup, geometries[i], style, feature);
                }
                return;
            }
            var replay = replayGroup.getReplay(style.getZIndex(), ol.render.ReplayType.DEFAULT);
            replay.drawCustom(/** @type {ol.geom.SimpleGeometry} */ (geometry), feature, style.getRenderer());
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.GeometryCollection} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderGeometryCollectionGeometry_ = function(replayGroup, geometry, style, feature) {
            var geometries = geometry.getGeometriesArray();
            var i, ii;
            for (i = 0, ii = geometries.length; i < ii; ++i) {
                var geometryRenderer =
                    ol.renderer.vector.GEOMETRY_RENDERERS_[geometries[i].getType()];
                geometryRenderer(replayGroup, geometries[i], style, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.LineString|ol.render.Feature} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
            var strokeStyle = style.getStroke();
            if (strokeStyle) {
                var lineStringReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.LINE_STRING);
                lineStringReplay.setFillStrokeStyle(null, strokeStyle);
                lineStringReplay.drawLineString(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.MultiLineString|ol.render.Feature} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
            var strokeStyle = style.getStroke();
            if (strokeStyle) {
                var lineStringReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.LINE_STRING);
                lineStringReplay.setFillStrokeStyle(null, strokeStyle);
                lineStringReplay.drawMultiLineString(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.MultiPolygon} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderMultiPolygonGeometry_ = function(replayGroup, geometry, style, feature) {
            var fillStyle = style.getFill();
            var strokeStyle = style.getStroke();
            if (strokeStyle || fillStyle) {
                var polygonReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.POLYGON);
                polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
                polygonReplay.drawMultiPolygon(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.Point|ol.render.Feature} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style, feature) {
            var imageStyle = style.getImage();
            if (imageStyle) {
                if (imageStyle.getImageState() != ol.ImageState.LOADED) {
                    return;
                }
                var imageReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.IMAGE);
                imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false));
                imageReplay.drawPoint(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.MultiPoint|ol.render.Feature} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, style, feature) {
            var imageStyle = style.getImage();
            if (imageStyle) {
                if (imageStyle.getImageState() != ol.ImageState.LOADED) {
                    return;
                }
                var imageReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.IMAGE);
                imageReplay.setImageStyle(imageStyle, replayGroup.addDeclutter(false));
                imageReplay.drawMultiPoint(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(!!imageStyle));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @param {ol.render.ReplayGroup} replayGroup Replay group.
         * @param {ol.geom.Polygon|ol.render.Feature} geometry Geometry.
         * @param {ol.style.Style} style Style.
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @private
         */
        ol.renderer.vector.renderPolygonGeometry_ = function(replayGroup, geometry, style, feature) {
            var fillStyle = style.getFill();
            var strokeStyle = style.getStroke();
            if (fillStyle || strokeStyle) {
                var polygonReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.POLYGON);
                polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
                polygonReplay.drawPolygon(geometry, feature);
            }
            var textStyle = style.getText();
            if (textStyle) {
                var textReplay = replayGroup.getReplay(
                    style.getZIndex(), ol.render.ReplayType.TEXT);
                textReplay.setTextStyle(textStyle, replayGroup.addDeclutter(false));
                textReplay.drawText(geometry, feature);
            }
        };


        /**
         * @const
         * @private
         * @type {Object.<ol.geom.GeometryType,
     *                function(ol.render.ReplayGroup, ol.geom.Geometry,
     *                         ol.style.Style, Object)>}
         */
        ol.renderer.vector.GEOMETRY_RENDERERS_ = {
            'Point': ol.renderer.vector.renderPointGeometry_,
            'LineString': ol.renderer.vector.renderLineStringGeometry_,
            'Polygon': ol.renderer.vector.renderPolygonGeometry_,
            'MultiPoint': ol.renderer.vector.renderMultiPointGeometry_,
            'MultiLineString': ol.renderer.vector.renderMultiLineStringGeometry_,
            'MultiPolygon': ol.renderer.vector.renderMultiPolygonGeometry_,
            'GeometryCollection': ol.renderer.vector.renderGeometryCollectionGeometry_,
            'Circle': ol.renderer.vector.renderCircleGeometry_
        };

    var spriteImageCanvas = {};

    var mvtStyleClass = new MvtStyle(ol, true);
    var mvtRenderer2D = new MvtRenderer2D({
        mvtStyle: mvtStyleClass,
        openlayer: ol,
        useOffscreen: true
    });

    function MVTWorker(parameters, transferableObjects) {
        var canvas = new OffscreenCanvas(parameters.canvasWidth, parameters.canvasWidth);
        var idCanvas = new OffscreenCanvas(parameters.canvasWidth, parameters.canvasWidth);
        var pbfData = parameters.pbfData;
        var layers = parameters.layers;
        var transform = parameters.transform;
        var squaredTolerance = parameters.squaredTolerance;
        var spriteImageDatas = parameters.spriteImageDatas;
        var keepProperties = parameters.keepProperties;
        var tileLevel = parameters.tileLevel;
        var needSourceLayerNames = parameters.needSourceLayerNames;
        var selectEnabled = parameters.selectEnabled;
        var featureProperties = {};

        try {
            var mvtParser = new ol.format.MVT({
                featureClass: ol.Feature
            });
            var features = mvtParser.readFeatures(pbfData, {
                needSourceLayerNames: needSourceLayerNames
            });
            var renderResult = mvtRenderer2D.renderFeatures({
                colorCanvas: canvas,
                idCanvas: idCanvas,
                transform: transform,
                layers: layers,
                features: features,
                tileLevel: tileLevel,
                spriteImageCanvas: spriteImageCanvas,
                spriteImageDatas: spriteImageDatas,
                squaredTolerance: squaredTolerance,
                selectEnabled: selectEnabled,
                showBillboard: false
            });

            if (keepProperties) {
                var featuresToRender = renderResult.idFeatures;
                var featuresToRenderLength = featuresToRender.length;
                for (var i = 0; i < featuresToRenderLength; i++) {
                    var feature = featuresToRender[i];
                    var featureID = getFeatureID$1(feature);
                    var propertiesWithoutGeometry = feature.getProperties();
                    if (when.defined(propertiesWithoutGeometry.geometry)) {
                        delete propertiesWithoutGeometry.geometry;
                    }
                    featureProperties[featureID] = propertiesWithoutGeometry;
                }
            }
        }
        catch (err) {
        }

        var imageBitmap = canvas.transferToImageBitmap();
        var idImageBitmap = selectEnabled ? idCanvas.transferToImageBitmap() : null;
        transferableObjects.push(imageBitmap);
        return {
            buffer: imageBitmap,
            idBuffer: idImageBitmap,
            properties: featureProperties
        };
    }

    function getFeatureID$1(feature) {
        var id = feature.getId();
        // 只在颜色中记录256*256*256这么大范围的ID，超过这个范围的ID舍去
        var discard = Math.floor(id / 16777216);
        id = id - discard * 16777216;
        return id;
    }

    var MVTWorker$1 = createTaskProcessorWorker(MVTWorker);

    return MVTWorker$1;

});
