<commit_before>/* global importScripts, sauce */

importScripts('/src/common/base.js');
const version = new URLSearchParams(location.search).get('version');
const url = `${location.protocol}//${location.hostname}/`;
self.sauceBaseInit(null, url, {name: 'Sauce Worker', version});
importScripts('/src/bg/hist/db.js');
importScripts('/src/common/lib.js');

class SimpleEvent {
    constructor() {
        this._reset();
    }

    _reset() {
        this._p = new Promise(resolve => {
            this._resolve = resolve;
        });
        this._p.finally(() => this._reset());
    }

    async wait() {
        await this._p;
    }

    set() {
        this._resolve();
    }
}

const streamsStore = sauce.hist.db.StreamsStore.singleton();
const peaksStore = sauce.hist.db.PeaksStore.singleton();

async function getActivitiesStreams(activities, streamsDesc) {
    const streamKeys = [];
    const actStreams = new Map();
    for (const a of activities) {
        const streams = Array.isArray(streamsDesc) ?
            streamsDesc :
            streamsDesc[a.basetype === 'run' ? 'run' : a.basetype === 'ride' ? 'ride' : 'other'];
        for (const stream of streams) {
            streamKeys.push([a.id, stream]);
        }
        actStreams.set(a.id, {});
    }
    for (const x of await streamsStore.getMany(streamKeys, {_skipClone: true, _skipCache: true})) {
        if (x) {
            actStreams.get(x.activity)[x.stream] = x.data;
        }
    }
    return actStreams;
}

class WorkerProcessor {
    static factory(...args) {
        const instance = new this(...args);
        instance.start();
        return instance;
    }

    constructor({id, outgoingPort, incomingPort}) {
        this.id = id;
        this._outgoingPort = outgoingPort;
        this._incomingPort = incomingPort;
        this.stopping = false;
        this.incoming = [];
        this._lastSeq;
        this.wakeEvent = new SimpleEvent();
        incomingPort.addEventListener('message', ev => this._onIncoming(ev.data));
        incomingPort.start();
        outgoingPort.start();  // We don't actually use listeners, but I don't want to forget.
    }

    async _onIncoming({seq, op, data}) {
        if (this._lastSeq && seq - this._lastSeq !== 1) {
            debugger;
            console.error("Worker message is out of sequence", this._lastSeq, seq);
        }
        try {
            if (this.stopping) {
                console.error("Worker recieved message after stop request", {seq, op});
                throw new Error('worker is stopping/stopped');
            }
            await this.__onIncoming({data, op});
            this._incomingPort.postMessage({seq, success: true});
        } catch(e) {
            this._incomingPort.postMessage({seq, success: false, error: e.message});
        }
    }

    async __onIncoming({op, data}) {
        if (op === 'data') {
            for (const x of data) {
                this.incoming.push(x);
                // XXX Wake event surely?
            }
            this.wakeEvent.set();
        } else if (op === 'stop') {
            await this.stop();
        } else {
            throw new Error('Invalid worker channel operation');
        }
    }

    send(data) {
        this._outgoingPort.postMessage(data);
    }

    start() {
        this._procTask = this.runProcessor();
    }

    async stop() {
        if (this.stopping) {
            throw new TypeError('already stopping');
        }
        this.stopping = true;
        this.wakeEvent.set();
        await this._procTask;
    }

    async runProcessor() {
        try {
            await this.processor();
        } finally {
            this.stopping = true;
            this._procTask = null;
        }
    }

    processor() {
        // while ! this.stopping...
    }
}

class FindPeaks extends WorkerProcessor {
    constructor({periods, distances, athlete, ...args}) {
        super(args);
        this.periods = periods;
        this.distances = distances;
        this.useEstWatts = athlete.estCyclingWatts && athlete.estCyclingPeaks;
        this.athlete = athlete;
        this.gender = athlete.gender || 'male';
    }

    async processor() {
        while (!this.stopping) {
            while (this.incoming.length) {
                const batch = this.incoming.splice(0, Infinity);
                const errors = await this.findPeaks(batch);
                this.send({done: batch.map(x => x.id), errors});
            }
            await this.wakeEvent.wait();
        }
    }

    getRankLevel(period, p, wp, weight) {
        const rank = sauce.power.rankLevel(period, p, wp, weight, this.gender);
        if (rank.level > 0) {
            return rank;
        }
    }

    async findPeaks(activities) {
        const actStreams = await getActivitiesStreams(activities, {
            run: ['time', 'active', 'heartrate', 'distance', 'grade_adjusted_distance'],
            ride: ['time', 'active', 'heartrate', 'watts', 'watts_calc'].filter(x =>
                x !== 'watts_calc' || this.useEstWatts),
            other: ['time', 'active', 'heartrate', 'watts'],
        });
        const upPeaks = [];
        const errors = [];
        let ts = Date.now();
        for (const activity of activities) {
            if (Date.now() - ts > 400) {
                // Reduce message ACK latency by defering for one task iteration.
                await sauce.sleep(0);
                ts = Date.now();
            }
            if (activity.peaksExclude) {
                continue;  // cleanup happens in finalizer proc.
            }
            const isRun = activity.basetype === 'run';
            const isRide = activity.basetype === 'ride';
            const streams = actStreams.get(activity.id);
            const addPeak = (a1, a2, a3, a4, extra) => {
                const entry = sauce.peaks.createStoreEntry(a1, a2, a3, a4, streams.time, activity, extra);
                if (entry) {
                    upPeaks.push(entry);
                }
            };
            if (streams.heartrate) {
                try {
                    for (const period of this.periods) {
                        const roll = sauce.data.peakAverage(period, streams.time,
                            streams.heartrate, {active: true, ignoreZeros: true, activeStream: streams.active});
                        if (roll) {
                            addPeak('hr', period, roll.avg(), roll);
                        }
                    }
                } catch(e) {
                    // XXX make this better than a big try/catch
                    debugger;
                    console.error(this.id, "Failed to create peaks for: " + activity.id, e);
                    errors.push({activity: activity.id, error: e.message});
                }
            }
            const watts = streams.watts || streams.watts_calc;
            if (watts && !isRun) { // Runs have their own processor for this.
                const estimate = !streams.watts;
                const weight = sauce.model.getAthleteHistoryValueAt(this.athlete.weightHistory, activity.ts);
                try {
                    for (const period of this.periods) {
                        if (estimate && period < 300) {
                            continue;
                        }
                        const rp = sauce.power.correctedRollingPower(streams.time, period);
                        if (!rp) {
                            continue;
                        }
                        const leadCloneOpts = {inlineXP: false, inlineNP: false};
                        const wrp = (!estimate && period >= 300) ?
                            rp.clone({active: true, inlineNP: true, inlineXP: true}) :
                            undefined;
                        const leaders = {};
                        // Instead of using peakPower, peakNP, we do our own reduction to save
                        // repeative iterations on the same dataset; it's about 50% faster.
                        for (let i = 0; i < streams.time.length; i++) {
                            const t = streams.time[i];
                            const w = watts[i];
                            const a = streams.active[i];
                            rp.add(t, w, a);
                            if (wrp) {
                                wrp.resize();
                            }
                            if (rp.full()) {
                                const power = rp.avg();
                                if (power && (!leaders.power || power >= leaders.power.value)) {
                                    leaders.power = {roll: rp.clone(), value: power, np: wrp && wrp.np()};
                                }
                            }
                            if (wrp && wrp.full()) {
                                const np = wrp.np();
                                if (np && (!leaders.np || np >= leaders.np.value)) {
                                    leaders.np = {roll: wrp.clone(leadCloneOpts), value: np};
                                }
                                const xp = wrp.xp();
                                if (xp && (!leaders.xp || xp >= leaders.xp.value)) {
                                    leaders.xp = {roll: wrp.clone(leadCloneOpts), value: xp};
                                }
                            }
                        }
                        if (leaders.power) {
                            const l = leaders.power;
                            const rankLevel = isRide ? this.getRankLevel(l.roll.active(), l.value, l.np, weight) : undefined;
                            addPeak('power', period, l.value, l.roll, {wp: l.np, estimate, rankLevel});
                        }
                        if (leaders.np) {
                            const l = leaders.np;
                            const np = l.roll.np({external: true});
                            const power = l.roll.avg({active: false});
                            const rankLevel = isRide ? this.getRankLevel(l.roll.active(), power, np, weight) : undefined;
                            addPeak('np', period, np, l.roll, {power, estimate, rankLevel});
                        }
                        if (leaders.xp) {
                            const l = leaders.xp;
                            const xp = l.roll.xp({external: true});
                            const power = l.roll.avg({active: false});
                            const rankLevel = isRide ? this.getRankLevel(l.roll.active(), power, xp, weight) : undefined;
                            addPeak('xp', period, xp, l.roll, {power, estimate, rankLevel});
                        }
                    }
                } catch(e) {
                    // XXX make this better than a big try/catch
                    debugger;
                    console.error(this.id, "Failed to create peaks for: " + activity.id, e);
                    errors.push({activity: activity.id, error: e.message});
                }
            }
            if (isRun && streams.distance) {
                try {
                    for (const distance of this.distances) {
                        let roll = sauce.pace.bestPace(distance, streams.time, streams.distance);
                        if (roll) {
                            addPeak('pace', distance, roll.avg(), roll);
                        }
                        if (streams.grade_adjusted_distance) {
                            roll = sauce.pace.bestPace(distance, streams.time, streams.grade_adjusted_distance);
                            if (roll) {
                                addPeak('gap', distance, roll.avg(), roll);
                            }
                        }
                    }
                } catch(e) {
                    // XXX make this better than a big try/catch
                    debugger;
                    console.error(this.id, "Failed to create peaks for: " + activity.id, e);
                    errors.push({activity: activity.id, error: e.message});
                }
            }
        }
        await peaksStore.putMany(upPeaks);
        return errors;
    }
}

