<commit_before>(function () {
    'use strict';

    angular
        .module('dpMap')
        .factory('drawTool', drawToolFactory);

    drawToolFactory.$inject = ['$rootScope', 'L', 'DRAW_TOOL_CONFIG', 'suppress'];

    function drawToolFactory ($rootScope, L, DRAW_TOOL_CONFIG, suppress) {
        // holds all information about the state of the shape being created or edited
        const DEFAULTS = {
            isConsistent: true,
            type: null,
            layer: null,
            markers: [],
            markersPrev: [],
            markersMaxCount: DRAW_TOOL_CONFIG.MAX_MARKERS,
            area: 0,
            areaTxt: '',
            distance: 0,
            distanceTxt: ''
        };

        let currentShape = angular.copy(DEFAULTS);

        // holds all publicly available information about the last consistent state of the current shape
        const shapeInfo = {};
        updateShapeInfo(currentShape);  // initialise to initial current shape

        // holds all information of the leaflet.draw drawing and editing structures
        const drawTool = {
            drawingMode: DRAW_TOOL_CONFIG.DRAWING_MODE.NONE,
            drawnItems: null,
            drawShapeHandler: null,
            editShapeHandler: null,
            lastEvent: null,
            timeout: null
        };

        // these callback methods will be called on a finished polygon and on a change of drawing mode
        let _onFinishPolygon,
            _onDrawingMode;

        return {
            initialize,
            isEnabled,
            enable,
            disable,
            setPolygon,
            shape: shapeInfo
        };

        // initialise factory; initialise draw tool, register callback methods and register events
        function initialize (map, onFinish, onDrawingMode) {
            currentShape = angular.copy(DEFAULTS);

            initDrawTool(map);
            _onFinishPolygon = onFinish;    // callback method to call on finish draw/edit polygon
            _onDrawingMode = onDrawingMode; // callback method to call on change of drawing mode

            registerDrawEvents();
            registerMapEvents();
        }

        // triggered when a polygon has finished drawing or editing
        function onFinishPolygon () {
            if (angular.isFunction(_onFinishPolygon)) {
                // call any registered callback function, applyAsync because triggered by a leaflet event
                $rootScope.$applyAsync(() => _onFinishPolygon(shapeInfo));
            }
        }

        // triggered when a polygon has changed to a new valid state
        function onChangePolygon () {
            // add class to drawn polygon
            if (currentShape.layer && currentShape.layer._path) {
                angular.element(currentShape.layer._path).addClass('c-map__leaflet-drawn-polygon');
            }

            // update the publicly available shape info, applyAsync because triggered by a leaflet event
            $rootScope.$applyAsync(() => updateShapeInfo());
        }

        // triggered when the drawing mode has changed
        function onChangeDrawingMode () {
            if (angular.isFunction(_onDrawingMode)) {
                $rootScope.$applyAsync(() => {
                    // call any registered callback function, applyAsync because triggered by a leaflet event
                    // The exact drawingMode is an internal attribute of the factory
                    // The outside knowledge is just true or false; enabled or disabled
                    _onDrawingMode(drawTool.drawingMode);
                });
            }
        }

        // Construct a polygon from a array of coordinates
        function setPolygon (latLngs) {
            deletePolygon();    // delete any existing polygon
            if (latLngs.length > 0) {
                createPolygon(new L.Polygon(latLngs));
            }
            updateShape();
        }

        // Delete an existing polygon
        function deletePolygon () {
            if (currentShape.layer) {
                currentShape.layer.off('click', toggleEditModeOnShapeClick);
                drawTool.drawnItems.removeLayer(currentShape.layer);
                drawTool.map.fire(L.Draw.Event.DELETED, { layers: currentShape.layer });
                currentShape.layer = null;  // No current layer after delete polygon
            }
        }

        // Create a new polygon by registering the layer, show the layer and register a click handler to start/end edit
        function createPolygon (layer) {
            currentShape.layer = layer;
            drawTool.drawnItems.addLayer(layer);
            layer.on('click', toggleEditModeOnShapeClick);
        }

        // Called when a polygon is finished (end draw or end edit)
        function finishPolygon () {
            setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.NONE);
            onFinishPolygon();
        }

        // updates the drawing mode and trigger any callback method
        function setDrawingMode (drawingMode) {
            if (drawTool.drawingMode !== drawingMode) {
                drawTool.drawingMode = drawingMode;
                onChangeDrawingMode();
            }
        }

        // Initialisation of the draw tool, initialise drawing and register required objects in the drawTool object
        function initDrawTool (map) {
            L.drawLocal.format = DRAW_TOOL_CONFIG.format;

            drawTool.map = map;
            drawTool.drawnItems = new L.FeatureGroup();
            drawTool.drawShapeHandler = new L.Draw.Polygon(drawTool.map, DRAW_TOOL_CONFIG.draw.polygon);

            const editConfig = angular.copy(DRAW_TOOL_CONFIG.edit);
            editConfig.featureGroup = drawTool.drawnItems;
            const editToolbar = new L.EditToolbar(editConfig);

            drawTool.editShapeHandler = editToolbar.getModeHandlers(drawTool.map)[0].handler;

            drawTool.map.addLayer(drawTool.drawnItems);
        }

        // enforce the maximum markers limit and the non-intersection of line segments limit
        function enforceLimits () {
            if (!currentShape.isConsistent) {
                const markersPrev = angular.copy(currentShape.markersPrev);   // restore previous state

                deletePolygon();    // delete current polygon
                $rootScope.$applyAsync(() => {
                    setPolygon(markersPrev);    // restore previous polygon
                    updateShape();
                });
            } else {
                // check for auto close of shape in drawing mode
                autoClose();
            }
        }

        // Auto close polygon when in drawing mode and max markers has been reached
        function autoClose () {
            if (drawTool.drawingMode === DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW &&
                currentShape.markers.length === currentShape.markersMaxCount) {
                $rootScope.$applyAsync(() => {
                    disable();
                });
            }
        }

        // handle any leaflet.draw event
        function handleDrawEvent (eventName, e) {
            const handlers = {
                // Triggered when the user has chosen to draw a particular vector or marker
                DRAWSTART: () => setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW),

                // Triggered when a vertex is created on a polyline or polygon
                DRAWVERTEX: bindLastDrawnMarker,

                // Triggered when a new vector or marker has been created
                CREATED: () => {
                    createPolygon(e.layer);
                    finishPolygon();
                },

                // Triggered when the user starts edit mode by clicking the edit tool button
                EDITSTART: () => setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.EDIT),

                // Triggered when the user has finshed editing (edit mode) and saves edits
                EDITSTOP: finishPolygon,

                // Triggered when layers have been removed (and saved) from the FeatureGroup
                DELETED: () => currentShape.layer = null
            };

            const handler = handlers[eventName];
            if (handler) {
                handler(e);
            }

            drawTool.lastEvent = eventName;
        }

        // Every leaflet.draw event is catched, thereby taking care that the shape is always up-to-date,
        // that all limits are respected and that the last consistent state gets exposed by the drawing tool
        function registerDrawEvents () {
            Object.keys(L.Draw.Event).forEach(eventName => {
                drawTool.map.on(L.Draw.Event[eventName], function (e) {
                    console.log('draw tool factory - draw event', eventName);
                    if (eventName === 'DELETED') { // IE HACK
                        suppress.start();
                    }

                    handleDrawEvent(eventName, e);

                    updateShape();  // Update current shape and tooltip

                    $rootScope.$applyAsync(() => {
                        // Force Leaflet to enable TextSelection (tg-2728)
                        L.DomUtil.enableTextSelection();

                        // Execute this code after leaflet.draw has finished the event
                        enforceLimits();    // max vertices, auto close when max reached

                        if (currentShape.isConsistent) {
                            onChangePolygon();  // trigger change on new consistent state of the polygon
                        }
                    });
                });
            });
        }

        // register any non-leaflet.draw events
        function registerMapEvents () {
            // Click outside shape => delete shape
            drawTool.map.on('click', function () {
                if (suppress.isBusy()) {
                    return;
                }

                // In edit mode => disable()
                if (drawTool.drawingMode === DRAW_TOOL_CONFIG.DRAWING_MODE.EDIT) {
                    disable();
                } else if (drawTool.drawingMode !== DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW && currentShape.layer) {
                    // If not in Draw or EDIT mode and a polygon exists
                    // then the current polygon gets deleted
                    // Note: In draw mode the click on map adds a new marker
                    deletePolygon();
                    updateShape();
                    onFinishPolygon();
                    disable();
                }
            });
        }

        // Click on the shape toggles EDIT mode
        function toggleEditModeOnShapeClick (e) {
            L.DomEvent.stop(e);
            toggle();
        }

        // toggle between draw/edit (enable) and show mode (null)
        function toggle () {
            return isEnabled() ? disable() : enable();
        }

        function isEnabled () {
            // isEnabled => shape is being created or being edited
            return drawTool.drawingMode !== DRAW_TOOL_CONFIG.DRAWING_MODE.NONE;
        }

        // start draw or edit mode for current layer or start create mode for new shape
        // the outside only knows enabled or disabled, internally the mode is further defined in DRAW or EDIT
        function enable () {
            if (!isEnabled()) {
                if (currentShape.layer) {   // Shape exists, start edit
                    drawTool.editShapeHandler.enable();
                } else {    // Shape does not yet exist, start draw
                    drawTool.drawShapeHandler.enable();
                }
            }
        }

        // end of draw or edit mode => in create mode complete shape, in edit mode save shape
        function disable () {
            if (isEnabled()) {
                if (drawTool.drawingMode === DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW) {
                    if (currentShape.markers.length > 1) {
                        // Close the polyline between the first and last points
                        drawTool.drawShapeHandler.completeShape();
                    } else {
                        drawTool.drawShapeHandler.disable();
                    }
                } else {
                    drawTool.editShapeHandler.save();   // Save the layer geometries
                    drawTool.editShapeHandler.disable();
                }
                setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.NONE);
            }
        }

        // Shape method for shape.info
        // while drawing the polygon is not closed => distance is distance of the lines
        // When editing the polygon is closed => distance is surrounding
        // When only tow points => distance is line lenght
        function getDistance (latLngs, isClosed) {
            return latLngs.reduce((total, latlng, i) => {
                if (i > 0) {
                    const dist = latlng.distanceTo(latLngs[i - 1]);
                    total += dist;
                }
                return total;
            }, isClosed && latLngs.length > 2 ? latLngs[0].distanceTo(latLngs[latLngs.length - 1]) : 0);
        }

        // Update the internal information about the current shape
        function updateShape () {
            const DISTANCE_IN_KILOMETERS = 1000;    // Show in km starting from this #meters, else show in m

            let latLngs = [],
                area = 0,
                distance = 0,
                intersects = false;

            if (currentShape.layer) {
                latLngs = currentShape.layer.getLatLngs()[0];
                distance = getDistance(latLngs, true);
                area = L.GeometryUtil.geodesicArea(latLngs);
                intersects = currentShape.layer.intersects();
            } else if (drawTool.drawShapeHandler._markers && drawTool.drawShapeHandler._markers.length > 0) {
                latLngs = drawTool.drawShapeHandler._markers.map(m => m._latlng);
                area = drawTool.drawShapeHandler._area;
                distance = getDistance(latLngs, false);
            }

            currentShape.markersPrev = angular.copy(currentShape.markers);
            currentShape.markers = latLngs.map(({lat, lng}) => [lat, lng]);
            currentShape.area = area;
            currentShape.areaTxt = L.GeometryUtil.readableArea(
                area,
                DRAW_TOOL_CONFIG.draw.polygon.metric,
                DRAW_TOOL_CONFIG.draw.polygon.precision
            );
            currentShape.distance = distance;
            if (distance >= DISTANCE_IN_KILOMETERS) {
                currentShape.distanceTxt = L.GeometryUtil.formattedNumber(distance / DISTANCE_IN_KILOMETERS, 2) + ' km';
            } else {
                currentShape.distanceTxt = L.GeometryUtil.formattedNumber(distance, 1) + ' m';
            }
            currentShape.intersects = intersects;

            currentShape.isConsistent = !(
                currentShape.markers.length > currentShape.markersMaxCount || currentShape.intersects
            );

            if (currentShape.isConsistent) {
                L.drawLocal.edit.handlers.edit.tooltip.text = currentShape.areaTxt;
                L.drawLocal.edit.handlers.edit.tooltip.subtext = currentShape.distanceTxt;
                updateShapeInfo();  // update public shape info of new consistent state of the polygon
            }
        }

        // Updates the publicly available info for the current shape
        // Only to be called when shape is in a consistent state
        function updateShapeInfo () {
            // Copy a set of properties of the current shape into the shapeInfo object
            ['type', 'markers', 'markersMaxCount', 'area', 'areaTxt', 'distance', 'distanceTxt']
                .forEach(key => {
                    shapeInfo[key] = currentShape[key];
                });
        }

        // delete a marker in DRAW mode
        function deleteMarker (marker) {
            const drawShapeHandler = drawTool.drawShapeHandler;
            const markers = drawShapeHandler._markers;    // is always an array
            const index = markers.findIndex(m => m._leaflet_id === marker._leaflet_id);
            let nDelete = markers.length - index;   // Delete all from last to marker, inclusive
            while (nDelete-- > 0) {
                // Remove the last vertex from the polyline, removes polyline from map if only one point exists
                drawShapeHandler.deleteLastVertex();
            }
        }

        // returns the last marker that was added to the polygon (only called in draw mode)
        function getLastDrawnMarker () {
            const drawShapeHandler = drawTool.drawShapeHandler;
            return drawShapeHandler._markers[drawShapeHandler._markers.length - 1];
        }

        // bind last marker in DRAW mode to deleteMarker
        // bind first marker in DRAW mode to close polygon (by calling disable())
        function bindLastDrawnMarker () {
            const lastMarker = getLastDrawnMarker();
            const isFirstMarker = drawTool.drawShapeHandler._markers.length === 1;
            ['mousedown', 'click'].forEach(key => lastMarker.on(key, () => {
                if (drawTool.drawShapeHandler.enabled() && isFirstMarker) {
                    const isLineOrPolygon = currentShape.markers.length > 1;
                    disable();  // Includes auto close for any line or polygon
                    if (!isLineOrPolygon) {
                        // Reopen draw mode to place first marker somewhere else
                        enable();
                    }
                } else {
                    deleteMarker(lastMarker);
                }
            }));
        }
    }
})();
<commit_msg>refactored reset drawing from reducers to drawing tool component part 2 tg-3702<commit_after>(function () {
    'use strict';

    angular
        .module('dpMap')
        .factory('drawTool', drawToolFactory);

    drawToolFactory.$inject = ['$rootScope', 'L', 'DRAW_TOOL_CONFIG', 'suppress'];

    function drawToolFactory ($rootScope, L, DRAW_TOOL_CONFIG, suppress) {
        // holds all information about the state of the shape being created or edited
        const DEFAULTS = {
            isConsistent: true,
            type: null,
            layer: null,
            markers: [],
            markersPrev: [],
            markersMaxCount: DRAW_TOOL_CONFIG.MAX_MARKERS,
            area: 0,
            areaTxt: '',
            distance: 0,
            distanceTxt: ''
        };

        let currentShape = angular.copy(DEFAULTS);

        // holds all publicly available information about the last consistent state of the current shape
        const shapeInfo = {};
        updateShapeInfo(currentShape);  // initialise to initial current shape

        // holds all information of the leaflet.draw drawing and editing structures
        const drawTool = {
            drawingMode: DRAW_TOOL_CONFIG.DRAWING_MODE.NONE,
            drawnItems: null,
            drawShapeHandler: null,
            editShapeHandler: null,
            lastEvent: null,
            timeout: null
        };

        // these callback methods will be called on a finished polygon and on a change of drawing mode
        let _onFinishPolygon,
            _onDrawingMode;

        return {
            initialize,
            isEnabled,
            enable,
            disable,
            setPolygon,
            shape: shapeInfo
        };

        // initialise factory; initialise draw tool, register callback methods and register events
        function initialize (map, onFinish, onDrawingMode) {
            currentShape = angular.copy(DEFAULTS);

            initDrawTool(map);
            _onFinishPolygon = onFinish;    // callback method to call on finish draw/edit polygon
            _onDrawingMode = onDrawingMode; // callback method to call on change of drawing mode

            registerDrawEvents();
            registerMapEvents();
        }

        // triggered when a polygon has finished drawing or editing
        function onFinishPolygon () {
            if (angular.isFunction(_onFinishPolygon)) {
                // call any registered callback function, applyAsync because triggered by a leaflet event
                $rootScope.$applyAsync(() => _onFinishPolygon(shapeInfo));
            }
        }

        // triggered when a polygon has changed to a new valid state
        function onChangePolygon () {
            // add class to drawn polygon
            if (currentShape.layer && currentShape.layer._path) {
                angular.element(currentShape.layer._path).addClass('c-map__leaflet-drawn-polygon');
            }

            // update the publicly available shape info, applyAsync because triggered by a leaflet event
            $rootScope.$applyAsync(() => updateShapeInfo());
        }

        // triggered when the drawing mode has changed
        function onChangeDrawingMode () {
            if (angular.isFunction(_onDrawingMode)) {
                $rootScope.$applyAsync(() => {
                    // call any registered callback function, applyAsync because triggered by a leaflet event
                    // The exact drawingMode is an internal attribute of the factory
                    // The outside knowledge is just true or false; enabled or disabled
                    _onDrawingMode(drawTool.drawingMode);
                });
            }
        }

        // Construct a polygon from a array of coordinates
        function setPolygon (latLngs) {
            deletePolygon();    // delete any existing polygon
            if (latLngs.length > 0) {
                createPolygon(new L.Polygon(latLngs));
            }
            updateShape();
        }

        // Delete an existing polygon
        function deletePolygon () {
            if (currentShape.layer) {
                currentShape.layer.off('click', toggleEditModeOnShapeClick);
                drawTool.drawnItems.removeLayer(currentShape.layer);
                drawTool.map.fire(L.Draw.Event.DELETED, { layers: currentShape.layer });
                currentShape.layer = null;  // No current layer after delete polygon
            }
        }

        // Create a new polygon by registering the layer, show the layer and register a click handler to start/end edit
        function createPolygon (layer) {
            currentShape.layer = layer;
            drawTool.drawnItems.addLayer(layer);
            layer.on('click', toggleEditModeOnShapeClick);
        }

        // Called when a polygon is finished (end draw or end edit)
        function finishPolygon () {
            setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.NONE);
            onFinishPolygon();
        }

        // updates the drawing mode and trigger any callback method
        function setDrawingMode (drawingMode) {
            if (drawTool.drawingMode !== drawingMode) {
                drawTool.drawingMode = drawingMode;
                onChangeDrawingMode();
            }
        }

        // Initialisation of the draw tool, initialise drawing and register required objects in the drawTool object
        function initDrawTool (map) {
            L.drawLocal.format = DRAW_TOOL_CONFIG.format;

            drawTool.map = map;
            drawTool.drawnItems = new L.FeatureGroup();
            drawTool.drawShapeHandler = new L.Draw.Polygon(drawTool.map, DRAW_TOOL_CONFIG.draw.polygon);

            const editConfig = angular.copy(DRAW_TOOL_CONFIG.edit);
            editConfig.featureGroup = drawTool.drawnItems;
            const editToolbar = new L.EditToolbar(editConfig);

            drawTool.editShapeHandler = editToolbar.getModeHandlers(drawTool.map)[0].handler;

            drawTool.map.addLayer(drawTool.drawnItems);
        }

        // enforce the maximum markers limit and the non-intersection of line segments limit
        function enforceLimits () {
            if (!currentShape.isConsistent) {
                const markersPrev = angular.copy(currentShape.markersPrev);   // restore previous state

                deletePolygon();    // delete current polygon
                $rootScope.$applyAsync(() => {
                    setPolygon(markersPrev);    // restore previous polygon
                    updateShape();
                });
            } else {
                // check for auto close of shape in drawing mode
                autoClose();
            }
        }

        // Auto close polygon when in drawing mode and max markers has been reached
        function autoClose () {
            if (drawTool.drawingMode === DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW &&
                currentShape.markers.length === currentShape.markersMaxCount) {
                $rootScope.$applyAsync(() => {
                    disable();
                });
            }
        }

        // handle any leaflet.draw event
        function handleDrawEvent (eventName, e) {
            const handlers = {
                // Triggered when the user has chosen to draw a particular vector or marker
                DRAWSTART: () => setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW),

                // Triggered when a vertex is created on a polyline or polygon
                DRAWVERTEX: bindLastDrawnMarker,

                // Triggered when a new vector or marker has been created
                CREATED: () => {
                    createPolygon(e.layer);
                    finishPolygon();
                },

                // Triggered when the user starts edit mode by clicking the edit tool button
                EDITSTART: () => setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.EDIT),

                // Triggered when the user has finshed editing (edit mode) and saves edits
                EDITSTOP: finishPolygon,

                // Triggered when layers have been removed (and saved) from the FeatureGroup
                DELETED: () => currentShape.layer = null
            };

            const handler = handlers[eventName];
            if (handler) {
                handler(e);
            }

            drawTool.lastEvent = eventName;
        }

        // Every leaflet.draw event is catched, thereby taking care that the shape is always up-to-date,
        // that all limits are respected and that the last consistent state gets exposed by the drawing tool
        function registerDrawEvents () {
            Object.keys(L.Draw.Event).forEach(eventName => {
                drawTool.map.on(L.Draw.Event[eventName], function (e) {
                    if (eventName === 'DELETED') { // IE HACK
                        suppress.start();
                    }

                    handleDrawEvent(eventName, e);

                    updateShape();  // Update current shape and tooltip

                    $rootScope.$applyAsync(() => {
                        // Force Leaflet to enable TextSelection (tg-2728)
                        L.DomUtil.enableTextSelection();

                        // Execute this code after leaflet.draw has finished the event
                        enforceLimits();    // max vertices, auto close when max reached

                        if (currentShape.isConsistent) {
                            onChangePolygon();  // trigger change on new consistent state of the polygon
                        }
                    });
                });
            });
        }

        // register any non-leaflet.draw events
        function registerMapEvents () {
            // Click outside shape => delete shape
            drawTool.map.on('click', function () {
                if (suppress.isBusy()) {
                    return;
                }

                // In edit mode => disable()
                if (drawTool.drawingMode === DRAW_TOOL_CONFIG.DRAWING_MODE.EDIT) {
                    disable();
                } else if (drawTool.drawingMode !== DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW && currentShape.layer) {
                    // If not in Draw or EDIT mode and a polygon exists
                    // then the current polygon gets deleted
                    // Note: In draw mode the click on map adds a new marker
                    deletePolygon();
                    updateShape();
                    onFinishPolygon();
                    disable();
                }
            });
        }

        // Click on the shape toggles EDIT mode
        function toggleEditModeOnShapeClick (e) {
            L.DomEvent.stop(e);
            toggle();
        }

        // toggle between draw/edit (enable) and show mode (null)
        function toggle () {
            return isEnabled() ? disable() : enable();
        }

        function isEnabled () {
            // isEnabled => shape is being created or being edited
            return drawTool.drawingMode !== DRAW_TOOL_CONFIG.DRAWING_MODE.NONE;
        }

        // start draw or edit mode for current layer or start create mode for new shape
        // the outside only knows enabled or disabled, internally the mode is further defined in DRAW or EDIT
        function enable () {
            if (!isEnabled()) {
                if (currentShape.layer) {   // Shape exists, start edit
                    drawTool.editShapeHandler.enable();
                } else {    // Shape does not yet exist, start draw
                    drawTool.drawShapeHandler.enable();
                }
            }
        }

        // end of draw or edit mode => in create mode complete shape, in edit mode save shape
        function disable () {
            if (isEnabled()) {
                if (drawTool.drawingMode === DRAW_TOOL_CONFIG.DRAWING_MODE.DRAW) {
                    if (currentShape.markers.length > 1) {
                        // Close the polyline between the first and last points
                        drawTool.drawShapeHandler.completeShape();
                    } else {
                        drawTool.drawShapeHandler.disable();
                    }
                } else {
                    drawTool.editShapeHandler.save();   // Save the layer geometries
                    drawTool.editShapeHandler.disable();
                }
                setDrawingMode(DRAW_TOOL_CONFIG.DRAWING_MODE.NONE);
            }
        }

        // Shape method for shape.info
        // while drawing the polygon is not closed => distance is distance of the lines
        // When editing the polygon is closed => distance is surrounding
        // When only tow points => distance is line lenght
        function getDistance (latLngs, isClosed) {
            return latLngs.reduce((total, latlng, i) => {
                if (i > 0) {
                    const dist = latlng.distanceTo(latLngs[i - 1]);
                    total += dist;
                }
                return total;
            }, isClosed && latLngs.length > 2 ? latLngs[0].distanceTo(latLngs[latLngs.length - 1]) : 0);
        }

        // Update the internal information about the current shape
        function updateShape () {
            const DISTANCE_IN_KILOMETERS = 1000;    // Show in km starting from this #meters, else show in m

            let latLngs = [],
                area = 0,
                distance = 0,
                intersects = false;

            if (currentShape.layer) {
                latLngs = currentShape.layer.getLatLngs()[0];
                distance = getDistance(latLngs, true);
                area = L.GeometryUtil.geodesicArea(latLngs);
                intersects = currentShape.layer.intersects();
            } else if (drawTool.drawShapeHandler._markers && drawTool.drawShapeHandler._markers.length > 0) {
                latLngs = drawTool.drawShapeHandler._markers.map(m => m._latlng);
                area = drawTool.drawShapeHandler._area;
                distance = getDistance(latLngs, false);
            }

            currentShape.markersPrev = angular.copy(currentShape.markers);
            currentShape.markers = latLngs.map(({lat, lng}) => [lat, lng]);
            currentShape.area = area;
            currentShape.areaTxt = L.GeometryUtil.readableArea(
                area,
                DRAW_TOOL_CONFIG.draw.polygon.metric,
                DRAW_TOOL_CONFIG.draw.polygon.precision
            );
            currentShape.distance = distance;
            if (distance >= DISTANCE_IN_KILOMETERS) {
                currentShape.distanceTxt = L.GeometryUtil.formattedNumber(distance / DISTANCE_IN_KILOMETERS, 2) + ' km';
            } else {
                currentShape.distanceTxt = L.GeometryUtil.formattedNumber(distance, 1) + ' m';
            }
            currentShape.intersects = intersects;

            currentShape.isConsistent = !(
                currentShape.markers.length > currentShape.markersMaxCount || currentShape.intersects
            );

            if (currentShape.isConsistent) {
                L.drawLocal.edit.handlers.edit.tooltip.text = currentShape.areaTxt;
                L.drawLocal.edit.handlers.edit.tooltip.subtext = currentShape.distanceTxt;
                updateShapeInfo();  // update public shape info of new consistent state of the polygon
            }
        }

        // Updates the publicly available info for the current shape
        // Only to be called when shape is in a consistent state
        function updateShapeInfo () {
            // Copy a set of properties of the current shape into the shapeInfo object
            ['type', 'markers', 'markersMaxCount', 'area', 'areaTxt', 'distance', 'distanceTxt']
                .forEach(key => {
                    shapeInfo[key] = currentShape[key];
                });
        }

        // delete a marker in DRAW mode
        function deleteMarker (marker) {
            const drawShapeHandler = drawTool.drawShapeHandler;
            const markers = drawShapeHandler._markers;    // is always an array
            const index = markers.findIndex(m => m._leaflet_id === marker._leaflet_id);
            let nDelete = markers.length - index;   // Delete all from last to marker, inclusive
            while (nDelete-- > 0) {
                // Remove the last vertex from the polyline, removes polyline from map if only one point exists
                drawShapeHandler.deleteLastVertex();
            }
        }

        // returns the last marker that was added to the polygon (only called in draw mode)
        function getLastDrawnMarker () {
            const drawShapeHandler = drawTool.drawShapeHandler;
            return drawShapeHandler._markers[drawShapeHandler._markers.length - 1];
        }

        // bind last marker in DRAW mode to deleteMarker
        // bind first marker in DRAW mode to close polygon (by calling disable())
        function bindLastDrawnMarker () {
            const lastMarker = getLastDrawnMarker();
            const isFirstMarker = drawTool.drawShapeHandler._markers.length === 1;
            ['mousedown', 'click'].forEach(key => lastMarker.on(key, () => {
                if (drawTool.drawShapeHandler.enabled() && isFirstMarker) {
                    const isLineOrPolygon = currentShape.markers.length > 1;
                    disable();  // Includes auto close for any line or polygon
                    if (!isLineOrPolygon) {
                        // Reopen draw mode to place first marker somewhere else
                        enable();
                    }
                } else {
                    deleteMarker(lastMarker);
                }
            }));
        }
    }
})();
<|endoftext|>


