(function(factory, window) {
    // module loaders support for LeafletJS plugins, see:
    // https://github.com/Leaflet/Leaflet/blob/master/PLUGIN-GUIDE.md#module-loaders

    // AMD module that relies on "leaflet"
    if (typeof define === 'function' && define.amd) {
        define(['leaflet'], factory);

        // Common JS module that relies on "leaflet"
    } else if (typeof exports === 'object') {
        module.exports = factory(require('leaflet'));
    }

    // attach plugin to the global leaflet "L" variable
    if (typeof window !== 'undefined' && window.L) {
        window.L.CanvasFlowmapLayer = factory(L);

        window.L.canvasFlowmapLayer = function(originAndDestinationGeoJsonPoints, opts) {
            return new window.L.CanvasFlowmapLayer(originAndDestinationGeoJsonPoints, opts);
        };
    }
}(function(L) {
    // layer source code
    var canvasRenderer = L.canvas();

    var CanvasFlowmapLayer = L.GeoJSON.extend({
        options: {
        // this is only a default option example,
        // developers will most likely need to provide this
        // options object with values unique to their data
            originAndDestinationFieldIds: {
                originUniqueIdField: 'origin_id',
                originGeometry: {
                    x: 'origin_lon',
                    y: 'origin_lat'
                },
                destinationUniqueIdField: 'destination_id',
                destinationGeometry: {
                    x: 'destination_lon',
                    y: 'destination_lat'
                }
            },
            canvasBezierStyle: {
                type: 'simple',
                symbol: {
                    // use canvas styling options (compare to CircleMarker styling below)
                    strokeStyle: 'rgba(255, 0, 51, 0.8)',
                    lineWidth: 0.75,
                    lineCap: 'round',
                    shadowColor: 'rgb(255, 0, 51)',
                    shadowBlur: 1.5
                }
            },
            animatedCanvasBezierStyle: {
                type: 'simple',
                symbol: {
                    // use canvas styling options (compare to CircleMarker styling below)
                    strokeStyle: 'rgb(255, 46, 88)',
                    lineWidth: 1.25,
                    lineDashOffsetSize: 4, // custom property used with animation sprite sizes
                    lineCap: 'round',
                    shadowColor: 'rgb(255, 0, 51)',
                    shadowBlur: 2
                }
            },
            // valid values: 'selection' or 'all'
            // use 'all' to display all Bezier paths immediately
            // use 'selection' if Bezier paths will be drawn with user interactions
            pathDisplayMode: 'all',
            wrapAroundCanvas: true,
            animationStarted: false,
            animationEasingFamily: 'Cubic',
            animationEasingType: 'In',
            animationDuration: 2000,
            pointToLayer: function(geoJsonPoint, latlng) {
                return L.circleMarker(latlng);
            },
            style: function(geoJsonFeature) {
                // use leaflet's path styling options
                // since the GeoJSON feature properties are modified by the layer,
                // developers can rely on the "isOrigin" property to set different
                // symbols for origin vs destination CircleMarker stylings
                if (geoJsonFeature.properties.isOrigin) {
                    return {
                        renderer: canvasRenderer, // recommended to use L.canvas()
                        radius: 5,
                        weight: 1,
                        color: 'rgb(195, 255, 62)',
                        fillColor: 'rgba(195, 255, 62, 0.6)',
                        fillOpacity: 0.6
                    };
                } else {
                    return {
                        renderer: canvasRenderer,
                        radius: 2.5,
                        weight: 0.25,
                        color: 'rgb(17, 142, 170)',
                        fillColor: 'rgb(17, 142, 170)',
                        fillOpacity: 0.7
                    };
                }
            }
        },

        _customCanvases: [],

        initialize: function(geoJson, options) {
        // same as L.GeoJSON intialize method, but first performs custom GeoJSON
        // data parsing and reformatting before finally calling L.GeoJSON addData method
            L.setOptions(this, options);

            this._animationPropertiesStatic = {
                offset: 0,
                resetOffset: 200,
                repeat: Infinity,
                yoyo: false
            };

            this._animationPropertiesDynamic = {
                duration: null,
                easingInfo: null
            };

            this._layers = {};

            // beginning of customized initialize method
            if (geoJson && this.options.originAndDestinationFieldIds) {
                this.setOriginAndDestinationGeoJsonPoints(geoJson);
            }

            // establish animation properties using Tween.js library
            // currently requires the developer to add it to their own app index.html
            // TODO: find better way to wrap it up in this layer source code
            if (window.hasOwnProperty('TWEEN')) {
                // set this._animationPropertiesDynamic.duration value
                this.setAnimationDuration(this.options.animationDuration);
                // set this._animationPropertiesDynamic.easingInfo value
                this.setAnimationEasing(this.options.animationEasingFamily, this.options.animationEasingType);

                // initiate the active animation tween
                this._animationTween = new TWEEN.Tween(this._animationPropertiesStatic)
                    .to({
                        offset: this._animationPropertiesStatic.resetOffset
                    }, this._animationPropertiesDynamic.duration)
                    .easing(this._animationPropertiesDynamic.easingInfo.tweenEasingFunction)
                    .repeat(this._animationPropertiesStatic.repeat)
                    .yoyo(this._animationPropertiesStatic.yoyo)
                    .start();
            } else {
                // Tween.js lib isn't available,
                // ensure that animations aren't attempted at the beginning
                this.options.animationStarted = false;
            }
        },

        setOriginAndDestinationGeoJsonPoints: function(geoJsonFeatureCollection) {
            if (geoJsonFeatureCollection.features) {
                var configOriginGeometryObject = this.options.originAndDestinationFieldIds.originGeometry;
                var configDestinationGeometryObject = this.options.originAndDestinationFieldIds.destinationGeometry;

                geoJsonFeatureCollection.features.forEach(function(feature, index) {
                    if (feature.type === 'Feature' && feature.geometry && feature.geometry.type === 'Point') {
                        // origin feature -- modify attribute properties and geometry
                        feature.properties.isOrigin = true;
                        feature.properties._isSelectedForPathDisplay = this.options.pathDisplayMode === 'all' ? true : false;
                        feature.properties._uniqueId = index + '_origin';

                        feature.geometry.coordinates = [
                            feature.properties[configOriginGeometryObject.x],
                            feature.properties[configOriginGeometryObject.y]
                        ];

                        // destination feature -- clone, modify, and push to feature collection
                        var destinationFeature = JSON.parse(JSON.stringify(feature));

                        destinationFeature.properties.isOrigin = false;
                        destinationFeature.properties._isSelectedForPathDisplay = false;
                        destinationFeature.properties._uniqueId = index + '_destination';

                        destinationFeature.geometry.coordinates = [
                            destinationFeature.properties[configDestinationGeometryObject.x],
                            destinationFeature.properties[configDestinationGeometryObject.y]
                        ];

                        geoJsonFeatureCollection.features.push(destinationFeature);
                    }
                }, this);

                // all origin/destination features are available for future internal used
                // but only a filtered subset of these are drawn on the map
                this.originAndDestinationGeoJsonPoints = geoJsonFeatureCollection;
                var geoJsonPointsToDraw = this._filterGeoJsonPointsToDraw(geoJsonFeatureCollection);
                this.addData(geoJsonPointsToDraw);
            } else {
                // TODO: improved handling of invalid incoming GeoJson FeatureCollection?
                this.originAndDestinationGeoJsonPoints = null;
            }

            return this;
        },

        onAdd: function(map) {
        // call the L.GeoJSON onAdd method,
        // then continue with custom code
            L.GeoJSON.prototype.onAdd.call(this, map);

            // create new canvas element for optional, animated bezier curves
            this._animationCanvasElement = this._insertCustomCanvasElement(map, this.options);

            // create new canvas element for manually drawing bezier curves
            //  - most of the magic happens in this canvas element
            //  - this canvas element is established last because it will be
            //    inserted before (underneath) the animation canvas element
            this._canvasElement = this._insertCustomCanvasElement(map, this.options);

            // create a reference to both canvas elements in an array for convenience
            this._customCanvases = [this._canvasElement, this._animationCanvasElement]

            // establish custom event listeners
            this.on('click mouseover', this._modifyInteractionEvent, this);
            map.on('move', this._resetCanvas, this);
            map.on('moveend', this._resetCanvasAndWrapGeoJsonCircleMarkers, this);
            map.on('resize', this._resizeCanvas, this);
            if (map.options.zoomAnimation && L.Browser.any3d) {
                map.on('zoomanim', this._animateZoom, this);
            }

            // calculate initial size and position of canvas
            // and draw its content for the first time
            this._resizeCanvas();
            this._resetCanvasAndWrapGeoJsonCircleMarkers();

            return this;
        },

        onRemove: function(map) {
        // call the L.GeoJSON onRemove method,
        // then continue with custom code
            L.GeoJSON.prototype.onRemove.call(this, map);

            this._clearCanvas();

            this._customCanvases.forEach(function(canvas) {
                L.DomUtil.remove(canvas);
            });

            // remove custom event listeners
            this.off('click mouseover', this._modifyInteractionEvent, this);
            map.off('move', this._resetCanvas, this);
            map.off('moveend', this._resetCanvasAndWrapGeoJsonCircleMarkers, this);
            map.off('resize', this._resizeCanvas, this);
            if (map.options.zoomAnimation) {
                map.off('zoomanim', this._animateZoom, this);
            }

            return this;
        },

        bringToBack: function() {
        // call the L.GeoJSON bringToBack method to manage the point graphics
            L.GeoJSON.prototype.bringToBack.call(this);

            // keep the animation canvas element on top of the main canvas element
            L.DomUtil.toBack(this._animationCanvasElement);

            // keep the main canvas element underneath the animation canvas element
            L.DomUtil.toBack(this._canvasElement);

            return this;
        },

        bringToFront: function() {
        // keep the main canvas element underneath the animation canvas element
            L.DomUtil.toFront(this._canvasElement);

            // keep the animation canvas element on top of the main canvas element
            L.DomUtil.toFront(this._animationCanvasElement);

            // call the L.GeoJSON bringToFront method to manage the point graphics
            L.GeoJSON.prototype.bringToFront.call(this);

            return this;
        },

        setAnimationDuration: function(milliseconds) {
            milliseconds = Number(milliseconds) || this.options.animationDuration;

            // change the tween duration on the active animation tween
            if (this._animationTween) {
                this._animationTween.to({
                    offset: this._animationPropertiesStatic.resetOffset
                }, milliseconds);
            }

            this._animationPropertiesDynamic.duration = milliseconds;
        },

        setAnimationEasing: function(easingFamily, easingType) {
            var tweenEasingFunction;
            if (
                TWEEN.Easing.hasOwnProperty(easingFamily) &&
          TWEEN.Easing[easingFamily].hasOwnProperty(easingType)
            ) {
                tweenEasingFunction = TWEEN.Easing[easingFamily][easingType];
            } else {
                easingFamily = this.options.animationEasingFamily;
                easingType = this.options.animationEasingType;
                tweenEasingFunction = TWEEN.Easing[easingFamily][easingType];
            }

            // change the tween easing function on the active animation tween
            if (this._animationTween) {
                this._animationTween.easing(tweenEasingFunction);
            }

            this._animationPropertiesDynamic.easingInfo = {
                easingFamily: easingFamily,
                easingType: easingType,
                tweenEasingFunction: tweenEasingFunction
            };
        },

        getAnimationEasingOptions: function(prettyPrint) {
            var tweenEasingConsoleOptions = {};
            var tweenEasingOptions = {};

            Object.keys(TWEEN.Easing).forEach(function(family) {
                tweenEasingConsoleOptions[family] = {
                    types: Object.keys(TWEEN.Easing[family]).join('", "')
                };

                tweenEasingOptions[family] = {
                    types: Object.keys(TWEEN.Easing[family])
                };
            });

            if (!!prettyPrint) {
                console.table(tweenEasingConsoleOptions);
            }

            return tweenEasingOptions;
        },

        playAnimation: function() {
            this.options.animationStarted = true;
            this._redrawCanvas();
        },

        stopAnimation: function() {
            this.options.animationStarted = false;
            this._redrawCanvas();
        },

        selectFeaturesForPathDisplay: function(selectionFeatures, selectionMode) {
            this._applyFeaturesSelection(selectionFeatures, selectionMode, '_isSelectedForPathDisplay');
        },

        selectFeaturesForPathDisplayById: function(uniqueOriginOrDestinationIdField, idValue, originBoolean, selectionMode) {
            if (
                uniqueOriginOrDestinationIdField !== this.options.originAndDestinationFieldIds.originUniqueIdField &&
          uniqueOriginOrDestinationIdField !== this.options.originAndDestinationFieldIds.destinationUniqueIdField
            ) {
                console.error('Invalid unique id field supplied for origin or destination. It must be one of these: ' +
            this.options.originAndDestinationFieldIds.originUniqueIdField + ', ' + this.options.originAndDestinationFieldIds.destinationUniqueIdField);
                return;
            }

            var existingOriginOrDestinationFeature = this.originAndDestinationGeoJsonPoints.features.filter(function(feature) {
                return feature.properties.isOrigin === originBoolean &&
            feature.properties[uniqueOriginOrDestinationIdField] === idValue;
            })[0];

            var odInfo = this._getSharedOriginOrDestinationFeatures(existingOriginOrDestinationFeature);

            if (odInfo.isOriginFeature) {
                this.selectFeaturesForPathDisplay(odInfo.sharedOriginFeatures, selectionMode);
            } else {
                this.selectFeaturesForPathDisplay(odInfo.sharedDestinationFeatures, selectionMode);
            }
        },

        clearAllPathSelections: function() {
            this.originAndDestinationGeoJsonPoints.features.forEach(function(feature) {
                feature.properties._isSelectedForPathDisplay = false;
            });

            this._resetCanvas();
        },

        _filterGeoJsonPointsToDraw: function(geoJsonFeatureCollection) {
            var newGeoJson = {
                type: 'FeatureCollection',
                features: []
            };

            var originUniqueIdValues = [];
            var destinationUniqueIdValues = [];

            var originUniqueIdField = this.options.originAndDestinationFieldIds.originUniqueIdField;
            var destinationUniqueIdField = this.options.originAndDestinationFieldIds.destinationUniqueIdField;

            geoJsonFeatureCollection.features.forEach(function(feature) {
                var isOrigin = feature.properties.isOrigin;

                if (isOrigin && originUniqueIdValues.indexOf(feature.properties[originUniqueIdField]) === -1) {
                    originUniqueIdValues.push(feature.properties[originUniqueIdField]);
                    newGeoJson.features.push(feature);
                } else if (!isOrigin && destinationUniqueIdValues.indexOf(feature.properties[destinationUniqueIdField]) === -1) {
                    destinationUniqueIdValues.push(feature.properties[destinationUniqueIdField]);
                    newGeoJson.features.push(feature);
                } else {
                    // do not attempt to draw an origin or destination circle on the canvas if it is already in one of the tracking arrays
                    return;
                }
            });

            return newGeoJson;
        },

        _insertCustomCanvasElement: function(map, options) {
            var canvas = L.DomUtil.create('canvas', 'leaflet-zoom-animated');

            var originProp = L.DomUtil.testProp(['transformOrigin', 'WebkitTransformOrigin', 'msTransformOrigin']);
            canvas.style[originProp] = '50% 50%';

            var pane = map.getPane(options.pane);
            pane.insertBefore(canvas, pane.firstChild);

            return canvas;
        },

        _modifyInteractionEvent: function(e) {
            var odInfo = this._getSharedOriginOrDestinationFeatures(e.layer.feature);
            e.isOriginFeature = odInfo.isOriginFeature;
            e.sharedOriginFeatures = odInfo.sharedOriginFeatures;
            e.sharedDestinationFeatures = odInfo.sharedDestinationFeatures;
        },

        _getSharedOriginOrDestinationFeatures: function(testFeature) {
            var isOriginFeature = testFeature.properties.isOrigin;
            var sharedOriginFeatures = [];
            var sharedDestinationFeatures = [];

            if (isOriginFeature) {
                // for an ORIGIN point that was interacted with,
                // make an array of all other ORIGIN features with the same ORIGIN ID field
                var originUniqueIdField = this.options.originAndDestinationFieldIds.originUniqueIdField;
                var testFeatureOriginId = testFeature.properties[originUniqueIdField];
                sharedOriginFeatures = this.originAndDestinationGeoJsonPoints.features.filter(function(feature) {
                    return feature.properties.isOrigin &&
              feature.properties[originUniqueIdField] === testFeatureOriginId;
                });
            } else {
                // for a DESTINATION point that was interacted with,
                // make an array of all other ORIGIN features with the same DESTINATION ID field
                var destinationUniqueIdField = this.options.originAndDestinationFieldIds.destinationUniqueIdField;
                var testFeatureDestinationId = testFeature.properties[destinationUniqueIdField];
                sharedDestinationFeatures = this.originAndDestinationGeoJsonPoints.features.filter(function(feature) {
                    return feature.properties.isOrigin &&
              feature.properties[destinationUniqueIdField] === testFeatureDestinationId;
                });
            }

            return {
                isOriginFeature: isOriginFeature, // Boolean
                sharedOriginFeatures: sharedOriginFeatures, // Array of features
                sharedDestinationFeatures: sharedDestinationFeatures // Array of features
            };
        },

        _applyFeaturesSelection: function(selectionFeatures, selectionMode, selectionAttributeName) {
            var selectionIds = selectionFeatures.map(function(feature) {
                return feature.properties._uniqueId;
            });

            if (selectionMode === 'SELECTION_NEW') {
                this.originAndDestinationGeoJsonPoints.features.forEach(function(feature) {
                    if (selectionIds.indexOf(feature.properties._uniqueId) > -1) {
                        feature.properties[selectionAttributeName] = true;
                    } else {
                        feature.properties[selectionAttributeName] = false;
                    }
                });
            } else if (selectionMode === 'SELECTION_ADD') {
                this.originAndDestinationGeoJsonPoints.features.forEach(function(feature) {
                    if (selectionIds.indexOf(feature.properties._uniqueId) > -1) {
                        feature.properties[selectionAttributeName] = true;
                    }
                });
            } else if (selectionMode === 'SELECTION_SUBTRACT') {
                this.originAndDestinationGeoJsonPoints.features.forEach(function(feature) {
                    if (selectionIds.indexOf(feature.properties._uniqueId) > -1) {
                        feature.properties[selectionAttributeName] = false;
                    }
                });
            } else {
                return;
            }

            this._resetCanvas();
        },

        _animateZoom: function(e) {
        // see: https://github.com/Leaflet/Leaflet.heat
            var scale = this._map.getZoomScale(e.zoom);
            var offset = this._map._getCenterOffset(e.center)._multiplyBy(-scale).subtract(this._map._getMapPanePos());

            if (L.DomUtil.setTransform) {
                this._customCanvases.forEach(function(canvas) {
                    L.DomUtil.setTransform(canvas, offset, scale);
                });
            } else {
                this._customCanvases.forEach(function(canvas) {
                    canvas.style[L.DomUtil.TRANSFORM] = L.DomUtil.getTranslateString(offset) + ' scale(' + scale + ')';
                });
            }
        },

        _resizeCanvas: function() {
        // update the canvas size
            var size = this._map.getSize();
            this._customCanvases.forEach(function(canvas) {
                canvas.width = size.x;
                canvas.height = size.y;
            });

            this._resetCanvas();
        },

        _resetCanvas: function() {
        // update the canvas position and redraw its content
            var topLeft = this._map.containerPointToLayerPoint([0, 0]);
            this._customCanvases.forEach(function(canvas) {
                L.DomUtil.setPosition(canvas, topLeft);
            });

            this._redrawCanvas();
        },

        _resetCanvasAndWrapGeoJsonCircleMarkers: function() {
            this._resetCanvas();
            // Leaflet will redraw a CircleMarker when its latLng is changed
            // sometimes they are drawn 2+ times if this occurs during many "move" events
            // so for now, only chang CircleMarker latlng after a single "moveend" event
            this._wrapGeoJsonCircleMarkers();
        },

        _redrawCanvas: function() {
        // draw canvas content (only the Bezier curves)
            if (this._map && this.originAndDestinationGeoJsonPoints) {
                this._clearCanvas();

                // loop over each of the "selected" features and re-draw the canvas paths
                this._drawSelectedCanvasPaths(false);

                if (this._animationFrameId) {
                    L.Util.cancelAnimFrame(this._animationFrameId);
                }

                if (
                    this.options.animationStarted &&
            this.originAndDestinationGeoJsonPoints.features.some(function(feature) {
                return feature.properties._isSelectedForPathDisplay;
            })
                ) {
                    // start animation loop if the layer is currently set for showing animations,
                    // and if there is at least 1 feature selected for displaying paths
                    this._animator();
                }
            }
        },

        _clearCanvas: function() {
            this._customCanvases.forEach(function(canvas) {
                canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
            });

            if (this._animationFrameId) {
                L.Util.cancelAnimFrame(this._animationFrameId);
            }
        },

        _drawSelectedCanvasPaths: function(animate) {
            var ctx = animate ?
                this._animationCanvasElement.getContext('2d') :
                this._canvasElement.getContext('2d');

            var originAndDestinationFieldIds = this.options.originAndDestinationFieldIds;

            this.originAndDestinationGeoJsonPoints.features.forEach(function(feature) {

                if (feature.properties._isSelectedForPathDisplay) {
                    var originXCoordinate = feature.properties[originAndDestinationFieldIds.originGeometry.x];
                    var originYCoordinate = feature.properties[originAndDestinationFieldIds.originGeometry.y];
                    var destinationXCoordinate = feature.properties[originAndDestinationFieldIds.destinationGeometry.x];
                    var destinationYCoordinate = feature.properties[originAndDestinationFieldIds.destinationGeometry.y];

                    // origin and destination points for drawing curved lines
                    // ensure that canvas features will be drawn beyond +/-180 longitude
                    var originLatLng = this._wrapAroundLatLng(L.latLng([originYCoordinate, originXCoordinate]));
                    var destinationLatLng = this._wrapAroundLatLng(L.latLng([destinationYCoordinate, destinationXCoordinate]));

                    // convert geometry to screen coordinates for canvas drawing
                    var screenOriginPoint = this._map.latLngToContainerPoint(originLatLng);
                    var screenDestinationPoint = this._map.latLngToContainerPoint(destinationLatLng);

                    // get the canvas symbol properties,
                    // and draw a curved canvas line
                    var symbol;
                    if (animate) {
                        symbol = this._getSymbolProperties(feature, this.options.animatedCanvasBezierStyle);
                        ctx.beginPath();
                        this._animateCanvasLineSymbol(ctx, symbol, screenOriginPoint, screenDestinationPoint);
                        ctx.stroke();
                        ctx.closePath();
                    } else {
                        symbol = this._getSymbolProperties(feature, this.options.canvasBezierStyle);
                        ctx.beginPath();
                        this._applyCanvasLineSymbol(ctx, symbol, screenOriginPoint, screenDestinationPoint);
                        ctx.stroke();
                        ctx.closePath();
                    }
                }
            }, this);
        },

        _getSymbolProperties: function(feature, canvasSymbolConfig) {
        // get the canvas symbol properties
            var symbol;
            var filteredSymbols;
            if (canvasSymbolConfig.type === 'simple') {
                symbol = canvasSymbolConfig.symbol;
            } else if (canvasSymbolConfig.type === 'uniqueValue') {
                filteredSymbols = canvasSymbolConfig.uniqueValueInfos.filter(function(info) {
                    return info.value === feature.properties[canvasSymbolConfig.field];
                });
                symbol = filteredSymbols[0].symbol;
            } else if (canvasSymbolConfig.type === 'classBreaks') {
                filteredSymbols = canvasSymbolConfig.classBreakInfos.filter(function(info) {
                    return (
                        info.classMinValue <= feature.properties[canvasSymbolConfig.field] &&
              info.classMaxValue >= feature.properties[canvasSymbolConfig.field]
                    );
                });
                if (filteredSymbols.length) {
                    symbol = filteredSymbols[0].symbol;
                } else {
                    symbol = canvasSymbolConfig.defaultSymbol;
                }
            }
            return symbol;
        },

        _applyCanvasLineSymbol: function(ctx, symbolObject, screenOriginPoint, screenDestinationPoint) {
            ctx.lineCap = symbolObject.lineCap;
            ctx.lineWidth = symbolObject.lineWidth;
            ctx.strokeStyle = symbolObject.strokeStyle;
            ctx.shadowBlur = symbolObject.shadowBlur;
            ctx.shadowColor = symbolObject.shadowColor;
            ctx.moveTo(screenOriginPoint.x, screenOriginPoint.y);
            ctx.bezierCurveTo(screenOriginPoint.x, screenDestinationPoint.y, screenDestinationPoint.x, screenDestinationPoint.y, screenDestinationPoint.x, screenDestinationPoint.y);
        },

        _animateCanvasLineSymbol: function(ctx, symbolObject, screenOriginPoint, screenDestinationPoint) {
            ctx.lineCap = symbolObject.lineCap;
            ctx.lineWidth = symbolObject.lineWidth;
            ctx.strokeStyle = symbolObject.strokeStyle;
            ctx.shadowBlur = symbolObject.shadowBlur;
            ctx.shadowColor = symbolObject.shadowColor;
            ctx.setLineDash([symbolObject.lineDashOffsetSize, (this._animationPropertiesStatic.resetOffset - symbolObject.lineDashOffsetSize)]);
            ctx.lineDashOffset = -this._animationPropertiesStatic.offset; // this makes the dot appear to move when the entire top canvas is redrawn
            ctx.moveTo(screenOriginPoint.x, screenOriginPoint.y);
            ctx.bezierCurveTo(screenOriginPoint.x, screenDestinationPoint.y, screenDestinationPoint.x, screenDestinationPoint.y, screenDestinationPoint.x, screenDestinationPoint.y);
        },

        _animator: function(time) {
            this._animationCanvasElement.getContext('2d')
                .clearRect(0, 0, this._animationCanvasElement.width, this._animationCanvasElement.height);

            this._drawSelectedCanvasPaths(true); // draw it again to give the appearance of a moving dot with a new lineDashOffset

            TWEEN.update(time);

            this._animationFrameId = L.Util.requestAnimFrame(this._animator, this);
        },

        _wrapGeoJsonCircleMarkers: function() {
        // ensure that the GeoJson point features,
        // which are drawn on the map as individual CircleMarker layers,
        // will be drawn beyond +/-180 longitude
            this.eachLayer(function(layer) {
                var wrappedLatLng = this._wrapAroundLatLng(layer.getLatLng());
                layer.setLatLng(wrappedLatLng);
            }, this);
        },

        _wrapAroundLatLng: function(latLng) {
            if (this._map && this.options.wrapAroundCanvas) {
                var wrappedLatLng = latLng.clone();
                var mapCenterLng = this._map.getCenter().lng;
                var wrapAroundDiff = mapCenterLng - wrappedLatLng.lng;
                if (wrapAroundDiff < -180 || wrapAroundDiff > 180) {
                    wrappedLatLng.lng += (Math.round(wrapAroundDiff / 360) * 360);
                }
                return wrappedLatLng;
            } else {
                return latLng;
            }
        }
    });

    return CanvasFlowmapLayer;

}, window));