const processors = {
    "find-peaks": (...args) => FindPeaks.factory(...args),
};

self.addEventListener('message', async ev => {
    const processor = processors[ev.data.processor];
    if (!processor) {
        throw new Error("Invalid Processor");
    }
    await processor(ev.data);
});
<commit_msg>More secure worker processor lookup.<commit_after>/* global importScripts, sauce */

importScripts('/src/common/base.js');
const version = new URLSearchParams(location.search).get('version');
const url = `${location.protocol}//${location.hostname}/`;
self.sauceBaseInit(null, url, {name: 'Sauce Worker', version});
importScripts('/src/bg/hist/db.js');
importScripts('/src/common/lib.js');

class SimpleEvent {
    constructor() {
        this._reset();
    }

    _reset() {
        this._p = new Promise(resolve => {
            this._resolve = resolve;
        });
        this._p.finally(() => this._reset());
    }

    async wait() {
        await this._p;
    }

    set() {
        this._resolve();
    }
}

const streamsStore = sauce.hist.db.StreamsStore.singleton();
const peaksStore = sauce.hist.db.PeaksStore.singleton();

async function getActivitiesStreams(activities, streamsDesc) {
    const streamKeys = [];
    const actStreams = new Map();
    for (const a of activities) {
        const streams = Array.isArray(streamsDesc) ?
            streamsDesc :
            streamsDesc[a.basetype === 'run' ? 'run' : a.basetype === 'ride' ? 'ride' : 'other'];
        for (const stream of streams) {
            streamKeys.push([a.id, stream]);
        }
        actStreams.set(a.id, {});
    }
    for (const x of await streamsStore.getMany(streamKeys, {_skipClone: true, _skipCache: true})) {
        if (x) {
            actStreams.get(x.activity)[x.stream] = x.data;
        }
    }
    return actStreams;
}

class WorkerProcessor {
    static factory(...args) {
        const instance = new this(...args);
        instance.start();
        return instance;
    }

    constructor({id, outgoingPort, incomingPort}) {
        this.id = id;
        this._outgoingPort = outgoingPort;
        this._incomingPort = incomingPort;
        this.stopping = false;
        this.incoming = [];
        this._lastSeq;
        this.wakeEvent = new SimpleEvent();
        incomingPort.addEventListener('message', ev => this._onIncoming(ev.data));
        incomingPort.start();
        outgoingPort.start();  // We don't actually use listeners, but I don't want to forget.
    }

    async _onIncoming({seq, op, data}) {
        if (this._lastSeq && seq - this._lastSeq !== 1) {
            debugger;
            console.error("Worker message is out of sequence", this._lastSeq, seq);
        }
        try {
            if (this.stopping) {
                console.error("Worker recieved message after stop request", {seq, op});
                throw new Error('worker is stopping/stopped');
            }
            await this.__onIncoming({data, op});
            this._incomingPort.postMessage({seq, success: true});
        } catch(e) {
            this._incomingPort.postMessage({seq, success: false, error: e.message});
        }
    }

    async __onIncoming({op, data}) {
        if (op === 'data') {
            for (const x of data) {
                this.incoming.push(x);
                // XXX Wake event surely?
            }
            this.wakeEvent.set();
        } else if (op === 'stop') {
            await this.stop();
        } else {
            throw new Error('Invalid worker channel operation');
        }
    }

    send(data) {
        this._outgoingPort.postMessage(data);
    }

    start() {
        this._procTask = this.runProcessor();
    }

    async stop() {
        if (this.stopping) {
            throw new TypeError('already stopping');
        }
        this.stopping = true;
        this.wakeEvent.set();
        await this._procTask;
    }

    async runProcessor() {
        try {
            await this.processor();
        } finally {
            this.stopping = true;
            this._procTask = null;
        }
    }

    processor() {
        // while ! this.stopping...
    }
}

class FindPeaks extends WorkerProcessor {
    constructor({periods, distances, athlete, ...args}) {
        super(args);
        this.periods = periods;
        this.distances = distances;
        this.useEstWatts = athlete.estCyclingWatts && athlete.estCyclingPeaks;
        this.athlete = athlete;
        this.gender = athlete.gender || 'male';
    }

    async processor() {
        while (!this.stopping) {
            while (this.incoming.length) {
                const batch = this.incoming.splice(0, Infinity);
                const errors = await this.findPeaks(batch);
                this.send({done: batch.map(x => x.id), errors});
            }
            await this.wakeEvent.wait();
        }
    }

    getRankLevel(period, p, wp, weight) {
        const rank = sauce.power.rankLevel(period, p, wp, weight, this.gender);
        if (rank.level > 0) {
            return rank;
        }
    }

    async findPeaks(activities) {
        const actStreams = await getActivitiesStreams(activities, {
            run: ['time', 'active', 'heartrate', 'distance', 'grade_adjusted_distance'],
            ride: ['time', 'active', 'heartrate', 'watts', 'watts_calc'].filter(x =>
                x !== 'watts_calc' || this.useEstWatts),
            other: ['time', 'active', 'heartrate', 'watts'],
        });
        const upPeaks = [];
        const errors = [];
        let ts = Date.now();
        for (const activity of activities) {
            if (Date.now() - ts > 400) {
                // Reduce message ACK latency by defering for one task iteration.
                await sauce.sleep(0);
                ts = Date.now();
            }
            if (activity.peaksExclude) {
                continue;  // cleanup happens in finalizer proc.
            }
            const isRun = activity.basetype === 'run';
            const isRide = activity.basetype === 'ride';
            const streams = actStreams.get(activity.id);
            const addPeak = (a1, a2, a3, a4, extra) => {
                const entry = sauce.peaks.createStoreEntry(a1, a2, a3, a4, streams.time, activity, extra);
                if (entry) {
                    upPeaks.push(entry);
                }
            };
            if (streams.heartrate) {
                try {
                    for (const period of this.periods) {
                        const roll = sauce.data.peakAverage(period, streams.time,
                            streams.heartrate, {active: true, ignoreZeros: true, activeStream: streams.active});
                        if (roll) {
                            addPeak('hr', period, roll.avg(), roll);
                        }
                    }
                } catch(e) {
                    // XXX make this better than a big try/catch
                    debugger;
                    console.error(this.id, "Failed to create peaks for: " + activity.id, e);
                    errors.push({activity: activity.id, error: e.message});
                }
            }
            const watts = streams.watts || streams.watts_calc;
            if (watts && !isRun) { // Runs have their own processor for this.
                const estimate = !streams.watts;
                const weight = sauce.model.getAthleteHistoryValueAt(this.athlete.weightHistory, activity.ts);
                try {
                    for (const period of this.periods) {
                        if (estimate && period < 300) {
                            continue;
                        }
                        const rp = sauce.power.correctedRollingPower(streams.time, period);
                        if (!rp) {
                            continue;
                        }
                        const leadCloneOpts = {inlineXP: false, inlineNP: false};
                        const wrp = (!estimate && period >= 300) ?
                            rp.clone({active: true, inlineNP: true, inlineXP: true}) :
                            undefined;
                        const leaders = {};
                        // Instead of using peakPower, peakNP, we do our own reduction to save
                        // repeative iterations on the same dataset; it's about 50% faster.
                        for (let i = 0; i < streams.time.length; i++) {
                            const t = streams.time[i];
                            const w = watts[i];
                            const a = streams.active[i];
                            rp.add(t, w, a);
                            if (wrp) {
                                wrp.resize();
                            }
                            if (rp.full()) {
                                const power = rp.avg();
                                if (power && (!leaders.power || power >= leaders.power.value)) {
                                    leaders.power = {roll: rp.clone(), value: power, np: wrp && wrp.np()};
                                }
                            }
                            if (wrp && wrp.full()) {
                                const np = wrp.np();
                                if (np && (!leaders.np || np >= leaders.np.value)) {
                                    leaders.np = {roll: wrp.clone(leadCloneOpts), value: np};
                                }
                                const xp = wrp.xp();
                                if (xp && (!leaders.xp || xp >= leaders.xp.value)) {
                                    leaders.xp = {roll: wrp.clone(leadCloneOpts), value: xp};
                                }
                            }
                        }
                        if (leaders.power) {
                            const l = leaders.power;
                            const rankLevel = isRide ? this.getRankLevel(l.roll.active(), l.value, l.np, weight) : undefined;
                            addPeak('power', period, l.value, l.roll, {wp: l.np, estimate, rankLevel});
                        }
                        if (leaders.np) {
                            const l = leaders.np;
                            const np = l.roll.np({external: true});
                            const power = l.roll.avg({active: false});
                            const rankLevel = isRide ? this.getRankLevel(l.roll.active(), power, np, weight) : undefined;
                            addPeak('np', period, np, l.roll, {power, estimate, rankLevel});
                        }
                        if (leaders.xp) {
                            const l = leaders.xp;
                            const xp = l.roll.xp({external: true});
                            const power = l.roll.avg({active: false});
                            const rankLevel = isRide ? this.getRankLevel(l.roll.active(), power, xp, weight) : undefined;
                            addPeak('xp', period, xp, l.roll, {power, estimate, rankLevel});
                        }
                    }
                } catch(e) {
                    // XXX make this better than a big try/catch
                    debugger;
                    console.error(this.id, "Failed to create peaks for: " + activity.id, e);
                    errors.push({activity: activity.id, error: e.message});
                }
            }
            if (isRun && streams.distance) {
                try {
                    for (const distance of this.distances) {
                        let roll = sauce.pace.bestPace(distance, streams.time, streams.distance);
                        if (roll) {
                            addPeak('pace', distance, roll.avg(), roll);
                        }
                        if (streams.grade_adjusted_distance) {
                            roll = sauce.pace.bestPace(distance, streams.time, streams.grade_adjusted_distance);
                            if (roll) {
                                addPeak('gap', distance, roll.avg(), roll);
                            }
                        }
                    }
                } catch(e) {
                    // XXX make this better than a big try/catch
                    debugger;
                    console.error(this.id, "Failed to create peaks for: " + activity.id, e);
                    errors.push({activity: activity.id, error: e.message});
                }
            }
        }
        await peaksStore.putMany(upPeaks);
        return errors;
    }
}