<commit_before>;(function() {
  var lookupPage = function(pageName) {
    var def = $.Deferred()
    var findPage = function(pages) {
      //console.log(pages)
      var page = pages.filter(function(page) {
        return page.url.match('/'+pageName+'.html')
      })[0]
      def.resolve(page)
    }

    $.get('http://127.0.0.1:9999/json').then(findPage)

    return def.promise()
  }

  var load = function(pageName, script) {
    lookupPage(pageName).then(function(page) {
      //console.log(page)
      loadScriptOnPage(page, script)
    })
  }

  var loadScriptOnPage = function(page, script) {
    evaluateOnPage(page, loadScript(script))
  }

  var loadScript = function(script) {
    return 'loadScript("' + script + '")'
  }

  var message = function(pageName, message, payload) {
    lookupPage(pageName).then(function(page) {
      //console.log(page)
      messagePage(page, message, payload)
    })
  }

  var messagePage = function(page, message, payload) {
    evaluateOnPage(page, invokeHandler(message, payload))
  }

  var invokeHandler = function(message, payload) {
    var args = ''
    if (payload) {
      args = JSON.stringify(payload)
    }
    return 'handlers["' + message + '"](' + args + ')'
  }

  var evaluateOnPage = function(page, expression) {
    debugMessage(page, 'Runtime.evaluate', {expression: expression})
  }

  var mailboxes = {}
  var Mailbox = function(pageid) {
    if (mailboxes[pageid]) return mailboxes[pageid]

    this.pageid = pageid
    this.messages = []
    this.pending = 0
    mailboxes[pageid] = this
    return this
  }

  Mailbox.prototype.flush = function() {
    while (this.messages.length > 0) {
      this.pending++
      this.ws.send(this.messages.shift())
    }
  }
  Mailbox.prototype.send = function(method, params) {
    this.messages.push(JSON.stringify({
      id: 0,
      method: method,
      params: params
    }))
    if (this.ws && this.ws.readyState == 1) {
      this.flush()
    }
  }
  Mailbox.prototype.receive = function(message) {
    if (message && message.data) {
      //console.log('data', JSON.parse(message.data).result)
    } else {
      //console.log('message', message)
    }
    this.pending--
    if (this.pending < 1 && this.messages.length < 1) {
      this.close()
    }
  }
  Mailbox.prototype.close = function() {
    this.ws.close()
    this.ws = null
  }

  var debugMessage = function(page, method, params) {
    if (!page) return

    var mb = new Mailbox(page.id)
    mb.send(method, params)

    if (mb.ws) return

    ws = mb.ws = new WebSocket('ws://127.0.0.1:9999/devtools/page/' + page.id);
    ws.onmessage = function(message) {
      //console.log('message')
      mb.receive(message)
    }
    ws.onerror = function(error) {
      console.error('error', error);
      mb.close()
    }
    ws.onopen = function() {
      //console.log('open')
      mb.flush()
    }
    ws.onclose = function() {
      //console.log('close')
      mb.ws = null
    }
  }
  var close = function() {
    mailboxes.forEach(function(mb) {
      if (mb.ws) {
        mb.close()
      }
    })
  }

  window.atlasMessage = window.atlasMessage || {}
  atlasMessage.load = load
  atlasMessage.message = message
  atlasMessage.close = close
})()
<commit_msg>typo and async lookup issues<commit_after>;(function() {
  var lookupPage = function(pageName) {
    var def = $.Deferred()
    var findPage = function(pages) {
      //console.log(pages)
      var page = pages.filter(function(page) {
        return page.url.match('/'+pageName+'.html')
      })[0]
      def.resolve(page)
    }

    $.get('http://127.0.0.1:9999/json').then(findPage)

    return def.promise()
  }

  var load = function(pageName, script) {
    evaluateOnPage(pageName, loadScript(script))
  }

  var loadScript = function(script) {
    return 'loadScript("' + script + '")'
  }

  var message = function(pageName, message, payload) {
    evaluateOnPage(pageName, invokeHandler(message, payload))
  }

  var invokeHandler = function(message, payload) {
    var args = ''
    if (payload) {
      args = JSON.stringify(payload)
    }
    return 'handlers["' + message + '"](' + args + ')'
  }

  var evaluateOnPage = function(pageName, expression) {
    debugMessage(pageName, 'Runtime.evaluate', {expression: expression})
  }

  var mailboxes = {}
  var Mailbox = function(pageName) {
    if (mailboxes[pageName]) return mailboxes[pageName]

    this.pageName = pageName
    this.messages = []
    this.pending = 0
    this.lookupPending = false
    mailboxes[pageName] = this
    return this
  }

  Mailbox.prototype.flush = function() {
    //console.log('flush')
    while (this.messages.length > 0) {
      this.pending++
      var message = this.messages.shift()
      //console.log('sending', message)
      this.ws.send(message)
    }
  }
  Mailbox.prototype.send = function(method, params) {
    //console.log('mb send', method,params)
    this.messages.push(JSON.stringify({
      id: 0,
      method: method,
      params: params
    }))
    if (this.ws) {
      if (this.ws.readyState == 1) {
        this.flush()
      }
    } else if (!this.lookupPending) {
      this.connect()
    }
  }
  Mailbox.prototype.receive = function(message) {
    if (message && message.data) {
      var data = JSON.parse(message.data)
      if (data.wasThrown) {
        //console.log('data:error', data.result.description)
      } else {
        //console.log('data', data.result.result)
      }
    } else {
      //console.log('message', message)
    }
    this.pending--
    if (this.pending < 1 && this.messages.length < 1) {
      this.close()
    }
  }
  Mailbox.prototype.connect = function() {
    var mb = this
    //console.log('connect', mb.ws, mb.lookupPending)
    if (mb.ws || mb.lookupPending) return

    mb.lookupPending = true
    lookupPage(this.pageName).then(function(page) {
      mb.lookupPending = false
      mb.openSocket(page)
    })
  }
  Mailbox.prototype.openSocket = function(page) {
    if (!page) return

    var mb = this
    ws = mb.ws = new WebSocket('ws://127.0.0.1:9999/devtools/page/' + page.id);
    ws.onmessage = function(message) {
      //console.log('message')
      mb.receive(message)
    }
    ws.onerror = function(error) {
      console.error('error', error);
      mb.close()
    }
    ws.onopen = function() {
      //console.log('open')
      mb.flush()
    }
    ws.onclose = function() {
      //console.log('close')
      mb.ws = null
    }
  }
  Mailbox.prototype.close = function() {
    this.ws.close()
    this.ws = null
  }

  var debugMessage = function(pageName, method, params) {
    if (!pageName) return

    var mb = new Mailbox(pageName)
    mb.send(method, params)
  }
  var close = function() {
    mailboxes.forEach(function(mb) {
      if (mb.ws) {
        mb.close()
      }
    })
  }

  window.atlasMessage = window.atlasMessage || {}
  atlasMessage.load = load
  atlasMessage.message = message
  atlasMessage.close = close
})()
<|endoftext|>