const processors = new Map(Object.entries({
    "find-peaks": (...args) => FindPeaks.factory(...args),
}));

self.addEventListener('message', async ev => {
    const processor = processors.get(ev.data.processor);
    if (!processor) {
        throw new Error("Invalid Processor");
    }
    await processor(ev.data);
});
<|endoftext|>


<commit_before>'use strict';

const _ = require('lodash');

const RNFatchBlob = require('react-native-fetch-blob').default;

const {
    fs
} = RNFatchBlob;

const baseCacheDir = fs.dirs.CacheDir + '/imagesCacheDir';

const SHA1 = require("crypto-js/sha1");
const URL = require('url-parse');

const defaultHeaders = {};
const defaultResolveHeaders = _.constant(defaultHeaders);

const defaultOptions = {
    useQueryParamsInCacheKey: false
};

const activeDownloads = {};

function serializeObjectKeys(obj) {
    return _(obj)
        .toPairs()
        .sortBy(a => a[0])
        .map(a => a[1])
        .value();
}

function getQueryForCacheKey(url, useQueryParamsInCacheKey) {
    if (_.isArray(useQueryParamsInCacheKey)) {
        return serializeObjectKeys(_.pick(url.query, useQueryParamsInCacheKey));
    }
    if (useQueryParamsInCacheKey) {
        return serializeObjectKeys(url.query);
    }
    return '';
}

function generateCacheKey(url, options) {
    const parsedUrl = new URL(url, null, true);

    const pathParts = parsedUrl.pathname.split('/');

    // last path part is the file name
    const fileName = pathParts.pop();
    const filePath = pathParts.join('/');

    const parts = fileName.split('.');
    // TODO - try to figure out the file type or let the user provide it, for now use jpg as default
    const type = parts.length > 1 ? parts.pop() : 'jpg';

    const cacheable = filePath + fileName + type + getQueryForCacheKey(parsedUrl, options.useQueryParamsInCacheKey);
    return SHA1(cacheable) + '.' + type;
}

function getCachePath(url, options) {
    if (options.cacheGroup) {
        return options.cacheGroup;
    }
    const {
        host
    } = new URL(url);
    return host.replace(/[^a-z0-9]/gi, '').toLowerCase();
}

function getCachedImageFilePath(url, options) {
    const cachePath = getCachePath(url, options);
    const cacheKey = generateCacheKey(url, options);

    return `${baseCacheDir}/${cachePath}/${cacheKey}`;
}

function deleteFile(filePath) {
    return fs.stat(filePath)
        .then(res => res && res.type === 'file')
        .then(exists => exists && fs.unlink(filePath))
        .catch((err) => {
            // swallow error to always resolve
        });
}

function getDirPath(filePath) {
    return _.initial(filePath.split('/')).join('/');
}

function ensurePath(dirPath) {
    return fs.isDir(dirPath)
        .then(exists =>
            !exists && fs.mkdir(dirPath)
        )
        .catch(err => {
            // swallow folder already exists errors
            if (err.message.includes('folder already exists')) {
                return;
            }
            throw err;
        });
}

/**
 * returns a promise that is resolved when the download of the requested file
 * is complete and the file is saved.
 * if the download fails, or was stopped the partial file is deleted, and the
 * promise is rejected
 * @param fromUrl   String source url
 * @param toFile    String destination path
 * @param headers   Object headers to use when downloading the file
 * @returns {Promise}
 */
function downloadImage(fromUrl, toFile, headers = {}) {
    // use toFile as the key as is was created using the cacheKey
    if (!_.has(activeDownloads, toFile)) {
        // create an active download for this file
        activeDownloads[toFile] = new Promise((resolve, reject) => {
            RNFatchBlob
                .config({path: toFile})
                .fetch('GET', fromUrl, headers)
                .then(res => resolve(toFile))
                .catch(err =>
                    deleteFile(toFile)
                        .then(() => reject(err))
                )
                .finally(() => {
                    // cleanup
                    delete activeDownloads[toFile];
                });
        });
    }
    return activeDownloads[toFile];
}

function createPrefetcer(list) {
    const urls = _.clone(list);
    return {
        next() {
            return urls.shift();
        }
    };
}

function runPrefetchTask(prefetcher, options) {
    const url = prefetcher.next();
    if (!url) {
        return Promise.resolve();
    }
    // if url is cacheable - cache it
    if (isCacheable(url)) {
        // check cache
        return getCachedImagePath(url, options)
        // if not found download
            .catch(() => cacheImage(url, options))
            // then run next task
            .then(() => runPrefetchTask(prefetcher, options));
    }
    // else get next
    return runPrefetchTask(prefetcher, options);
}

function collectFilesInfo(basePath) {
    return fs.stat(basePath)
        .then((info) => {
            if (info.type === 'file') {
                return [info];
            }
            return fs.ls(basePath)
                .then(files => {
                    const promises = _.map(files, file => {
                        return collectFilesInfo(`${basePath}/${file}`);
                    });
                    return Promise.all(promises);
                });
        })
        .catch(err => {
            return [];
        });
}

// API

/**
 * Check whether a url is cacheable.
 * Takes an image source and if it's a valid url return `true`
 * @param url
 * @returns {boolean}
 */
function isCacheable(url) {
    return _.isString(url) && (_.startsWith(url, 'http://') || _.startsWith(url, 'https://'));
}

/**
 * Get the local path corresponding to the given url and options.
 * @param url
 * @param options
 * @returns {Promise.<String>}
 */
function getCachedImagePath(url, options = defaultOptions) {
    const filePath = getCachedImageFilePath(url, options);
    return fs.stat(filePath)
        .then(res => {
            if (res.type !== 'file') {
                // reject the promise if res is not a file
                throw new Error('Failed to get image from cache');
            }
            if (!res.size) {
                // something went wrong with the download, file size is 0, remove it
                return deleteFile(filePath)
                    .then(() => {
                        throw new Error('Failed to get image from cache');
                    });
            }
            return filePath;
        })
        .catch(err => {
            throw err;
        })
}

/**
 * Download the image to the cache and return the local file path.
 * @param url
 * @param options
 * @param resolveHeaders
 * @returns {Promise.<String>}
 */
function cacheImage(url, options = defaultOptions, resolveHeaders = defaultResolveHeaders) {
    const filePath = getCachedImageFilePath(url, options);
    const dirPath = getDirPath(filePath);
    return ensurePath(dirPath)
        .then(() => resolveHeaders())
        .then(headers => downloadImage(url, filePath, headers));
}

/**
 * Delete the cached image corresponding to the given url and options.
 * @param url
 * @param options
 * @returns {Promise}
 */
function deleteCachedImage(url, options = defaultOptions) {
    const filePath = getCachedImageFilePath(url, options);
    return deleteFile(filePath);
}

/**
 * Cache an array of urls.
 * Usually used to prefetch images.
 * @param urls
 * @param options
 * @returns {Promise}
 */
function cacheMultipleImages(urls, options = defaultOptions) {
    const prefetcher = createPrefetcer(urls);
    const numberOfWorkers = urls.length;
    const promises = _.times(numberOfWorkers, () =>
        runPrefetchTask(prefetcher, options)
    );
    return Promise.all(promises);
}

/**
 * Delete an array of cached images by their urls.
 * Usually used to clear the prefetched images.
 * @param urls
 * @param options
 * @returns {Promise}
 */
function deleteMultipleCachedImages(urls, options = defaultOptions) {
    return _.reduce(urls, (p, url) =>
            p.then(() => deleteCachedImage(url, options)),
        Promise.resolve()
    );
}

/**
* Seed the cache of a specified url with a local image
* Handy if you have a local copy of a remote image, e.g. you just uploaded local to url.
* @param local
* @param url
* @param options
* @returns {Promise}
*/
function seedCache(local, url, options = defaultOptions) {
  const filePath = getCachedImageFilePath(url, options);
  const dirPath = getDirPath(filePath);
  return ensurePath(dirPath)
    .then(() => fs.cp(local, filePath))
}

/**
 * Clear the entire cache.
 * @returns {Promise}
 */
function clearCache() {
    return fs.unlink(baseCacheDir)
        .catch(() => {
            // swallow exceptions if path doesn't exist
        })
        .then(() => ensurePath(baseCacheDir));
}

/**
 * Return info about the cache, list of files and the total size of the cache.
 * @returns {Promise.<{size}>}
 */
function getCacheInfo() {
    return ensurePath(baseCacheDir)
        .then(() => collectFilesInfo(baseCacheDir))
        .then(cache => {
            const files = _.flattenDeep(cache);
            const size = _.sumBy(files, 'size');
            return {
                files,
                size
            };
        });
}

module.exports = {
    isCacheable,
    getCachedImagePath,
    cacheImage,
    deleteCachedImage,
    cacheMultipleImages,
    deleteMultipleCachedImages,
    clearCache,
    seedCache,
    getCacheInfo
};
<commit_msg>properly fail when response status code is not 20*<commit_after>'use strict';

const _ = require('lodash');

const RNFatchBlob = require('react-native-fetch-blob').default;

const {
    fs
} = RNFatchBlob;

const baseCacheDir = fs.dirs.CacheDir + '/imagesCacheDir';

const SHA1 = require("crypto-js/sha1");
const URL = require('url-parse');

const defaultHeaders = {};
const defaultResolveHeaders = _.constant(defaultHeaders);

const defaultOptions = {
    useQueryParamsInCacheKey: false
};

const activeDownloads = {};

function serializeObjectKeys(obj) {
    return _(obj)
        .toPairs()
        .sortBy(a => a[0])
        .map(a => a[1])
        .value();
}

function getQueryForCacheKey(url, useQueryParamsInCacheKey) {
    if (_.isArray(useQueryParamsInCacheKey)) {
        return serializeObjectKeys(_.pick(url.query, useQueryParamsInCacheKey));
    }
    if (useQueryParamsInCacheKey) {
        return serializeObjectKeys(url.query);
    }
    return '';
}

function generateCacheKey(url, options) {
    const parsedUrl = new URL(url, null, true);

    const pathParts = parsedUrl.pathname.split('/');

    // last path part is the file name
    const fileName = pathParts.pop();
    const filePath = pathParts.join('/');

    const parts = fileName.split('.');
    // TODO - try to figure out the file type or let the user provide it, for now use jpg as default
    const type = parts.length > 1 ? parts.pop() : 'jpg';

    const cacheable = filePath + fileName + type + getQueryForCacheKey(parsedUrl, options.useQueryParamsInCacheKey);
    return SHA1(cacheable) + '.' + type;
}

function getCachePath(url, options) {
    if (options.cacheGroup) {
        return options.cacheGroup;
    }
    const {
        host
    } = new URL(url);
    return host.replace(/[^a-z0-9]/gi, '').toLowerCase();
}

function getCachedImageFilePath(url, options) {
    const cachePath = getCachePath(url, options);
    const cacheKey = generateCacheKey(url, options);

    return `${baseCacheDir}/${cachePath}/${cacheKey}`;
}

function deleteFile(filePath) {
    return fs.stat(filePath)
        .then(res => res && res.type === 'file')
        .then(exists => exists && fs.unlink(filePath))
        .catch((err) => {
            // swallow error to always resolve
        });
}

function getDirPath(filePath) {
    return _.initial(filePath.split('/')).join('/');
}

function ensurePath(dirPath) {
    return fs.isDir(dirPath)
        .then(exists =>
            !exists && fs.mkdir(dirPath)
        )
        .catch(err => {
            // swallow folder already exists errors
            if (err.message.includes('folder already exists')) {
                return;
            }
            throw err;
        });
}

/**
 * returns a promise that is resolved when the download of the requested file
 * is complete and the file is saved.
 * if the download fails, or was stopped the partial file is deleted, and the
 * promise is rejected
 * @param fromUrl   String source url
 * @param toFile    String destination path
 * @param headers   Object headers to use when downloading the file
 * @returns {Promise}
 */
function downloadImage(fromUrl, toFile, headers = {}) {
    // use toFile as the key as is was created using the cacheKey
    if (!_.has(activeDownloads, toFile)) {
        // create an active download for this file
        activeDownloads[toFile] = new Promise((resolve, reject) => {
            RNFatchBlob
                .config({path: toFile})
                .fetch('GET', fromUrl, headers)
                .then(res => {
                    if (Math.floor(res.respInfo.status / 100) !== 2) {
                        throw new Error('Failed to successfully download image');
                    }
                    resolve(toFile);
                })
                .catch(err => {
                    return deleteFile(toFile)
                        .then(() => reject(err));
                })
                .finally(() => {
                    // cleanup
                    delete activeDownloads[toFile];
                });
        });
    }
    return activeDownloads[toFile];
}

function createPrefetcer(list) {
    const urls = _.clone(list);
    return {
        next() {
            return urls.shift();
        }
    };
}

function runPrefetchTask(prefetcher, options) {
    const url = prefetcher.next();
    if (!url) {
        return Promise.resolve();
    }
    // if url is cacheable - cache it
    if (isCacheable(url)) {
        // check cache
        return getCachedImagePath(url, options)
        // if not found download
            .catch(() => cacheImage(url, options))
            // allow prefetch task to fail without terminating other prefetch tasks
            .catch(_.noop)
            // then run next task
            .then(() => runPrefetchTask(prefetcher, options));
    }
    // else get next
    return runPrefetchTask(prefetcher, options);
}

function collectFilesInfo(basePath) {
    return fs.stat(basePath)
        .then((info) => {
            if (info.type === 'file') {
                return [info];
            }
            return fs.ls(basePath)
                .then(files => {
                    const promises = _.map(files, file => {
                        return collectFilesInfo(`${basePath}/${file}`);
                    });
                    return Promise.all(promises);
                });
        })
        .catch(err => {
            return [];
        });
}

// API

/**
 * Check whether a url is cacheable.
 * Takes an image source and if it's a valid url return `true`
 * @param url
 * @returns {boolean}
 */
function isCacheable(url) {
    return _.isString(url) && (_.startsWith(url, 'http://') || _.startsWith(url, 'https://'));
}

/**
 * Get the local path corresponding to the given url and options.
 * @param url
 * @param options
 * @returns {Promise.<String>}
 */
function getCachedImagePath(url, options = defaultOptions) {
    const filePath = getCachedImageFilePath(url, options);
    return fs.stat(filePath)
        .then(res => {
            if (res.type !== 'file') {
                // reject the promise if res is not a file
                throw new Error('Failed to get image from cache');
            }
            if (!res.size) {
                // something went wrong with the download, file size is 0, remove it
                return deleteFile(filePath)
                    .then(() => {
                        throw new Error('Failed to get image from cache');
                    });
            }
            return filePath;
        })
        .catch(err => {
            throw err;
        })
}

/**
 * Download the image to the cache and return the local file path.
 * @param url
 * @param options
 * @param resolveHeaders
 * @returns {Promise.<String>}
 */
function cacheImage(url, options = defaultOptions, resolveHeaders = defaultResolveHeaders) {
    const filePath = getCachedImageFilePath(url, options);
    const dirPath = getDirPath(filePath);
    return ensurePath(dirPath)
        .then(() => resolveHeaders())
        .then(headers => downloadImage(url, filePath, headers));
}

/**
 * Delete the cached image corresponding to the given url and options.
 * @param url
 * @param options
 * @returns {Promise}
 */
function deleteCachedImage(url, options = defaultOptions) {
    const filePath = getCachedImageFilePath(url, options);
    return deleteFile(filePath);
}

/**
 * Cache an array of urls.
 * Usually used to prefetch images.
 * @param urls
 * @param options
 * @returns {Promise}
 */
function cacheMultipleImages(urls, options = defaultOptions) {
    const prefetcher = createPrefetcer(urls);
    const numberOfWorkers = urls.length;
    const promises = _.times(numberOfWorkers, () =>
        runPrefetchTask(prefetcher, options)
    );
    return Promise.all(promises);
}

/**
 * Delete an array of cached images by their urls.
 * Usually used to clear the prefetched images.
 * @param urls
 * @param options
 * @returns {Promise}
 */
function deleteMultipleCachedImages(urls, options = defaultOptions) {
    return _.reduce(urls, (p, url) =>
            p.then(() => deleteCachedImage(url, options)),
        Promise.resolve()
    );
}

/**
* Seed the cache of a specified url with a local image
* Handy if you have a local copy of a remote image, e.g. you just uploaded local to url.
* @param local
* @param url
* @param options
* @returns {Promise}
*/
function seedCache(local, url, options = defaultOptions) {
  const filePath = getCachedImageFilePath(url, options);
  const dirPath = getDirPath(filePath);
  return ensurePath(dirPath)
    .then(() => fs.cp(local, filePath))
}

/**
 * Clear the entire cache.
 * @returns {Promise}
 */
function clearCache() {
    return fs.unlink(baseCacheDir)
        .catch(() => {
            // swallow exceptions if path doesn't exist
        })
        .then(() => ensurePath(baseCacheDir));
}

/**
 * Return info about the cache, list of files and the total size of the cache.
 * @returns {Promise.<{size}>}
 */
function getCacheInfo() {
    return ensurePath(baseCacheDir)
        .then(() => collectFilesInfo(baseCacheDir))
        .then(cache => {
            const files = _.flattenDeep(cache);
            const size = _.sumBy(files, 'size');
            return {
                files,
                size
            };
        });
}

module.exports = {
    isCacheable,
    getCachedImagePath,
    cacheImage,
    deleteCachedImage,
    cacheMultipleImages,
    deleteMultipleCachedImages,
    clearCache,
    seedCache,
    getCacheInfo
};
<|endoftext|>


<commit_before>'use strict';

// workflows/createDeployment
var Joi = require('joi');

module.exports = {
  schema: Joi.object({
    deployerId: Joi.string().required(),
    name:       Joi.string().min(1).required(),
  }).required().unknown(true),

  version: '1.0',

  decider: function(args) {
    return {

      createDeploymentDoc: {
        activity: 'createDeploymentDoc',
        input:    (env) => ({}),
        output:   (env) => ({ theId: env.id, newValueThatIWant: env.newValueThatIWant})
      },

      startNewDeployment: {
        dependsOn: ['createDeploymentDoc'],
        input:     (env) => ({ deploymentId: env.theId }),
        workflow:  'startDeployment'
      },

      doSomethingWithDeployment: {
        dependsOn: ['startNewDeployment'],
        input:     (env) => {
          console.log("ENV IS:", env);

          return { theValue: env.newValueThatIWant };
        },
        workflow:  'doSomething'
      },

      setDeploymentStateCreated: {
        dependsOn: ['startNewDeployment'],
        input:     (env) => {
          return { state: 'Running', id: env.theId };
        },
        activity:  'setDeploymentDocState',
      },

    }
  },

  output: function(results) {
    return {
      env: {
        deployment: results.startNewDeployment
      }
    };
  }
};
<commit_msg>Update example<commit_after>'use strict';