<commit_before>/*
 * This script can be added to your Canvas custom JavaScript file
 * It will add a "People" link to the course search page
 */
(function() {
  var locationRegex = new RegExp('^/accounts/[0-9]+(/?$|/courses)');
  console.log(locationRegex);
  console.log(window.location.pathname);
  if (!locationRegex.test(window.location.pathname)) {
    return;
  }
  console.log('Running people');
  var courseIdRegex = new RegExp('^course_([0-9]+)$');
  var items = document.querySelectorAll('div#content > ul.courses > li');
  if (items) {
    for (var i = 0; i < items.length; i++) {
      var match = courseIdRegex.exec(items[i].id);
      if (match) {
        var courseId = match[1];
        var span = items[i].querySelector('div.info > span.links');
        if (span) {
          var spacer = document.createTextNode(' | ');
          span.appendChild(spacer);
          var link = document.createElement('a');
          link.href = '/courses/' + courseId + '/users';
          link.textContent = 'People';
          span.appendChild(link);
        }
      }
    }
  }
})();
<commit_msg>Removed logging<commit_after>/*
 * This script can be added to your Canvas custom JavaScript file
 * It will add a "People" link to the course search page
 */
(function() {
  var locationRegex = new RegExp('^/accounts/[0-9]+(/?$|/courses)');
  if (!locationRegex.test(window.location.pathname)) {
    return;
  }
  var courseIdRegex = new RegExp('^course_([0-9]+)$');
  var items = document.querySelectorAll('div#content > ul.courses > li');
  if (items) {
    for (var i = 0; i < items.length; i++) {
      var match = courseIdRegex.exec(items[i].id);
      if (match) {
        var courseId = match[1];
        var span = items[i].querySelector('div.info > span.links');
        if (span) {
          var spacer = document.createTextNode(' | ');
          span.appendChild(spacer);
          var link = document.createElement('a');
          link.href = '/courses/' + courseId + '/users';
          link.textContent = 'People';
          span.appendChild(link);
        }
      }
    }
  }
})();
<|endoftext|>