// workflows/createDeployment
var Joi = require('joi');

module.exports = {
  schema: Joi.object({
    deployerId: Joi.string().required(),
    name:       Joi.string().min(1).required(),
  }).required().unknown(true),

  version: '1.0',

  decider: function(args) {
    return {

      createDeploymentDoc: {
        activity: 'createDeploymentDoc',
        input:    (env) => ({}),
        output:   (env) => ({ theId: env.id, newValueThatIWant: env.newValueThatIWant })
      },

      startNewDeployment: {
        dependsOn: ['createDeploymentDoc'],
        input:     (env) => ({ deploymentId: env.theId }),
        workflow:  'startDeployment'
      },

      doSomethingWithDeployment: {
        dependsOn: ['startNewDeployment'],
        input:     (env) => {
          console.log("ENV IS:", env);

          return { theValue: env.newValueThatIWant };
        },
        output:    (env) => {
          env.BLAMMO = 'mrow0';
          return { env };
        },
        workflow:  'doSomething'
      },

      setDeploymentStateCreated: {
        dependsOn: ['startNewDeployment'],
        input:     (env) => {
          return { state: 'Running', id: env.theId };
        },
        activity:  'setDeploymentDocState',
      },

    }
  },

  output: function(results) {
    return {
      env: {
        deployment: results.startNewDeployment
      }
    };
  }
};
<|endoftext|>


<commit_before>(function(Resources, vCardParser){

    Resources.QRparser = function(scanData, callback){
            console.log("QRparser");
        if(scanData.match("END:VCARD")){
            console.log("VCARD");
            //VCARD
//            vCardParser(scanData, callback);
            /*
            fn VARCHAR(255) NOT NULL,\n\
            lastname VARCHAR(255) NULL,\n\
            firstname VARCHAR(255) NULL,\n\
            title VARCHAR(255) NULL,\n\
            org VARCHAR(255) NULL,\n\
            email VARCHAR(255) NULL,\n\
            tel VARCHAR(255) NULL,\n\
            adr TEXT NULL,\n\
            postcode VARCHAR(255) NULL,\n\
            street VARCHAR(255) NULL,\n\
            city VARCHAR(255) NULL,\n\
            state VARCHAR(255) NULL,\n\
            postcode VARCHAR(255) NULL,\n\
            country VARCHAR(255) NULL,\n\
            type VARCHAR(255) NULL,\n\
            version VARCHAR(255) NULL,\n\
            */
            var parsedData = {
                fn          : "",
                firstname   : "",
                lastname    : "",
                adr         : "",
                tel         : "",
                cel         : "",
                email       : "",
                website     : ""
            },

            re = /\r\n|\n\r|\n|\r/g,

            ar = scanData.replace(re,"\n").split("\n");

            console.log("ar ar");
            console.log(ar);

            ar.forEach(function(v){
                console.log(parsedData);
                console.log(v);
                var v = v.trim();
                if(v.match(/^N.*:/)){
                    parsedData.fn = v.match(/^N.*:(.*)/)[1];
                    console.log(v.match(/^N.*:(.*)/)[1]);
                    var fnAr = parsedData.fn.split(";");
                    fnAr.forEach(function(vv, k){
                        if(k === 0)
                            parsedData.lastname = vv;
                        if(k === 1)
                            parsedData.firstname = vv;
                    });
                }
                if(v.match(/^TITLE:/))
                    parsedData.title = v.match(/^TITLE:(.*)/)[1];
                if(v.match(/^ORG:/))
                    parsedData.title = v.match(/^ORG:(.*)/)[1];
                if(v.match(/^ADR;/)){
                    parsedData.adr = v.match(/^ADR;.*:;(.*)/)[1];
                    var adrAr = parsedData.adr.split(";");
                    adrAr.forEach(function(vv, k){
                        switch(k){
                            case 1:
                                parsedData.street = vv;
                                break;
                            case 2:
                                parsedData.city = vv;
                                break;
                            case 3:
                                parsedData.state = vv;
                                break;
                            case 4:
                                parsedData.postcode = vv;
                                break;
                            case 5:
                                parsedData.country = vv;
                                break;
                        }
                    });
                }
                if(v.match(/^TEL;/))
                    if(v.match(/CELL;/))
                        parsedData.cel = v.match(/^TEL;CELL;.*:(.*)/)[1];
                    if(v.match(/WORK;/))
                        parsedData.tel = v.match(/^TEL;WORK;.*:(.*)/)[1];
                if(v.match(/^URL;/))
                    parsedData.website = v.match(/^URL;.*:(.*)/)[1];
                if(v.match(/^EMAIL;/))
                    parsedData.email = v.match(/^EMAIL;.*:(.*)/)[1];
            });
            /*
            'BEGIN:VCARD\n\
            VERSION:2.1\n\
            N:Izraylevych;Igor;S.;Mr;PHD\n\
            FN:Mr Igor S. Izraylevych PHD\n\
            TITLE:FOunder\n\
            ORG:Smartsolutions;coding\n\
            ADR;WORK:;Suite 1;Maykovskiy;Zaporizhzhya;ZP;69000;Ukraine\n\
            TEL;WORK;VOICE:31299999\n\
            TEL;WORK;FAX:232\n\
            TEL;CELL;VOICE:0961155555\n\
            URL;WORK:http://website.com\n\
            EMAIL;PREF;INTERNET:igorizr1@gmail.com\n\
            END:VCARD'.replace(re,"\n").split("\n");

            */

        }else if(scanData.match("MECARD:")){
            console.log("MECARD");
            //MECARD
            var parsedData = {
                fn          : "",
                firstname   : "",
                lastname    : "",
                adr         : "",
                tel         : "",
                cel         : "",
                email       : "",
                website     : ""
            };

            var ar = scanData.split(";");
            ar.forEach(function(v){
                if(v.match(/MECARD:N:/)){
                    parsedData.fn = v.match(/MECARD:N:(.*)/)[1];
                    var fnAr = parsedData.fn.split(",");
                    parsedData.firstname = fnAr[0];
                    if(fnAr.length > 1)
                        parsedData.lastname = fnAr[1];
                }
                if(v.match(/TEL:/)){
                    var telmatch = v.match(/TEL:(.*)/)[1];
                    parsedData.tel += (parsedData.tel === "" ? telmatch : (","+telmatch));
                }
                if(v.match(/EMAIL:/)){
                    var emailmatch = v.match(/EMAIL:(.*)/)[1];
                    parsedData.email += (parsedData.email === "" ? emailmatch : (","+emailmatch));
                }
                if(v.match(/URL:/)){
                    var URLmatch = v.match(/URL:(.*)/)[1];
                    parsedData.website += (parsedData.website === "" ? URLmatch : (","+URLmatch));
                }
                if(v.match(/ADR:/)){
                    console.log("match adr");
                    var ADRmatch = v.match(/ADR:(.*)/)[1];
                    parsedData.adr += (parsedData.adr === "" ? ADRmatch : (","+ADRmatch));
                    var addrAR = parsedData.adr.split(",");
                    addrAR.forEach(function(vv, k){
                        if(k === 1)
                            parsedData.street = vv;
                        if(k === 2)
                            parsedData.city = vv;
                        if(k === 3)
                            parsedData.state = vv;
                        if(k === 4)
                            parsedData.postcode = vv;
                        if(k === 5)
                            parsedData.country = vv;
                    });
                }
                if(v.match(/NOTE:/)){
                    var NOTEmatch = v.match(/NOTE:(.*)/)[1],
                        noteAr = NOTEmatch.split(",");
                    noteAr.forEach(function(vv, k){
                        if(k === 0)
                            parsedData.title = noteAr[0];
                        if(k === 1)
                            parsedData.org = noteAr[1];
//                        if(k === 2)
//                            parsedData.pos = noteAr[2];
                    });
                }
            });
            console.log("parsedData Mecard");
            console.log(parsedData);
            callback(parsedData);
        }
    };

}(App.Resources, App.Resources.vCardParser));
/*

MECARD:N:Izraylevych,Igor;TEL:0961155555;TEL:31299999;EMAIL:igorizr1@gmail.com;URL:http://website.com;ADR:Suite 1,Maykovskiy,Zaporizhzhya,ZP,69000,Ukraine;NOTE:FOunder,Smartsolutions,coding;;

BEGIN:VCARD
VERSION:2.1
N:Izraylevych;Igor;S.;Mr;PHD
FN:Mr Igor S. Izraylevych PHD
TITLE:FOunder
ORG:Smartsolutions;coding
ADR;WORK:;Suite 1;Maykovskiy;Zaporizhzhya;ZP;69000;Ukraine
TEL;WORK;VOICE:31299999
TEL;WORK;FAX:232
TEL;CELL;VOICE:0961155555
URL;WORK:http://website.com
EMAIL;PREF;INTERNET:igorizr1@gmail.com
END:VCARD

*/

//[05.03.14, 21:07:34] Timothy Webb: fn -> ‘first name’, ‘last name'
//adr -> ‘street’, ‘city’, ‘state’, ‘post code’, ‘country’.
//missing -> ‘website’, ‘cell’
//[05.03.14, 21:07:43] Timothy Webb: it's ok if you want to also have fn, i don't mind having duplicate data
//[05.03.14, 21:07:50] Timothy Webb: but we need to maintain first name and last name separately<commit_msg>parser test<commit_after>(function(Resources, vCardParser){

    Resources.QRparser = function(scanData, callback){
            console.log("QRparser");
        if(scanData.match("END:VCARD")){
            console.log("VCARD");
            //VCARD
//            vCardParser(scanData, callback);
            /*
            fn VARCHAR(255) NOT NULL,\n\
            lastname VARCHAR(255) NULL,\n\
            firstname VARCHAR(255) NULL,\n\
            title VARCHAR(255) NULL,\n\
            org VARCHAR(255) NULL,\n\
            email VARCHAR(255) NULL,\n\
            tel VARCHAR(255) NULL,\n\
            adr TEXT NULL,\n\
            postcode VARCHAR(255) NULL,\n\
            street VARCHAR(255) NULL,\n\
            city VARCHAR(255) NULL,\n\
            state VARCHAR(255) NULL,\n\
            postcode VARCHAR(255) NULL,\n\
            country VARCHAR(255) NULL,\n\
            type VARCHAR(255) NULL,\n\
            version VARCHAR(255) NULL,\n\
            */
            var parsedData = {
                fn          : "",
                firstname   : "",
                lastname    : "",
                adr         : "",
                tel         : "",
                cel         : "",
                email       : "",
                website     : ""
            },

            re = /\r\n|\n\r|\n|\r/g,

            ar = scanData.replace(re,"\n").split("\n");

            console.log("ar ar");
            console.log(ar);

            ar.forEach(function(v){
                console.log(parsedData);
                console.log(v);
                var v = v.trim();
                if(v.match(/^N.*:/)){
                    console.log("name");
                    console.log(v.match(/^N.*:(.*)/)[1]);
                    parsedData.fn = v.match(/^N.*:(.*)/)[1];
                    var fnAr = parsedData.fn.split(";");
                    fnAr.forEach(function(vv, k){
                        if(k === 0)
                            parsedData.lastname = vv;
                        if(k === 1)
                            parsedData.firstname = vv;
                    });
                    console.log(parsedData);
                }
                if(v.match(/^TITLE.*:/))
                    parsedData.title = v.match(/^TITLE.*:(.*)/)[1];
                if(v.match(/^ORG.*:/))
                    parsedData.title = v.match(/^ORG.*:(.*)/)[1];
                if(v.match(/^ADR;/)){
                    parsedData.adr = v.match(/^ADR.*:;(.*)/)[1];
                    var adrAr = parsedData.adr.split(";");
                    adrAr.forEach(function(vv, k){
                        switch(k){
                            case 1:
                                parsedData.street = vv;
                                break;
                            case 2:
                                parsedData.city = vv;
                                break;
                            case 3:
                                parsedData.state = vv;
                                break;
                            case 4:
                                parsedData.postcode = vv;
                                break;
                            case 5:
                                parsedData.country = vv;
                                break;
                        }
                    });
                }
                if(v.match(/^TEL;/))
                    if(v.match(/CELL.*:/))
                        parsedData.cel = v.match(/^TEL;CELL.*:(.*)/)[1];
                    if(v.match(/WORK.*:/))
                        parsedData.tel = v.match(/^TEL;WORK.*:(.*)/)[1];
                if(v.match(/^URL.*:/))
                    parsedData.website = v.match(/^URL.*:(.*)/)[1];
                if(v.match(/^EMAIL.*:/))
                    parsedData.email = v.match(/^EMAIL.*:(.*)/)[1];
            });
            /*

            BEGIN:VCARD
            N;CHARSET=utf-8:Izraylevych;Igor;;;
            FN;CHARSET=utf-8:Igor Izraylevych
            ORG;CHARSET=utf-8:org
            TITLE;CHARSET=utf-8:developer
            TEL;WORK:343967
            TEL;CELL:0961155555
            TEL;WORK;FAX:2323
            EMAIL;INTERNET;WORK;CHARSET=utf-8:igorizr1@gmail.com
            ADR;WORK;CHARSET=utf-8:;;Prvdy 3;Zaporizhzhya;Zp;69000;Ukraine
            URL;WORK;CHARSET=utf-8:http://2727 Duke st
            VERSION:2.1
            END:VCARD

            'BEGIN:VCARD\n\
            VERSION:2.1\n\
            N:Izraylevych;Igor;S.;Mr;PHD\n\
            FN:Mr Igor S. Izraylevych PHD\n\
            TITLE:FOunder\n\
            ORG:Smartsolutions;coding\n\
            ADR;WORK:;Suite 1;Maykovskiy;Zaporizhzhya;ZP;69000;Ukraine\n\
            TEL;WORK;VOICE:31299999\n\
            TEL;WORK;FAX:232\n\
            TEL;CELL;VOICE:0961155555\n\
            URL;WORK:http://website.com\n\
            EMAIL;PREF;INTERNET:igorizr1@gmail.com\n\
            END:VCARD'.replace(re,"\n").split("\n");

            */
           console.log(parsedData);
           callback(parsedData);

        }else if(scanData.match("MECARD:")){
            console.log("MECARD");
            //MECARD
            var parsedData = {
                fn          : "",
                firstname   : "",
                lastname    : "",
                adr         : "",
                tel         : "",
                cel         : "",
                email       : "",
                website     : ""
            };

            var ar = scanData.split(";");
            ar.forEach(function(v){
                if(v.match(/MECARD:N:/)){
                    parsedData.fn = v.match(/MECARD:N:(.*)/)[1];
                    var fnAr = parsedData.fn.split(",");
                    parsedData.firstname = fnAr[0];
                    if(fnAr.length > 1)
                        parsedData.lastname = fnAr[1];
                }
                if(v.match(/TEL:/)){
                    var telmatch = v.match(/TEL:(.*)/)[1];
                    parsedData.tel += (parsedData.tel === "" ? telmatch : (","+telmatch));
                }
                if(v.match(/EMAIL:/)){
                    var emailmatch = v.match(/EMAIL:(.*)/)[1];
                    parsedData.email += (parsedData.email === "" ? emailmatch : (","+emailmatch));
                }
                if(v.match(/URL:/)){
                    var URLmatch = v.match(/URL:(.*)/)[1];
                    parsedData.website += (parsedData.website === "" ? URLmatch : (","+URLmatch));
                }
                if(v.match(/ADR:/)){
                    console.log("match adr");
                    var ADRmatch = v.match(/ADR:(.*)/)[1];
                    parsedData.adr += (parsedData.adr === "" ? ADRmatch : (","+ADRmatch));
                    var addrAR = parsedData.adr.split(",");
                    addrAR.forEach(function(vv, k){
                        if(k === 1)
                            parsedData.street = vv;
                        if(k === 2)
                            parsedData.city = vv;
                        if(k === 3)
                            parsedData.state = vv;
                        if(k === 4)
                            parsedData.postcode = vv;
                        if(k === 5)
                            parsedData.country = vv;
                    });
                }
                if(v.match(/NOTE:/)){
                    var NOTEmatch = v.match(/NOTE:(.*)/)[1],
                        noteAr = NOTEmatch.split(",");
                    noteAr.forEach(function(vv, k){
                        if(k === 0)
                            parsedData.title = noteAr[0];
                        if(k === 1)
                            parsedData.org = noteAr[1];
//                        if(k === 2)
//                            parsedData.pos = noteAr[2];
                    });
                }
            });
            console.log("parsedData Mecard");
            console.log(parsedData);
            callback(parsedData);
        }
    };

}(App.Resources, App.Resources.vCardParser));
/*

MECARD:N:Izraylevych,Igor;TEL:0961155555;TEL:31299999;EMAIL:igorizr1@gmail.com;URL:http://website.com;ADR:Suite 1,Maykovskiy,Zaporizhzhya,ZP,69000,Ukraine;NOTE:FOunder,Smartsolutions,coding;;

BEGIN:VCARD
VERSION:2.1
N:Izraylevych;Igor;S.;Mr;PHD
FN:Mr Igor S. Izraylevych PHD
TITLE:FOunder
ORG:Smartsolutions;coding
ADR;WORK:;Suite 1;Maykovskiy;Zaporizhzhya;ZP;69000;Ukraine
TEL;WORK;VOICE:31299999
TEL;WORK;FAX:232
TEL;CELL;VOICE:0961155555
URL;WORK:http://website.com
EMAIL;PREF;INTERNET:igorizr1@gmail.com
END:VCARD

*/

//[05.03.14, 21:07:34] Timothy Webb: fn -> ‘first name’, ‘last name'
//adr -> ‘street’, ‘city’, ‘state’, ‘post code’, ‘country’.
//missing -> ‘website’, ‘cell’
//[05.03.14, 21:07:43] Timothy Webb: it's ok if you want to also have fn, i don't mind having duplicate data
//[05.03.14, 21:07:50] Timothy Webb: but we need to maintain first name and last name separately<|endoftext|>