<commit_before>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
var SoundEvent = require('../SoundEvent');
var SoundValueEvent = require('../SoundValueEvent');
/*!
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
var WebAudioSound = new Class({
    Extends: BaseSound,
    /**
     * @class Phaser.Sound.WebAudioSound
     * @constructor
     * @param {Phaser.Sound.WebAudioSoundManager} manager - Reference to the current sound manager instance.
     * @param {string} key - Asset key for the sound.
     * @param {ISoundConfig} [config={}] - An optional config object containing default sound settings.
     */
    initialize: function WebAudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * Audio buffer containing decoded data of the audio asset to be played.
         *
         * @private
         * @property {AudioBuffer} audioBuffer
         */
        this.audioBuffer = manager.game.cache.audio.get(key);
        if (!this.audioBuffer) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * A reference to an audio source node used for playing back audio from
         * audio data stored in an Phaser.Sound.WebAudioSound#audioBuffer.
         *
         * @private
         * @property {AudioBufferSourceNode} source
         * @default null
         */
        this.source = null;
        /**
         * A reference to a second audio source used for gapless looped playback.
         *
         * @private
         * @property {AudioBufferSourceNode} loopSource
         * @default null
         */
        this.loopSource = null;
        /**
         * Gain node responsible for controlling this sound's muting.
         *
         * @private
         * @property {GainNode} muteNode
         */
        this.muteNode = manager.context.createGain();
        /**
         * Gain node responsible for controlling this sound's volume.
         *
         * @private
         * @property {GainNode} volumeNode
         */
        this.volumeNode = manager.context.createGain();
        /**
         * The time at which the sound should have started playback from the beginning.
         * Based on BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} playTime
         * @default 0
         */
        this.playTime = 0;
        /**
         * The time at which the sound source should have actually started playback.
         * Based on BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} startTime
         * @default 0
         */
        this.startTime = 0;
        /**
         * The time at which the sound loop source should actually start playback.
         * Based on BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} loopTime
         * @default 0
         */
        this.loopTime = 0;
        /**
         * An array where we keep track of all rate updates during playback.
         *
         * @private
         * @property {{ time: number, rate: number }[]} rateUpdates
         * @default []
         */
        this.rateUpdates = [];
        /**
         * Used for keeping track when sound source playback has ended
         * so its state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasEnded
         * @default false
         */
        this.hasEnded = false;
        /**
         * Used for keeping track when sound source has looped
         * so its state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasLooped
         * @default false
         */
        this.hasLooped = false;
        this.muteNode.connect(this.volumeNode);
        this.volumeNode.connect(manager.destination);
        this.duration = this.audioBuffer.duration;
        this.totalDuration = this.audioBuffer.duration;
        BaseSound.call(this, manager, key, config);
    },
    /**
     * Play this sound, or a marked section of it.
     * It always plays the sound from the start. If you want to start playback from a specific time
     * you can set 'seek' setting of the config object, provided to this call, to that value.
     *
     * @method Phaser.Sound.WebAudioSound#play
     * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound.
     * @param {ISoundConfig} [config] - Optional sound config object to be applied to this marker or entire sound if no marker name is provided. It gets memorized for future plays of current section of the sound.
     * @returns {Phaser.Sound.WebAudioSound | null} This sound instance or 'null' if an error occurred.
     */
    play: function (markerName, config) {
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return null;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.createAndStartBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_PLAY'));
        return this;
    },
    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.WebAudioSound#pause
     * @returns {boolean} Whether the sound was paused successfully.
     */
    pause: function () {
        if (this.manager.context.currentTime < this.startTime) {
            return false;
        }
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time
        this.stopAndRemoveBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_PAUSE'));
        return true;
    },
    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.WebAudioSound#resume
     * @returns {boolean} Whether the sound was resumed successfully.
     */
    resume: function () {
        if (this.manager.context.currentTime < this.startTime) {
            return false;
        }
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.createAndStartBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_RESUME'));
        return true;
    },
    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.WebAudioSound#stop
     * @returns {boolean} Whether the sound was stopped successfully.
     */
    stop: function () {
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_STOP'));
        return true;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#createAndStartBufferSource
     */
    createAndStartBufferSource: function () {
        var seek = this.currentConfig.seek;
        var delay = this.currentConfig.delay;
        var when = this.manager.context.currentTime + delay;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        var duration = this.duration - seek;
        this.playTime = when - seek;
        this.startTime = when;
        this.source = this.createBufferSource();
        this.applyConfig();
        this.source.start(Math.max(0, when), Math.max(0, offset), Math.max(0, duration));
        this.resetConfig();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#createAndStartLoopBufferSource
     */
    createAndStartLoopBufferSource: function () {
        var when = this.getLoopTime();
        var offset = this.currentMarker ? this.currentMarker.start : 0;
        var duration = this.duration;
        this.loopTime = when;
        this.loopSource = this.createBufferSource();
        this.loopSource.playbackRate.setValueAtTime(this.totalRate, 0);
        this.loopSource.start(Math.max(0, when), Math.max(0, offset), Math.max(0, duration));
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#createBufferSource
     */
    createBufferSource: function () {
        var _this = this;
        var source = this.manager.context.createBufferSource();
        source.buffer = this.audioBuffer;
        source.connect(this.muteNode);
        source.onended = function (ev) {
            if (ev.target === _this.source) {
                // sound ended
                if (_this.currentConfig.loop) {
                    _this.hasLooped = true;
                }
                else {
                    _this.hasEnded = true;
                }
            }
            // else was stopped
        };
        return source;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#stopAndRemoveBufferSource
     */
    stopAndRemoveBufferSource: function () {
        if (this.source) {
            this.source.stop();
            this.source.disconnect();
            this.source = null;
        }
        this.playTime = 0;
        this.startTime = 0;
        this.stopAndRemoveLoopBufferSource();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#stopAndRemoveLoopBufferSource
     */
    stopAndRemoveLoopBufferSource: function () {
        if (this.loopSource) {
            this.loopSource.stop();
            this.loopSource.disconnect();
            this.loopSource = null;
        }
        this.loopTime = 0;
    },
    /**
     * @protected
     * @method Phaser.Sound.WebAudioSound#applyConfig
     */
    applyConfig: function () {
        this.rateUpdates.length = 0;
        this.rateUpdates.push({
            time: 0,
            rate: 1
        });
        BaseSound.prototype.applyConfig.call(this);
    },
    /**
     * Update method called automatically by sound manager on every game step.
     *
     * @protected
     * @method Phaser.Sound.WebAudioSound#update
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (this.hasEnded) {
            this.hasEnded = false;
            BaseSound.prototype.stop.call(this);
            this.stopAndRemoveBufferSource();
            this.events.dispatch(new SoundEvent(this, 'SOUND_ENDED'));
        }
        else if (this.hasLooped) {
            this.hasLooped = false;
            this.source = this.loopSource;
            this.loopSource = null;
            this.playTime = this.startTime = this.loopTime;
            this.rateUpdates.length = 0;
            this.rateUpdates.push({
                time: 0,
                rate: this.totalRate
            });
            this.createAndStartLoopBufferSource();
            this.events.dispatch(new SoundEvent(this, 'SOUND_LOOP'));
        }
    },
    /**
     * Calls Phaser.Sound.BaseSound#destroy method
     * and cleans up all Web Audio API related stuff.
     *
     * @method Phaser.Sound.WebAudioSound#destroy
     */
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.audioBuffer = null;
        this.stopAndRemoveBufferSource();
        this.muteNode.disconnect();
        this.muteNode = null;
        this.volumeNode.disconnect();
        this.volumeNode = null;
        this.rateUpdates = null;
    },
    /**
     * Method used internally to calculate total playback rate of the sound.
     *
     * @protected
     * @method Phaser.Sound.WebAudioSound#setRate
     */
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        var now = this.manager.context.currentTime;
        if (this.source) {
            this.source.playbackRate.setValueAtTime(this.totalRate, now);
        }
        if (this.isPlaying) {
            this.rateUpdates.push({
                time: Math.max(this.startTime, now) - this.playTime,
                rate: this.totalRate
            });
            if (this.loopSource) {
                this.stopAndRemoveLoopBufferSource();
                this.createAndStartLoopBufferSource();
            }
        }
    },
    /**
     * @private
     */
    getCurrentTime: function () {
        var currentTime = 0;
        for (var i = 0; i < this.rateUpdates.length; i++) {
            var nextTime = void 0;
            if (i < this.rateUpdates.length - 1) {
                nextTime = this.rateUpdates[i + 1].time;
            }
            else {
                nextTime = this.manager.context.currentTime - this.playTime;
            }
            currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        return currentTime;
    },
    /**
     * @private
     */
    getLoopTime: function () {
        var lastRateUpdateCurrentTime = 0;
        for (var i = 0; i < this.rateUpdates.length - 1; i++) {
            lastRateUpdateCurrentTime +=
                (this.rateUpdates[i + 1].time - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        var lastRateUpdate = this.rateUpdates[this.rateUpdates.length - 1];
        return this.playTime + lastRateUpdate.time
            + (this.duration - lastRateUpdateCurrentTime) / lastRateUpdate.rate;
    }
});
/**
 * Mute setting.
 * @property {boolean} mute
 */
Object.defineProperty(WebAudioSound.prototype, 'mute', {
    get: function () {
        return this.muteNode.gain.value === 0;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0);
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_MUTE', value));
    }
});
/**
 * Volume setting.
 * @property {number} volume
 */
Object.defineProperty(WebAudioSound.prototype, 'volume', {
    get: function () {
        return this.volumeNode.gain.value;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        this.volumeNode.gain.setValueAtTime(value, 0);
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_VOLUME', value));
    }
});
/**
 * Playback rate.
 * @property {number} rate
 */
Object.defineProperty(WebAudioSound.prototype, 'rate', {
    get: function () {
        return this.currentConfig.rate;
    },
    set: function (value) {
        this.currentConfig.rate = value;
        this.setRate();
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_RATE', value));
    }
});
/**
 * Detuning of sound.
 * @property {number} detune
 */
Object.defineProperty(WebAudioSound.prototype, 'detune', {
    get: function () {
        return this.currentConfig.detune;
    },
    set: function (value) {
        this.currentConfig.detune = value;
        this.setRate();
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_DETUNE', value));
    }
});
/**
 * Current position of playing sound.
 * @property {number} seek
 */
Object.defineProperty(WebAudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            if (this.manager.context.currentTime < this.startTime) {
                return this.startTime - this.playTime;
            }
            return this.getCurrentTime();
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.manager.context.currentTime < this.startTime) {
            return;
        }
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            this.currentConfig.seek = value;
            this.events.dispatch(new SoundValueEvent(this, 'SOUND_SEEK', value));
        }
        if (this.isPlaying) {
            this.stopAndRemoveBufferSource();
            this.createAndStartBufferSource();
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @property {boolean} loop
 */
Object.defineProperty(WebAudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        this.currentConfig.loop = value;
        if (this.isPlaying) {
            this.stopAndRemoveLoopBufferSource();
            if (value) {
                this.createAndStartLoopBufferSource();
            }
        }
    }
});
module.exports = WebAudioSound;
<commit_msg>Updated getCurrentTime method docs<commit_after>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
var SoundEvent = require('../SoundEvent');
var SoundValueEvent = require('../SoundValueEvent');
/*!
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
var WebAudioSound = new Class({
    Extends: BaseSound,
    /**
     * @class Phaser.Sound.WebAudioSound
     * @constructor
     * @param {Phaser.Sound.WebAudioSoundManager} manager - Reference to the current sound manager instance.
     * @param {string} key - Asset key for the sound.
     * @param {ISoundConfig} [config={}] - An optional config object containing default sound settings.
     */
    initialize: function WebAudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * Audio buffer containing decoded data of the audio asset to be played.
         *
         * @private
         * @property {AudioBuffer} audioBuffer
         */
        this.audioBuffer = manager.game.cache.audio.get(key);
        if (!this.audioBuffer) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * A reference to an audio source node used for playing back audio from
         * audio data stored in an Phaser.Sound.WebAudioSound#audioBuffer.
         *
         * @private
         * @property {AudioBufferSourceNode} source
         * @default null
         */
        this.source = null;
        /**
         * A reference to a second audio source used for gapless looped playback.
         *
         * @private
         * @property {AudioBufferSourceNode} loopSource
         * @default null
         */
        this.loopSource = null;
        /**
         * Gain node responsible for controlling this sound's muting.
         *
         * @private
         * @property {GainNode} muteNode
         */
        this.muteNode = manager.context.createGain();
        /**
         * Gain node responsible for controlling this sound's volume.
         *
         * @private
         * @property {GainNode} volumeNode
         */
        this.volumeNode = manager.context.createGain();
        /**
         * The time at which the sound should have started playback from the beginning.
         * Based on BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} playTime
         * @default 0
         */
        this.playTime = 0;
        /**
         * The time at which the sound source should have actually started playback.
         * Based on BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} startTime
         * @default 0
         */
        this.startTime = 0;
        /**
         * The time at which the sound loop source should actually start playback.
         * Based on BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} loopTime
         * @default 0
         */
        this.loopTime = 0;
        /**
         * An array where we keep track of all rate updates during playback.
         *
         * @private
         * @property {{ time: number, rate: number }[]} rateUpdates
         * @default []
         */
        this.rateUpdates = [];
        /**
         * Used for keeping track when sound source playback has ended
         * so its state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasEnded
         * @default false
         */
        this.hasEnded = false;
        /**
         * Used for keeping track when sound source has looped
         * so its state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasLooped
         * @default false
         */
        this.hasLooped = false;
        this.muteNode.connect(this.volumeNode);
        this.volumeNode.connect(manager.destination);
        this.duration = this.audioBuffer.duration;
        this.totalDuration = this.audioBuffer.duration;
        BaseSound.call(this, manager, key, config);
    },
    /**
     * Play this sound, or a marked section of it.
     * It always plays the sound from the start. If you want to start playback from a specific time
     * you can set 'seek' setting of the config object, provided to this call, to that value.
     *
     * @method Phaser.Sound.WebAudioSound#play
     * @param {string} [markerName=''] - If you want to play a marker then provide the marker name here, otherwise omit it to play the full sound.
     * @param {ISoundConfig} [config] - Optional sound config object to be applied to this marker or entire sound if no marker name is provided. It gets memorized for future plays of current section of the sound.
     * @returns {Phaser.Sound.WebAudioSound | null} This sound instance or 'null' if an error occurred.
     */
    play: function (markerName, config) {
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return null;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.createAndStartBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_PLAY'));
        return this;
    },
    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.WebAudioSound#pause
     * @returns {boolean} Whether the sound was paused successfully.
     */
    pause: function () {
        if (this.manager.context.currentTime < this.startTime) {
            return false;
        }
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time
        this.stopAndRemoveBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_PAUSE'));
        return true;
    },
    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.WebAudioSound#resume
     * @returns {boolean} Whether the sound was resumed successfully.
     */
    resume: function () {
        if (this.manager.context.currentTime < this.startTime) {
            return false;
        }
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.createAndStartBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_RESUME'));
        return true;
    },
    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.WebAudioSound#stop
     * @returns {boolean} Whether the sound was stopped successfully.
     */
    stop: function () {
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.events.dispatch(new SoundEvent(this, 'SOUND_STOP'));
        return true;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#createAndStartBufferSource
     */
    createAndStartBufferSource: function () {
        var seek = this.currentConfig.seek;
        var delay = this.currentConfig.delay;
        var when = this.manager.context.currentTime + delay;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        var duration = this.duration - seek;
        this.playTime = when - seek;
        this.startTime = when;
        this.source = this.createBufferSource();
        this.applyConfig();
        this.source.start(Math.max(0, when), Math.max(0, offset), Math.max(0, duration));
        this.resetConfig();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#createAndStartLoopBufferSource
     */
    createAndStartLoopBufferSource: function () {
        var when = this.getLoopTime();
        var offset = this.currentMarker ? this.currentMarker.start : 0;
        var duration = this.duration;
        this.loopTime = when;
        this.loopSource = this.createBufferSource();
        this.loopSource.playbackRate.setValueAtTime(this.totalRate, 0);
        this.loopSource.start(Math.max(0, when), Math.max(0, offset), Math.max(0, duration));
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#createBufferSource
     */
    createBufferSource: function () {
        var _this = this;
        var source = this.manager.context.createBufferSource();
        source.buffer = this.audioBuffer;
        source.connect(this.muteNode);
        source.onended = function (ev) {
            if (ev.target === _this.source) {
                // sound ended
                if (_this.currentConfig.loop) {
                    _this.hasLooped = true;
                }
                else {
                    _this.hasEnded = true;
                }
            }
            // else was stopped
        };
        return source;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#stopAndRemoveBufferSource
     */
    stopAndRemoveBufferSource: function () {
        if (this.source) {
            this.source.stop();
            this.source.disconnect();
            this.source = null;
        }
        this.playTime = 0;
        this.startTime = 0;
        this.stopAndRemoveLoopBufferSource();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#stopAndRemoveLoopBufferSource
     */
    stopAndRemoveLoopBufferSource: function () {
        if (this.loopSource) {
            this.loopSource.stop();
            this.loopSource.disconnect();
            this.loopSource = null;
        }
        this.loopTime = 0;
    },
    /**
     * @protected
     * @method Phaser.Sound.WebAudioSound#applyConfig
     */
    applyConfig: function () {
        this.rateUpdates.length = 0;
        this.rateUpdates.push({
            time: 0,
            rate: 1
        });
        BaseSound.prototype.applyConfig.call(this);
    },
    /**
     * Update method called automatically by sound manager on every game step.
     *
     * @protected
     * @method Phaser.Sound.WebAudioSound#update
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (this.hasEnded) {
            this.hasEnded = false;
            BaseSound.prototype.stop.call(this);
            this.stopAndRemoveBufferSource();
            this.events.dispatch(new SoundEvent(this, 'SOUND_ENDED'));
        }
        else if (this.hasLooped) {
            this.hasLooped = false;
            this.source = this.loopSource;
            this.loopSource = null;
            this.playTime = this.startTime = this.loopTime;
            this.rateUpdates.length = 0;
            this.rateUpdates.push({
                time: 0,
                rate: this.totalRate
            });
            this.createAndStartLoopBufferSource();
            this.events.dispatch(new SoundEvent(this, 'SOUND_LOOP'));
        }
    },
    /**
     * Calls Phaser.Sound.BaseSound#destroy method
     * and cleans up all Web Audio API related stuff.
     *
     * @method Phaser.Sound.WebAudioSound#destroy
     */
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.audioBuffer = null;
        this.stopAndRemoveBufferSource();
        this.muteNode.disconnect();
        this.muteNode = null;
        this.volumeNode.disconnect();
        this.volumeNode = null;
        this.rateUpdates = null;
    },
    /**
     * Method used internally to calculate total playback rate of the sound.
     *
     * @protected
     * @method Phaser.Sound.WebAudioSound#setRate
     */
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        var now = this.manager.context.currentTime;
        if (this.source) {
            this.source.playbackRate.setValueAtTime(this.totalRate, now);
        }
        if (this.isPlaying) {
            this.rateUpdates.push({
                time: Math.max(this.startTime, now) - this.playTime,
                rate: this.totalRate
            });
            if (this.loopSource) {
                this.stopAndRemoveLoopBufferSource();
                this.createAndStartLoopBufferSource();
            }
        }
    },
    /**
     * Method used internally for calculating current playback time of a playing sound.
     *
     * @private
     * @method Phaser.Sound.WebAudioSound#getCurrentTime
     */
    getCurrentTime: function () {
        var currentTime = 0;
        for (var i = 0; i < this.rateUpdates.length; i++) {
            var nextTime = void 0;
            if (i < this.rateUpdates.length - 1) {
                nextTime = this.rateUpdates[i + 1].time;
            }
            else {
                nextTime = this.manager.context.currentTime - this.playTime;
            }
            currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        return currentTime;
    },
    /**
     * @private
     */
    getLoopTime: function () {
        var lastRateUpdateCurrentTime = 0;
        for (var i = 0; i < this.rateUpdates.length - 1; i++) {
            lastRateUpdateCurrentTime +=
                (this.rateUpdates[i + 1].time - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        var lastRateUpdate = this.rateUpdates[this.rateUpdates.length - 1];
        return this.playTime + lastRateUpdate.time
            + (this.duration - lastRateUpdateCurrentTime) / lastRateUpdate.rate;
    }
});
/**
 * Mute setting.
 * @property {boolean} mute
 */
Object.defineProperty(WebAudioSound.prototype, 'mute', {
    get: function () {
        return this.muteNode.gain.value === 0;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0);
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_MUTE', value));
    }
});
/**
 * Volume setting.
 * @property {number} volume
 */
Object.defineProperty(WebAudioSound.prototype, 'volume', {
    get: function () {
        return this.volumeNode.gain.value;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        this.volumeNode.gain.setValueAtTime(value, 0);
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_VOLUME', value));
    }
});
/**
 * Playback rate.
 * @property {number} rate
 */
Object.defineProperty(WebAudioSound.prototype, 'rate', {
    get: function () {
        return this.currentConfig.rate;
    },
    set: function (value) {
        this.currentConfig.rate = value;
        this.setRate();
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_RATE', value));
    }
});
/**
 * Detuning of sound.
 * @property {number} detune
 */
Object.defineProperty(WebAudioSound.prototype, 'detune', {
    get: function () {
        return this.currentConfig.detune;
    },
    set: function (value) {
        this.currentConfig.detune = value;
        this.setRate();
        this.events.dispatch(new SoundValueEvent(this, 'SOUND_DETUNE', value));
    }
});
/**
 * Current position of playing sound.
 * @property {number} seek
 */
Object.defineProperty(WebAudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            if (this.manager.context.currentTime < this.startTime) {
                return this.startTime - this.playTime;
            }
            return this.getCurrentTime();
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.manager.context.currentTime < this.startTime) {
            return;
        }
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            this.currentConfig.seek = value;
            this.events.dispatch(new SoundValueEvent(this, 'SOUND_SEEK', value));
        }
        if (this.isPlaying) {
            this.stopAndRemoveBufferSource();
            this.createAndStartBufferSource();
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @property {boolean} loop
 */
Object.defineProperty(WebAudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        this.currentConfig.loop = value;
        if (this.isPlaying) {
            this.stopAndRemoveLoopBufferSource();
            if (value) {
                this.createAndStartLoopBufferSource();
            }
        }
    }
});
module.exports = WebAudioSound;
<|endoftext|>


<commit_before>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
//  Phaser.Sound.WebAudioSound
// TODO support webkitAudioContext implementation differences
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
var WebAudioSound = new Class({
    Extends: BaseSound,
    initialize: function WebAudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * [description]
         *
         * @private
         * @property {AudioBuffer} audioBuffer
         */
        this.audioBuffer = manager.game.cache.audio.get(key);
        if (!this.audioBuffer) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * [description]
         *
         * @private
         * @property {AudioBufferSourceNode} source
         */
        this.source = null;
        /**
         * [description]
         *
         * @private
         * @property {AudioBufferSourceNode} loopSource
         */
        this.loopSource = null;
        /**
         * [description]
         *
         * @private
         * @property {GainNode} muteNode
         */
        this.muteNode = manager.context.createGain();
        /**
         * [description]
         *
         * @private
         * @property {GainNode} volumeNode
         */
        this.volumeNode = manager.context.createGain();
        /**
         * The time the previous playback started at based on
         * BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} startTime
         */
        this.startTime = 0;
        /**
         * An array where we keep track of all rate updates during playback.
         *
         * @private
         * @property {{ time: number, rate: number }[]} rateUpdates
         */
        this.rateUpdates = [];
        /**
         * Used for keeping track when sound source playback has ended
         * so it's state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasEnded
         */
        this.hasEnded = false;
        this.muteNode.connect(this.volumeNode);
        this.volumeNode.connect(manager.destination);
        this.duration = this.audioBuffer.duration;
        this.totalDuration = this.audioBuffer.duration;
        BaseSound.call(this, manager, key, config);
    },
    play: function (markerName, config) {
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return null;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.createAndStartBufferSource();
        return this;
    },
    pause: function () {
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time
        this.stopAndRemoveBufferSource();
        return true;
    },
    resume: function () {
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.createAndStartBufferSource();
        return true;
    },
    stop: function () {
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        return true;
    },
    /**
     * @private
     */
    createBufferSource: function () {
        var _this = this;
        var source = this.manager.context.createBufferSource();
        source.buffer = this.audioBuffer;
        source.connect(this.muteNode);
        source.onended = function (ev) {
            if (ev.target === _this.source) {
                // sound ended
                _this.hasEnded = true;
            }
            // else was stopped
        };
        return source;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    // TODO add delay param
    createAndStartBufferSource: function () {
        var seek = this.currentConfig.seek;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        var duration = this.duration - seek;
        this.startTime = this.manager.context.currentTime - seek;
        this.source = this.createBufferSource();
        this.applyConfig();
        this.source.start(0, Math.max(0, offset), Math.max(0, duration));
        this.resetConfig();
    },
    /**
     * @private
     */
    createAndStartLoopBufferSource: function () {
        this.loopSource = this.createBufferSource();
        var offset = this.currentMarker ? this.currentMarker.start : 0;
        var duration = this.duration;
        this.loopSource.start(Math.max(0, this.startTime + duration), Math.max(0, offset), Math.max(0, duration));
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    stopAndRemoveBufferSource: function () {
        if (this.source) {
            this.source.stop();
            this.source = null;
        }
        if (this.loopSource) {
            this.source.stop();
            this.source = null;
        }
        this.startTime = 0;
    },
    /**
     * @protected
     */
    applyConfig: function () {
        this.rateUpdates.length = 0;
        this.rateUpdates.push({
            time: 0,
            rate: 1
        });
        BaseSound.prototype.applyConfig.call(this);
    },
    /**
     * Update method called on every game step.
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (this.hasEnded) {
            this.hasEnded = false;
            this.stop();
        }
    },
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.audioBuffer = null;
        this.stopAndRemoveBufferSource();
        this.muteNode.disconnect();
        this.muteNode = null;
        this.volumeNode.disconnect();
        this.volumeNode = null;
        this.rateUpdates = null;
    },
    /**
     * @private
     */
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        if (this.source) {
            this.source.playbackRate.setValueAtTime(this.totalRate, 0);
        }
        if (this.loopSource) {
            this.loopSource.playbackRate.setValueAtTime(this.totalRate, 0);
        }
        if (this.isPlaying) {
            this.rateUpdates.push({
                time: this.manager.context.currentTime - this.startTime,
                rate: this.totalRate
            });
        }
    },
    /**
     * @private
     */
    getCurrentTime: function () {
        var currentTime = 0;
        for (var i = 0; i < this.rateUpdates.length; i++) {
            var nextTime = void 0;
            if (i < this.rateUpdates.length - 1) {
                nextTime = this.rateUpdates[i + 1].time;
            }
            else {
                nextTime = this.manager.context.currentTime - this.startTime;
            }
            currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        return currentTime;
    }
});
/**
 * Mute setting.
 * @property {boolean} mute
 */
Object.defineProperty(WebAudioSound.prototype, 'mute', {
    get: function () {
        return this.muteNode.gain.value === 0;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0);
    }
});
/**
 * Volume setting.
 * @property {number} volume
 */
Object.defineProperty(WebAudioSound.prototype, 'volume', {
    get: function () {
        return this.volumeNode.gain.value;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        this.volumeNode.gain.setValueAtTime(value, 0);
    }
});
/**
 * Playback rate.
 * @property {number} rate
 */
Object.defineProperty(WebAudioSound.prototype, 'rate', {
    get: function () {
        return this.currentConfig.rate;
    },
    set: function (value) {
        this.currentConfig.rate = value;
        this.setRate();
    }
});
/**
 * Detuning of sound.
 * @property {number} detune
 */
Object.defineProperty(WebAudioSound.prototype, 'detune', {
    get: function () {
        return this.currentConfig.detune;
    },
    set: function (value) {
        this.currentConfig.detune = value;
        this.setRate();
    }
});
/**
 * Current position of playing sound.
 * @property {number} seek
 */
Object.defineProperty(WebAudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            return this.getCurrentTime();
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            this.currentConfig.seek = value;
        }
        if (this.isPlaying) {
            this.stopAndRemoveBufferSource();
            this.createAndStartBufferSource();
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @property {boolean} loop
 */
Object.defineProperty(WebAudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        if (value === this.currentConfig.loop) {
            return;
        }
        this.currentConfig.loop = value;
        if (this.isPlaying) {
            if (value) {
                // TODO Add loop source
            }
            else {
                // TODO Remove loop source
            }
        }
    }
});
module.exports = WebAudioSound;
<commit_msg>Ditching loopSource approach as logic would be too complicated when taking playback rate into account<commit_after>var Class = require('../../utils/Class');
var BaseSound = require('../BaseSound');
//  Phaser.Sound.WebAudioSound
// TODO support webkitAudioContext implementation differences
// https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext
var WebAudioSound = new Class({
    Extends: BaseSound,
    initialize: function WebAudioSound(manager, key, config) {
        if (config === void 0) { config = {}; }
        /**
         * [description]
         *
         * @private
         * @property {AudioBuffer} audioBuffer
         */
        this.audioBuffer = manager.game.cache.audio.get(key);
        if (!this.audioBuffer) {
            console.error('No audio loaded in cache with key: \'' + key + '\'!');
            return;
        }
        /**
         * [description]
         *
         * @private
         * @property {AudioBufferSourceNode} source
         */
        this.source = null;
        /**
         * [description]
         *
         * @private
         * @property {GainNode} muteNode
         */
        this.muteNode = manager.context.createGain();
        /**
         * [description]
         *
         * @private
         * @property {GainNode} volumeNode
         */
        this.volumeNode = manager.context.createGain();
        /**
         * The time the previous playback started at based on
         * BaseAudioContext.currentTime value.
         *
         * @private
         * @property {number} startTime
         */
        this.startTime = 0;
        /**
         * An array where we keep track of all rate updates during playback.
         *
         * @private
         * @property {{ time: number, rate: number }[]} rateUpdates
         */
        this.rateUpdates = [];
        /**
         * Used for keeping track when sound source playback has ended
         * so it's state can be updated accordingly.
         *
         * @private
         * @property {boolean} hasEnded
         */
        this.hasEnded = false;
        this.muteNode.connect(this.volumeNode);
        this.volumeNode.connect(manager.destination);
        this.duration = this.audioBuffer.duration;
        this.totalDuration = this.audioBuffer.duration;
        BaseSound.call(this, manager, key, config);
    },
    play: function (markerName, config) {
        if (!BaseSound.prototype.play.call(this, markerName, config)) {
            return null;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        this.createAndStartBufferSource();
        return this;
    },
    pause: function () {
        if (!BaseSound.prototype.pause.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = true \/\/\/
        this.currentConfig.seek = this.getCurrentTime(); // Equivalent to setting paused time
        this.stopAndRemoveBufferSource();
        return true;
    },
    resume: function () {
        if (!BaseSound.prototype.resume.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = true, isPaused = false \/\/\/
        this.createAndStartBufferSource();
        return true;
    },
    stop: function () {
        if (!BaseSound.prototype.stop.call(this)) {
            return false;
        }
        //  \/\/\/ isPlaying = false, isPaused = false \/\/\/
        this.stopAndRemoveBufferSource();
        return true;
    },
    /**
     * @private
     */
    createBufferSource: function () {
        var _this = this;
        var source = this.manager.context.createBufferSource();
        source.buffer = this.audioBuffer;
        source.connect(this.muteNode);
        source.onended = function (ev) {
            if (ev.target === _this.source) {
                // sound ended
                _this.hasEnded = true;
            }
            // else was stopped
        };
        return source;
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    // TODO add delay param
    createAndStartBufferSource: function () {
        var seek = this.currentConfig.seek;
        var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
        var duration = this.duration - seek;
        this.startTime = this.manager.context.currentTime - seek;
        this.source = this.createBufferSource();
        this.applyConfig();
        this.source.start(0, Math.max(0, offset), Math.max(0, duration));
        this.resetConfig();
    },
    /**
     * Used internally to do what the name says.
     *
     * @private
     */
    stopAndRemoveBufferSource: function () {
        if (this.source) {
            this.source.stop();
            this.source = null;
        }
        this.startTime = 0;
    },
    /**
     * @protected
     */
    applyConfig: function () {
        this.rateUpdates.length = 0;
        this.rateUpdates.push({
            time: 0,
            rate: 1
        });
        BaseSound.prototype.applyConfig.call(this);
    },
    /**
     * Update method called on every game step.
     *
     * @param {number} time - The current timestamp as generated by the Request Animation Frame or SetTimeout.
     * @param {number} delta - The delta time elapsed since the last frame.
     */
    update: function (time, delta) {
        if (this.hasEnded) {
            this.hasEnded = false;
            this.stop();
        }
    },
    destroy: function () {
        BaseSound.prototype.destroy.call(this);
        this.audioBuffer = null;
        this.stopAndRemoveBufferSource();
        this.muteNode.disconnect();
        this.muteNode = null;
        this.volumeNode.disconnect();
        this.volumeNode = null;
        this.rateUpdates = null;
    },
    /**
     * @private
     */
    setRate: function () {
        BaseSound.prototype.setRate.call(this);
        if (this.source) {
            this.source.playbackRate.setValueAtTime(this.totalRate, 0);
        }
        if (this.isPlaying) {
            this.rateUpdates.push({
                time: this.manager.context.currentTime - this.startTime,
                rate: this.totalRate
            });
        }
    },
    /**
     * @private
     */
    getCurrentTime: function () {
        var currentTime = 0;
        for (var i = 0; i < this.rateUpdates.length; i++) {
            var nextTime = void 0;
            if (i < this.rateUpdates.length - 1) {
                nextTime = this.rateUpdates[i + 1].time;
            }
            else {
                nextTime = this.manager.context.currentTime - this.startTime;
            }
            currentTime += (nextTime - this.rateUpdates[i].time) * this.rateUpdates[i].rate;
        }
        return currentTime;
    }
});
/**
 * Mute setting.
 * @property {boolean} mute
 */
Object.defineProperty(WebAudioSound.prototype, 'mute', {
    get: function () {
        return this.muteNode.gain.value === 0;
    },
    set: function (value) {
        this.currentConfig.mute = value;
        this.muteNode.gain.setValueAtTime(value ? 0 : 1, 0);
    }
});
/**
 * Volume setting.
 * @property {number} volume
 */
Object.defineProperty(WebAudioSound.prototype, 'volume', {
    get: function () {
        return this.volumeNode.gain.value;
    },
    set: function (value) {
        this.currentConfig.volume = value;
        this.volumeNode.gain.setValueAtTime(value, 0);
    }
});
/**
 * Playback rate.
 * @property {number} rate
 */
Object.defineProperty(WebAudioSound.prototype, 'rate', {
    get: function () {
        return this.currentConfig.rate;
    },
    set: function (value) {
        this.currentConfig.rate = value;
        this.setRate();
    }
});
/**
 * Detuning of sound.
 * @property {number} detune
 */
Object.defineProperty(WebAudioSound.prototype, 'detune', {
    get: function () {
        return this.currentConfig.detune;
    },
    set: function (value) {
        this.currentConfig.detune = value;
        this.setRate();
    }
});
/**
 * Current position of playing sound.
 * @property {number} seek
 */
Object.defineProperty(WebAudioSound.prototype, 'seek', {
    get: function () {
        if (this.isPlaying) {
            return this.getCurrentTime();
        }
        else if (this.isPaused) {
            return this.currentConfig.seek;
        }
        else {
            return 0;
        }
    },
    set: function (value) {
        if (this.isPlaying || this.isPaused) {
            value = Math.min(Math.max(0, value), this.duration);
            this.currentConfig.seek = value;
        }
        if (this.isPlaying) {
            this.stopAndRemoveBufferSource();
            this.createAndStartBufferSource();
        }
    }
});
/**
 * Property indicating whether or not
 * the sound or current sound marker will loop.
 *
 * @property {boolean} loop
 */
Object.defineProperty(WebAudioSound.prototype, 'loop', {
    get: function () {
        return this.currentConfig.loop;
    },
    set: function (value) {
        if (value === this.currentConfig.loop) {
            return;
        }
        this.currentConfig.loop = value;
        if (this.isPlaying) {
            if (value) {
                // TODO Add loop source
            }
            else {
                // TODO Remove loop source
            }
        }
    }
});
module.exports = WebAudioSound;
<|endoftext|>