<commit_before>var Class = require('../utils/Class');
var Extend = require('../utils/object/Extend');
var EventDispatcher = require('../events/EventDispatcher');
var NOOP = require('../utils/NOOP');
var SoundValueEvent = require('./SoundValueEvent');
/*!
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
var BaseSound = new Class({
    /**
     * @class Phaser.Sound.BaseSound
     * @constructor
     * @param {ISoundManager} 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 BaseSound(manager, key, config) {
        /**
         * Local reference to the sound manager.
         *
         * @private
         * @property {Phaser.Sound.BaseSoundManager} manager
         */
        this.manager = manager;
        /**
         * Asset key for the sound.
         *
         * @readonly
         * @property {string} key
         */
        this.key = key;
        /**
         * Event dispatcher used to handle all sound instance related events.
         *
         * @readonly
         * @property {Phaser.Events.EventDispatcher}
         */
        this.events = new EventDispatcher();
        /**
         * Flag indicating if sound is currently playing.
         *
         * @readonly
         * @property {boolean} isPlaying
         * @default false
         */
        this.isPlaying = false;
        /**
         * Flag indicating if sound is currently paused.
         *
         * @readonly
         * @property {boolean} isPaused
         * @default false
         */
        this.isPaused = false;
        /**
         * A property that holds the value of sound's actual playback rate,
         * after its rate and detune values has been combined with global
         * rate and detune values.
         *
         * @readonly
         * @property {number} totalRate
         * @default 1
         */
        this.totalRate = 1;
        /**
         * A value representing the duration, in seconds.
         * It could be total sound duration or a marker duration.
         *
         * @readonly
         * @property {number} duration
         */
        this.duration = this.duration || 0;
        /**
         * The total duration of the sound in seconds.
         *
         * @readonly
         * @property {number}
         */
        this.totalDuration = this.totalDuration || 0;
        /**
         * A config object used to store default sound settings' values.
         * Default values will be set by properties' setters.
         *
         * @private
         * @property {ISoundConfig} config
         */
        this.config = {
            /**
             * Initializing delay config setting
             */
            delay: 0
        };
        /**
         * Reference to the currently used config.
         * It could be default config or marker config.
         *
         * @private
         * @property {ISoundConfig} currentConfig
         */
        this.currentConfig = this.config;
        /**
         * Boolean indicating whether the sound is muted or not.
         * Gets or sets the muted state of this sound.
         *
         * @property {boolean} mute
         * @default false
         */
        this.mute = false;
        /**
         * Gets or sets the volume of this sound,
         * a value between 0 (silence) and 1 (full volume).
         *
         * @property {number} volume
         * @default 1
         */
        this.volume = 1;
        /**
         * Defines the speed at which the audio asset will be played.
         * Value of 1.0 plays the audio at full speed, 0.5 plays the audio
         * at half speed and 2.0 doubles the audio's playback speed.
         * This value gets multiplied by global rate to have the final playback speed.
         *
         * @property {number} rate
         * @default 1
         */
        this.rate = 1;
        /**
         * Represents detuning of sound in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29).
         * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent).
         *
         * @property {number} detune
         * @default 0
         */
        this.detune = 0;
        /**
         * Property representing the position of playback for this sound, in seconds.
         * Setting it to a specific value moves current playback to that position.
         * The value given is clamped to the range 0 to current marker duration.
         * Setting seek of a stopped sound has no effect.
         *
         * @property {number} seek
         * @default 0
         */
        this.seek = 0;
        /**
         * Flag indicating whether or not the sound or current sound marker will loop.
         *
         * @property {boolean} loop
         * @default false
         */
        this.loop = false;
        this.config = Extend(this.config, config);
        /**
         * Object containing markers definitions.
         *
         * @readonly
         * @property {Object.<string, ISoundMarker>} markers
         * @default {}
         */
        this.markers = {};
        /**
         * Currently playing marker.
         * 'null' if whole sound is playing.
         *
         * @readonly
         * @property {ISoundMarker} currentMarker
         * @default null
         */
        this.currentMarker = null;
        /**
         * Flag indicating if destroy method was called on this sound.
         *
         * @private
         * @property {boolean} pendingRemove
         * @default false
         */
        this.pendingRemove = false;
    },
    /**
     * Adds a marker into the current sound. A marker is represented by name, start time, duration, and optionally config object.
     * This allows you to bundle multiple sounds together into a single audio file and use markers to jump between them for playback.
     *
     * @method Phaser.Sound.BaseSound#addMarker
     * @param {ISoundMarker} marker - Marker object
     * @returns {boolean} Whether the marker was added successfully
     */
    addMarker: function (marker) {
        if (!marker) {
            console.error('addMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('addMarker - Marker has to have a valid name!');
            return false;
        }
        if (this.markers[marker.name]) {
            console.error('addMarker - Marker with name \'' + marker.name + '\' already exists for sound \'' + this.key + '\'!');
            return false;
        }
        marker = Extend(true, {
            name: '',
            start: 0,
            duration: this.totalDuration,
            config: {
                mute: false,
                volume: 1,
                rate: 1,
                detune: 0,
                seek: 0,
                loop: false,
                delay: 0
            }
        }, marker);
        this.markers[marker.name] = marker;
        return true;
    },
    /**
     * Updates previously added marker.
     *
     * @method Phaser.Sound.BaseSound#updateMarker
     * @param {ISoundMarker} marker - Marker object with updated values.
     * @returns {boolean} Whether the marker was updated successfully.
     */
    updateMarker: function (marker) {
        if (!marker) {
            console.error('updateMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('updateMarker - Marker has to have a valid name!');
            return false;
        }
        if (!this.markers[marker.name]) {
            console.error('updateMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return false;
        }
        this.markers[marker.name] = Extend(true, this.markers[marker.name], marker);
        return true;
    },
    /**
     * Removes a marker from the sound.
     *
     * @method Phaser.Sound.BaseSound#removeMarker
     * @param {string} markerName - The name of the marker to remove.
     * @returns {ISoundMarker | null} Removed marker object or 'null' if there was no marker with provided name.
     */
    removeMarker: function (markerName) {
        var marker = this.markers[markerName];
        if (!marker) {
            console.error('removeMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return null;
        }
        this.markers[markerName] = null;
        return marker;
    },
    /**
     * 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.BaseSound#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 {boolean} Whether the sound started playing successfully.
     */
    play: function (markerName, config) {
        if (markerName === void 0) { markerName = ''; }
        if (typeof markerName === 'object') {
            config = markerName;
            markerName = '';
        }
        if (typeof markerName !== 'string') {
            console.error('Sound marker name has to be a string!');
            return false;
        }
        if (!markerName) {
            this.currentMarker = null;
            this.currentConfig = this.config;
            this.duration = this.totalDuration;
        }
        else {
            if (!this.markers[markerName]) {
                console.error('No marker with name \'' + markerName + '\' found for sound \'' + this.key + '\'!');
                return false;
            }
            this.currentMarker = this.markers[markerName];
            this.currentConfig = this.currentMarker.config;
            this.duration = this.currentMarker.duration;
        }
        this.resetConfig();
        this.currentConfig = Extend(this.currentConfig, config);
        this.isPlaying = true;
        this.isPaused = false;
        return true;
    },
    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.BaseSound#pause
     * @returns {boolean} Whether the sound was paused successfully.
     */
    pause: function () {
        if (this.isPaused || !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = true;
        return true;
    },
    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.BaseSound#resume
     * @returns {boolean} Whether the sound was resumed successfully.
     */
    resume: function () {
        if (!this.isPaused || this.isPlaying) {
            return false;
        }
        this.isPlaying = true;
        this.isPaused = false;
        return true;
    },
    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.BaseSound#stop
     * @returns {boolean} Whether the sound was stopped successfully.
     */
    stop: function () {
        if (!this.isPaused && !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = false;
        this.resetConfig();
        return true;
    },
    /**
     * Method used internally for applying config values to some of the sound properties.
     *
     * @protected
     * @method Phaser.Sound.BaseSound#applyConfig
     */
    applyConfig: function () {
        this.mute = this.currentConfig.mute;
        this.volume = this.currentConfig.volume;
        this.rate = this.currentConfig.rate;
        this.detune = this.currentConfig.detune;
        this.loop = this.currentConfig.loop;
    },
    /**
     * Method used internally for resetting values of some of the config properties.
     *
     * @protected
     * @method Phaser.Sound.BaseSound#resetConfig
     */
    resetConfig: function () {
        this.currentConfig.seek = 0;
        this.currentConfig.delay = 0;
    },
    /**
     * Update method called automatically by sound manager on every game step.
     *
     * @override
     * @protected
     * @method Phaser.Sound.BaseSound#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: NOOP,
    /**
     * Destroys this sound and all associated events and marks it for removal from the sound manager.
     *
     * @method Phaser.Sound.BaseSound#destroy
     */
    destroy: function () {
        if (this.pendingRemove) {
            return;
        }
        this.pendingRemove = true;
        this.manager = null;
        this.key = '';
        this.events.destroy();
        this.events = null;
        this.isPlaying = false;
        this.isPaused = false;
        this.config = null;
        this.currentConfig = null;
        this.markers = null;
        this.currentMarker = null;
    },
    /**
     * Method used internally to calculate total playback rate of the sound.
     *
     * @protected
     * @method Phaser.Sound.BaseSound#setRate
     */
    setRate: function () {
        var cent = 1.0005777895065548; // Math.pow(2, 1/1200);
        var totalDetune = this.currentConfig.detune + this.manager.detune;
        var detuneRate = Math.pow(cent, totalDetune);
        this.totalRate = this.currentConfig.rate * this.manager.rate * detuneRate;
    }
});
/**
 * Playback rate.
 *
 * @name Phaser.Sound.BaseSound#rate
 * @property {number} rate
 */
Object.defineProperty(BaseSound.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));
    }
});
module.exports = BaseSound;
<commit_msg>Defining detune property on a BaseSound class since it's the shared behaviour between subclasses<commit_after>var Class = require('../utils/Class');
var Extend = require('../utils/object/Extend');
var EventDispatcher = require('../events/EventDispatcher');
var NOOP = require('../utils/NOOP');
var SoundValueEvent = require('./SoundValueEvent');
/*!
 * @author Pavle Goloskokovic <pgoloskokovic@gmail.com> (http://prunegames.com)
 */
var BaseSound = new Class({
    /**
     * @class Phaser.Sound.BaseSound
     * @constructor
     * @param {ISoundManager} 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 BaseSound(manager, key, config) {
        /**
         * Local reference to the sound manager.
         *
         * @private
         * @property {Phaser.Sound.BaseSoundManager} manager
         */
        this.manager = manager;
        /**
         * Asset key for the sound.
         *
         * @readonly
         * @property {string} key
         */
        this.key = key;
        /**
         * Event dispatcher used to handle all sound instance related events.
         *
         * @readonly
         * @property {Phaser.Events.EventDispatcher}
         */
        this.events = new EventDispatcher();
        /**
         * Flag indicating if sound is currently playing.
         *
         * @readonly
         * @property {boolean} isPlaying
         * @default false
         */
        this.isPlaying = false;
        /**
         * Flag indicating if sound is currently paused.
         *
         * @readonly
         * @property {boolean} isPaused
         * @default false
         */
        this.isPaused = false;
        /**
         * A property that holds the value of sound's actual playback rate,
         * after its rate and detune values has been combined with global
         * rate and detune values.
         *
         * @readonly
         * @property {number} totalRate
         * @default 1
         */
        this.totalRate = 1;
        /**
         * A value representing the duration, in seconds.
         * It could be total sound duration or a marker duration.
         *
         * @readonly
         * @property {number} duration
         */
        this.duration = this.duration || 0;
        /**
         * The total duration of the sound in seconds.
         *
         * @readonly
         * @property {number}
         */
        this.totalDuration = this.totalDuration || 0;
        /**
         * A config object used to store default sound settings' values.
         * Default values will be set by properties' setters.
         *
         * @private
         * @property {ISoundConfig} config
         */
        this.config = {
            /**
             * Initializing delay config setting
             */
            delay: 0
        };
        /**
         * Reference to the currently used config.
         * It could be default config or marker config.
         *
         * @private
         * @property {ISoundConfig} currentConfig
         */
        this.currentConfig = this.config;
        /**
         * Boolean indicating whether the sound is muted or not.
         * Gets or sets the muted state of this sound.
         *
         * @property {boolean} mute
         * @default false
         */
        this.mute = false;
        /**
         * Gets or sets the volume of this sound,
         * a value between 0 (silence) and 1 (full volume).
         *
         * @property {number} volume
         * @default 1
         */
        this.volume = 1;
        /**
         * Defines the speed at which the audio asset will be played.
         * Value of 1.0 plays the audio at full speed, 0.5 plays the audio
         * at half speed and 2.0 doubles the audio's playback speed.
         * This value gets multiplied by global rate to have the final playback speed.
         *
         * @property {number} rate
         * @default 1
         */
        this.rate = 1;
        /**
         * Represents detuning of sound in [cents](https://en.wikipedia.org/wiki/Cent_%28music%29).
         * The range of the value is -1200 to 1200, but we recommend setting it to [50](https://en.wikipedia.org/wiki/50_Cent).
         *
         * @property {number} detune
         * @default 0
         */
        this.detune = 0;
        /**
         * Property representing the position of playback for this sound, in seconds.
         * Setting it to a specific value moves current playback to that position.
         * The value given is clamped to the range 0 to current marker duration.
         * Setting seek of a stopped sound has no effect.
         *
         * @property {number} seek
         * @default 0
         */
        this.seek = 0;
        /**
         * Flag indicating whether or not the sound or current sound marker will loop.
         *
         * @property {boolean} loop
         * @default false
         */
        this.loop = false;
        this.config = Extend(this.config, config);
        /**
         * Object containing markers definitions.
         *
         * @readonly
         * @property {Object.<string, ISoundMarker>} markers
         * @default {}
         */
        this.markers = {};
        /**
         * Currently playing marker.
         * 'null' if whole sound is playing.
         *
         * @readonly
         * @property {ISoundMarker} currentMarker
         * @default null
         */
        this.currentMarker = null;
        /**
         * Flag indicating if destroy method was called on this sound.
         *
         * @private
         * @property {boolean} pendingRemove
         * @default false
         */
        this.pendingRemove = false;
    },
    /**
     * Adds a marker into the current sound. A marker is represented by name, start time, duration, and optionally config object.
     * This allows you to bundle multiple sounds together into a single audio file and use markers to jump between them for playback.
     *
     * @method Phaser.Sound.BaseSound#addMarker
     * @param {ISoundMarker} marker - Marker object
     * @returns {boolean} Whether the marker was added successfully
     */
    addMarker: function (marker) {
        if (!marker) {
            console.error('addMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('addMarker - Marker has to have a valid name!');
            return false;
        }
        if (this.markers[marker.name]) {
            console.error('addMarker - Marker with name \'' + marker.name + '\' already exists for sound \'' + this.key + '\'!');
            return false;
        }
        marker = Extend(true, {
            name: '',
            start: 0,
            duration: this.totalDuration,
            config: {
                mute: false,
                volume: 1,
                rate: 1,
                detune: 0,
                seek: 0,
                loop: false,
                delay: 0
            }
        }, marker);
        this.markers[marker.name] = marker;
        return true;
    },
    /**
     * Updates previously added marker.
     *
     * @method Phaser.Sound.BaseSound#updateMarker
     * @param {ISoundMarker} marker - Marker object with updated values.
     * @returns {boolean} Whether the marker was updated successfully.
     */
    updateMarker: function (marker) {
        if (!marker) {
            console.error('updateMarker - Marker object has to be provided!');
            return false;
        }
        if (!marker.name || typeof marker.name !== 'string') {
            console.error('updateMarker - Marker has to have a valid name!');
            return false;
        }
        if (!this.markers[marker.name]) {
            console.error('updateMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return false;
        }
        this.markers[marker.name] = Extend(true, this.markers[marker.name], marker);
        return true;
    },
    /**
     * Removes a marker from the sound.
     *
     * @method Phaser.Sound.BaseSound#removeMarker
     * @param {string} markerName - The name of the marker to remove.
     * @returns {ISoundMarker | null} Removed marker object or 'null' if there was no marker with provided name.
     */
    removeMarker: function (markerName) {
        var marker = this.markers[markerName];
        if (!marker) {
            console.error('removeMarker - Marker with name \'' + marker.name + '\' does not exist for sound \'' + this.key + '\'!');
            return null;
        }
        this.markers[markerName] = null;
        return marker;
    },
    /**
     * 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.BaseSound#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 {boolean} Whether the sound started playing successfully.
     */
    play: function (markerName, config) {
        if (markerName === void 0) { markerName = ''; }
        if (typeof markerName === 'object') {
            config = markerName;
            markerName = '';
        }
        if (typeof markerName !== 'string') {
            console.error('Sound marker name has to be a string!');
            return false;
        }
        if (!markerName) {
            this.currentMarker = null;
            this.currentConfig = this.config;
            this.duration = this.totalDuration;
        }
        else {
            if (!this.markers[markerName]) {
                console.error('No marker with name \'' + markerName + '\' found for sound \'' + this.key + '\'!');
                return false;
            }
            this.currentMarker = this.markers[markerName];
            this.currentConfig = this.currentMarker.config;
            this.duration = this.currentMarker.duration;
        }
        this.resetConfig();
        this.currentConfig = Extend(this.currentConfig, config);
        this.isPlaying = true;
        this.isPaused = false;
        return true;
    },
    /**
     * Pauses the sound.
     *
     * @method Phaser.Sound.BaseSound#pause
     * @returns {boolean} Whether the sound was paused successfully.
     */
    pause: function () {
        if (this.isPaused || !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = true;
        return true;
    },
    /**
     * Resumes the sound.
     *
     * @method Phaser.Sound.BaseSound#resume
     * @returns {boolean} Whether the sound was resumed successfully.
     */
    resume: function () {
        if (!this.isPaused || this.isPlaying) {
            return false;
        }
        this.isPlaying = true;
        this.isPaused = false;
        return true;
    },
    /**
     * Stop playing this sound.
     *
     * @method Phaser.Sound.BaseSound#stop
     * @returns {boolean} Whether the sound was stopped successfully.
     */
    stop: function () {
        if (!this.isPaused && !this.isPlaying) {
            return false;
        }
        this.isPlaying = false;
        this.isPaused = false;
        this.resetConfig();
        return true;
    },
    /**
     * Method used internally for applying config values to some of the sound properties.
     *
     * @protected
     * @method Phaser.Sound.BaseSound#applyConfig
     */
    applyConfig: function () {
        this.mute = this.currentConfig.mute;
        this.volume = this.currentConfig.volume;
        this.rate = this.currentConfig.rate;
        this.detune = this.currentConfig.detune;
        this.loop = this.currentConfig.loop;
    },
    /**
     * Method used internally for resetting values of some of the config properties.
     *
     * @protected
     * @method Phaser.Sound.BaseSound#resetConfig
     */
    resetConfig: function () {
        this.currentConfig.seek = 0;
        this.currentConfig.delay = 0;
    },
    /**
     * Update method called automatically by sound manager on every game step.
     *
     * @override
     * @protected
     * @method Phaser.Sound.BaseSound#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: NOOP,
    /**
     * Destroys this sound and all associated events and marks it for removal from the sound manager.
     *
     * @method Phaser.Sound.BaseSound#destroy
     */
    destroy: function () {
        if (this.pendingRemove) {
            return;
        }
        this.pendingRemove = true;
        this.manager = null;
        this.key = '';
        this.events.destroy();
        this.events = null;
        this.isPlaying = false;
        this.isPaused = false;
        this.config = null;
        this.currentConfig = null;
        this.markers = null;
        this.currentMarker = null;
    },
    /**
     * Method used internally to calculate total playback rate of the sound.
     *
     * @protected
     * @method Phaser.Sound.BaseSound#setRate
     */
    setRate: function () {
        var cent = 1.0005777895065548; // Math.pow(2, 1/1200);
        var totalDetune = this.currentConfig.detune + this.manager.detune;
        var detuneRate = Math.pow(cent, totalDetune);
        this.totalRate = this.currentConfig.rate * this.manager.rate * detuneRate;
    }
});
/**
 * Playback rate.
 *
 * @name Phaser.Sound.BaseSound#rate
 * @property {number} rate
 */
Object.defineProperty(BaseSound.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.
 *
 * @name Phaser.Sound.BaseSound#detune
 * @property {number} detune
 */
Object.defineProperty(BaseSound.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));
    }
});
module.exports = BaseSound;
<|endoftext|>