<commit_before>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

// for ES6 stuff like startsWith() that Safari doesn't handle
// and babel doesn't do by default
// Note we use this, as well as the babel transform-runtime plugin
// since transform-runtime does not cover instance methods
// such as "foobar".includes("foo") which bits of our library
// code use, but the babel transform-runtime plugin allows the
// regenerator runtime to be injected early enough in the process
// (it can't be here as it's too late: the alternative is to put
// the babel-polyfill as the first 'entry' in the webpack config).
// https://babeljs.io/docs/plugins/transform-runtime/
require('babel-polyfill');

// Require common CSS here; this will make webpack process it into bundle.css.
// Our own CSS (which is themed) is imported via separate webpack entry points
// in webpack.config.js
require('gemini-scrollbar/gemini-scrollbar.css');
require('gfm.css/gfm.css');
require('highlight.js/styles/github.css');
require('draft-js/dist/Draft.css');

const rageshake = require("./rageshake");
rageshake.init().then(() => {
    console.log("Initialised rageshake: See https://bugs.chromium.org/p/chromium/issues/detail?id=583193 to fix line numbers on Chrome.");
    rageshake.cleanup();
}, (err) => {
    console.error("Failed to initialise rageshake: " + err);
});

window.addEventListener('beforeunload', (e) => {
    console.log('riot-web closing');
    // try to flush the logs to indexeddb
    rageshake.flush();
});

 // add React and ReactPerf to the global namespace, to make them easier to
 // access via the console
global.React = require("react");
if (process.env.NODE_ENV !== 'production') {
    global.Perf = require("react-addons-perf");
}

var RunModernizrTests = require("./modernizr"); // this side-effects a global
var ReactDOM = require("react-dom");
var sdk = require("matrix-react-sdk");
const PlatformPeg = require("matrix-react-sdk/lib/PlatformPeg");
sdk.loadSkin(require('../component-index'));
var VectorConferenceHandler = require('../VectorConferenceHandler');
import Promise from 'bluebird';
var request = require('browser-request');
import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
// Also import _t directly so we can call it just `_t` as this is what gen-i18n.js expects
import { _t } from 'matrix-react-sdk/lib/languageHandler';

import url from 'url';

import {parseQs, parseQsFromFragment} from './url_utils';
import Platform from './platform';

import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import SettingsStore, {SettingLevel} from "matrix-react-sdk/lib/settings/SettingsStore";
import Tinter from 'matrix-react-sdk/lib/Tinter';
import SdkConfig from "matrix-react-sdk/lib/SdkConfig";

var lastLocationHashSet = null;

var CallHandler = require("matrix-react-sdk/lib/CallHandler");
CallHandler.setConferenceHandler(VectorConferenceHandler);

MatrixClientPeg.setIndexedDbWorkerScript(window.vector_indexeddb_worker_script);

function checkBrowserFeatures(featureList) {
    if (!window.Modernizr) {
        console.error("Cannot check features - Modernizr global is missing.");
        return false;
    }
    var featureComplete = true;
    for (var i = 0; i < featureList.length; i++) {
        if (window.Modernizr[featureList[i]] === undefined) {
            console.error(
                "Looked for feature '%s' but Modernizr has no results for this. " +
                "Has it been configured correctly?", featureList[i]
            );
            return false;
        }
        if (window.Modernizr[featureList[i]] === false) {
            console.error("Browser missing feature: '%s'", featureList[i]);
            // toggle flag rather than return early so we log all missing features
            // rather than just the first.
            featureComplete = false;
        }
    }
    return featureComplete;
}

var validBrowser = checkBrowserFeatures([
    "displaytable", "flexbox", "es5object", "es5function", "localstorage",
    "objectfit", "indexeddb", "webworkers",
]);

// Parse the given window.location and return parameters that can be used when calling
// MatrixChat.showScreen(screen, params)
function getScreenFromLocation(location) {
    const fragparts = parseQsFromFragment(location);
    return {
        screen: fragparts.location.substring(1),
        params: fragparts.params,
    }
}

// Here, we do some crude URL analysis to allow
// deep-linking.
function routeUrl(location) {
    if (!window.matrixChat) return;

    console.log("Routing URL ", location.href);
    const s = getScreenFromLocation(location);
    window.matrixChat.showScreen(s.screen, s.params);
}

function onHashChange(ev) {
    if (decodeURIComponent(window.location.hash) == lastLocationHashSet) {
        // we just set this: no need to route it!
        return;
    }
    routeUrl(window.location);
}

// This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately.
var onNewScreen = function(screen) {
    console.log("newscreen "+screen);
    var hash = '#/' + screen;
    lastLocationHashSet = hash;
    window.location.hash = hash;
};

// We use this to work out what URL the SDK should
// pass through when registering to allow the user to
// click back to the client having registered.
// It's up to us to recognise if we're loaded with
// this URL and tell MatrixClient to resume registration.
//
// If we're in electron, we should never pass through a file:// URL otherwise
// the identity server will try to 302 the browser to it, which breaks horribly.
// so in that instance, hardcode to use riot.im/app for now instead.
var makeRegistrationUrl = function(params) {
    let url;
    if (window.location.protocol === "file:") {
        url = 'https://riot.im/app/#/register';
    } else {
        url = (
            window.location.protocol + '//' +
            window.location.host +
            window.location.pathname +
            '#/register'
        );
    }

    const keys = Object.keys(params);
    for (let i = 0; i < keys.length; ++i) {
        if (i == 0) {
            url += '?';
        } else {
            url += '&';
        }
        const k = keys[i];
        url += k + '=' + encodeURIComponent(params[k]);
    }
    return url;
}

window.addEventListener('hashchange', onHashChange);

function getConfig(configJsonFilename) {
    let deferred = Promise.defer();

    request(
        { method: "GET", url: configJsonFilename },
        (err, response, body) => {
            if (err || response.status < 200 || response.status >= 300) {
                // Lack of a config isn't an error, we should
                // just use the defaults.
                // Also treat a blank config as no config, assuming
                // the status code is 0, because we don't get 404s
                // from file: URIs so this is the only way we can
                // not fail if the file doesn't exist when loading
                // from a file:// URI.
                if (response) {
                    if (response.status == 404 || (response.status == 0 && body == '')) {
                        deferred.resolve({});
                    }
                }
                deferred.reject({err: err, response: response});
                return;
            }

            // We parse the JSON ourselves rather than use the JSON
            // parameter, since this throws a parse error on empty
            // which breaks if there's no config.json and we're
            // loading from the filesystem (see above).
            deferred.resolve(JSON.parse(body));
        }
    );

    return deferred.promise;
}

function onTokenLoginCompleted() {
    // if we did a token login, we're now left with the token, hs and is
    // url as query params in the url; a little nasty but let's redirect to
    // clear them.
    var parsedUrl = url.parse(window.location.href);
    parsedUrl.search = "";
    var formatted = url.format(parsedUrl);
    console.log("Redirecting to " + formatted + " to drop loginToken " +
                "from queryparams");
    window.location.href = formatted;
}

async function loadApp() {
    await loadLanguage();

    const fragparts = parseQsFromFragment(window.location);
    const params = parseQs(window.location);

    // set the platform for react sdk (our Platform object automatically picks the right one)
    PlatformPeg.set(new Platform());

    // don't try to redirect to the native apps if we're
    // verifying a 3pid
    const preventRedirect = Boolean(fragparts.params.client_secret);

    if (!preventRedirect) {
        if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
            // FIXME: ugly status hardcoding
            if (SettingsStore.getValue("theme") === 'status') {
                window.location = "https://status.im/join-riot.html";
                return;
            }
            else {
                if (confirm(_t("Riot is not supported on mobile web. Install the app?"))) {
                    window.location = "https://itunes.apple.com/us/app/vector.im/id1083446067";
                    return;
                }
            }
        }
        else if (/Android/.test(navigator.userAgent)) {
            // FIXME: ugly status hardcoding
            if (SettingsStore.getValue("theme") === 'status') {
                window.location = "https://status.im/join-riot.html";
                return;
            }
            else {
                if (confirm(_t("Riot is not supported on mobile web. Install the app?"))) {
                    window.location = "https://play.google.com/store/apps/details?id=im.vector.alpha";
                    return;
                }
            }
        }
    }

    // Load the config file. First try to load up a domain-specific config of the
    // form "config.$domain.json" and if that fails, fall back to config.json.
    let configJson;
    let configError;
    try {
        try {
            configJson = await getConfig(`config.${document.domain}.json`);
            // 404s succeed with an empty json config, so check that there are keys
            if (Object.keys(configJson).length === 0) {
                throw new Error(); // throw to enter the catch
            }
        } catch (e) {
            configJson = await getConfig("config.json");
        }
    } catch (e) {
        configError = e;
    }
    SdkConfig.put(configJson);

    // as quickly as we possibly can, set a default theme...
    const styleElements = Object.create(null);
    let a;
    const theme = SettingsStore.getValue("theme");
    for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
        const href = a.getAttribute("href");
        if (!href) continue;
        // shouldn't we be using the 'title' tag rather than the href?
        const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
        if (match) {
            if (match[1] === theme) {
                // remove the disabled flag off the stylesheet
                a.removeAttribute("disabled");

                // in case the Tinter.tint() in MatrixChat fires before the
                // CSS has actually loaded (which in practice happens)...

                // FIXME: we should probably block loading the app or even
                // showing a spinner until the theme is loaded, to avoid
                // flashes of unstyled content.
                a.onload = () => {
                    Tinter.setTheme(theme);
                };
            }
        }
    }

    if (window.localStorage && window.localStorage.getItem('mx_accepts_unsupported_browser')) {
        console.log('User has previously accepted risks in using an unsupported browser');
        validBrowser = true;
    }

    console.log("Vector starting at "+window.location);
    if (configError) {
        window.matrixChat = ReactDOM.render(<div className="error">
            Unable to load config file: please refresh the page to try again.
        </div>, document.getElementById('matrixchat'));
    } else if (validBrowser) {
        const platform = PlatformPeg.get();
        platform.startUpdater();

        const MatrixChat = sdk.getComponent('structures.MatrixChat');
        window.matrixChat = ReactDOM.render(
            <MatrixChat
                onNewScreen={onNewScreen}
                makeRegistrationUrl={makeRegistrationUrl}
                ConferenceHandler={VectorConferenceHandler}
                config={configJson}
                realQueryParams={params}
                startingFragmentQueryParams={fragparts.params}
                enableGuest={!configJson.disable_guests}
                onTokenLoginCompleted={onTokenLoginCompleted}
                initialScreenAfterLogin={getScreenFromLocation(window.location)}
                defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
            />,
            document.getElementById('matrixchat')
        );
    } else {
        console.error("Browser is missing required features.");
        // take to a different landing page to AWOOOOOGA at the user
        var CompatibilityPage = sdk.getComponent("structures.CompatibilityPage");
        window.matrixChat = ReactDOM.render(
            <CompatibilityPage onAccept={function() {
                if (window.localStorage) window.localStorage.setItem('mx_accepts_unsupported_browser', true);
                validBrowser = true;
                console.log("User accepts the compatibility risks.");
                loadApp();
            }} />,
            document.getElementById('matrixchat')
        );
    }
}

async function loadLanguage() {
    const prefLang = SettingsStore.getValue("language");
    let langs = [];

    if (!prefLang) {
        languageHandler.getLanguagesFromBrowser().forEach((l) => {
            langs.push(...languageHandler.getNormalizedLanguageKeys(l));
        });
    } else {
        langs = [prefLang];
    }
    try {
        await languageHandler.setLanguage(langs);
    } catch (e) {
        console.error("Unable to set language", e);
    }
}

loadApp();
<commit_msg>Make sure we load the config before trying to redirect to a mobile page<commit_after>/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

'use strict';

// for ES6 stuff like startsWith() that Safari doesn't handle
// and babel doesn't do by default
// Note we use this, as well as the babel transform-runtime plugin
// since transform-runtime does not cover instance methods
// such as "foobar".includes("foo") which bits of our library
// code use, but the babel transform-runtime plugin allows the
// regenerator runtime to be injected early enough in the process
// (it can't be here as it's too late: the alternative is to put
// the babel-polyfill as the first 'entry' in the webpack config).
// https://babeljs.io/docs/plugins/transform-runtime/
require('babel-polyfill');

// Require common CSS here; this will make webpack process it into bundle.css.
// Our own CSS (which is themed) is imported via separate webpack entry points
// in webpack.config.js
require('gemini-scrollbar/gemini-scrollbar.css');
require('gfm.css/gfm.css');
require('highlight.js/styles/github.css');
require('draft-js/dist/Draft.css');

const rageshake = require("./rageshake");
rageshake.init().then(() => {
    console.log("Initialised rageshake: See https://bugs.chromium.org/p/chromium/issues/detail?id=583193 to fix line numbers on Chrome.");
    rageshake.cleanup();
}, (err) => {
    console.error("Failed to initialise rageshake: " + err);
});

window.addEventListener('beforeunload', (e) => {
    console.log('riot-web closing');
    // try to flush the logs to indexeddb
    rageshake.flush();
});

 // add React and ReactPerf to the global namespace, to make them easier to
 // access via the console
global.React = require("react");
if (process.env.NODE_ENV !== 'production') {
    global.Perf = require("react-addons-perf");
}

var RunModernizrTests = require("./modernizr"); // this side-effects a global
var ReactDOM = require("react-dom");
var sdk = require("matrix-react-sdk");
const PlatformPeg = require("matrix-react-sdk/lib/PlatformPeg");
sdk.loadSkin(require('../component-index'));
var VectorConferenceHandler = require('../VectorConferenceHandler');
import Promise from 'bluebird';
var request = require('browser-request');
import * as languageHandler from 'matrix-react-sdk/lib/languageHandler';
// Also import _t directly so we can call it just `_t` as this is what gen-i18n.js expects
import { _t } from 'matrix-react-sdk/lib/languageHandler';

import url from 'url';

import {parseQs, parseQsFromFragment} from './url_utils';
import Platform from './platform';

import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import SettingsStore, {SettingLevel} from "matrix-react-sdk/lib/settings/SettingsStore";
import Tinter from 'matrix-react-sdk/lib/Tinter';
import SdkConfig from "matrix-react-sdk/lib/SdkConfig";

var lastLocationHashSet = null;

var CallHandler = require("matrix-react-sdk/lib/CallHandler");
CallHandler.setConferenceHandler(VectorConferenceHandler);

MatrixClientPeg.setIndexedDbWorkerScript(window.vector_indexeddb_worker_script);

function checkBrowserFeatures(featureList) {
    if (!window.Modernizr) {
        console.error("Cannot check features - Modernizr global is missing.");
        return false;
    }
    var featureComplete = true;
    for (var i = 0; i < featureList.length; i++) {
        if (window.Modernizr[featureList[i]] === undefined) {
            console.error(
                "Looked for feature '%s' but Modernizr has no results for this. " +
                "Has it been configured correctly?", featureList[i]
            );
            return false;
        }
        if (window.Modernizr[featureList[i]] === false) {
            console.error("Browser missing feature: '%s'", featureList[i]);
            // toggle flag rather than return early so we log all missing features
            // rather than just the first.
            featureComplete = false;
        }
    }
    return featureComplete;
}

var validBrowser = checkBrowserFeatures([
    "displaytable", "flexbox", "es5object", "es5function", "localstorage",
    "objectfit", "indexeddb", "webworkers",
]);

// Parse the given window.location and return parameters that can be used when calling
// MatrixChat.showScreen(screen, params)
function getScreenFromLocation(location) {
    const fragparts = parseQsFromFragment(location);
    return {
        screen: fragparts.location.substring(1),
        params: fragparts.params,
    }
}

// Here, we do some crude URL analysis to allow
// deep-linking.
function routeUrl(location) {
    if (!window.matrixChat) return;

    console.log("Routing URL ", location.href);
    const s = getScreenFromLocation(location);
    window.matrixChat.showScreen(s.screen, s.params);
}

function onHashChange(ev) {
    if (decodeURIComponent(window.location.hash) == lastLocationHashSet) {
        // we just set this: no need to route it!
        return;
    }
    routeUrl(window.location);
}

// This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately.
var onNewScreen = function(screen) {
    console.log("newscreen "+screen);
    var hash = '#/' + screen;
    lastLocationHashSet = hash;
    window.location.hash = hash;
};

// We use this to work out what URL the SDK should
// pass through when registering to allow the user to
// click back to the client having registered.
// It's up to us to recognise if we're loaded with
// this URL and tell MatrixClient to resume registration.
//
// If we're in electron, we should never pass through a file:// URL otherwise
// the identity server will try to 302 the browser to it, which breaks horribly.
// so in that instance, hardcode to use riot.im/app for now instead.
var makeRegistrationUrl = function(params) {
    let url;
    if (window.location.protocol === "file:") {
        url = 'https://riot.im/app/#/register';
    } else {
        url = (
            window.location.protocol + '//' +
            window.location.host +
            window.location.pathname +
            '#/register'
        );
    }

    const keys = Object.keys(params);
    for (let i = 0; i < keys.length; ++i) {
        if (i == 0) {
            url += '?';
        } else {
            url += '&';
        }
        const k = keys[i];
        url += k + '=' + encodeURIComponent(params[k]);
    }
    return url;
}

window.addEventListener('hashchange', onHashChange);

function getConfig(configJsonFilename) {
    let deferred = Promise.defer();

    request(
        { method: "GET", url: configJsonFilename },
        (err, response, body) => {
            if (err || response.status < 200 || response.status >= 300) {
                // Lack of a config isn't an error, we should
                // just use the defaults.
                // Also treat a blank config as no config, assuming
                // the status code is 0, because we don't get 404s
                // from file: URIs so this is the only way we can
                // not fail if the file doesn't exist when loading
                // from a file:// URI.
                if (response) {
                    if (response.status == 404 || (response.status == 0 && body == '')) {
                        deferred.resolve({});
                    }
                }
                deferred.reject({err: err, response: response});
                return;
            }

            // We parse the JSON ourselves rather than use the JSON
            // parameter, since this throws a parse error on empty
            // which breaks if there's no config.json and we're
            // loading from the filesystem (see above).
            deferred.resolve(JSON.parse(body));
        }
    );

    return deferred.promise;
}

function onTokenLoginCompleted() {
    // if we did a token login, we're now left with the token, hs and is
    // url as query params in the url; a little nasty but let's redirect to
    // clear them.
    var parsedUrl = url.parse(window.location.href);
    parsedUrl.search = "";
    var formatted = url.format(parsedUrl);
    console.log("Redirecting to " + formatted + " to drop loginToken " +
                "from queryparams");
    window.location.href = formatted;
}

async function loadApp() {
    await loadLanguage();

    const fragparts = parseQsFromFragment(window.location);
    const params = parseQs(window.location);

    // set the platform for react sdk (our Platform object automatically picks the right one)
    PlatformPeg.set(new Platform());

    // Load the config file. First try to load up a domain-specific config of the
    // form "config.$domain.json" and if that fails, fall back to config.json.
    let configJson;
    let configError;
    try {
        try {
            configJson = await getConfig(`config.${document.domain}.json`);
            // 404s succeed with an empty json config, so check that there are keys
            if (Object.keys(configJson).length === 0) {
                throw new Error(); // throw to enter the catch
            }
        } catch (e) {
            configJson = await getConfig("config.json");
        }
    } catch (e) {
        configError = e;
    }
    SdkConfig.put(configJson);

    // don't try to redirect to the native apps if we're
    // verifying a 3pid (but after we've loaded the config)
    const preventRedirect = Boolean(fragparts.params.client_secret);

    if (!preventRedirect) {
        if (/iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream) {
            // FIXME: ugly status hardcoding
            if (SettingsStore.getValue("theme") === 'status') {
                window.location = "https://status.im/join-riot.html";
                return;
            }
            else {
                if (confirm(_t("Riot is not supported on mobile web. Install the app?"))) {
                    window.location = "https://itunes.apple.com/us/app/vector.im/id1083446067";
                    return;
                }
            }
        }
        else if (/Android/.test(navigator.userAgent)) {
            // FIXME: ugly status hardcoding
            if (SettingsStore.getValue("theme") === 'status') {
                window.location = "https://status.im/join-riot.html";
                return;
            }
            else {
                if (confirm(_t("Riot is not supported on mobile web. Install the app?"))) {
                    window.location = "https://play.google.com/store/apps/details?id=im.vector.alpha";
                    return;
                }
            }
        }
    }

    // as quickly as we possibly can, set a default theme...
    const styleElements = Object.create(null);
    let a;
    const theme = SettingsStore.getValue("theme");
    for (let i = 0; (a = document.getElementsByTagName("link")[i]); i++) {
        const href = a.getAttribute("href");
        if (!href) continue;
        // shouldn't we be using the 'title' tag rather than the href?
        const match = href.match(/^bundles\/.*\/theme-(.*)\.css$/);
        if (match) {
            if (match[1] === theme) {
                // remove the disabled flag off the stylesheet
                a.removeAttribute("disabled");

                // in case the Tinter.tint() in MatrixChat fires before the
                // CSS has actually loaded (which in practice happens)...

                // FIXME: we should probably block loading the app or even
                // showing a spinner until the theme is loaded, to avoid
                // flashes of unstyled content.
                a.onload = () => {
                    Tinter.setTheme(theme);
                };
            }
        }
    }

    if (window.localStorage && window.localStorage.getItem('mx_accepts_unsupported_browser')) {
        console.log('User has previously accepted risks in using an unsupported browser');
        validBrowser = true;
    }

    console.log("Vector starting at "+window.location);
    if (configError) {
        window.matrixChat = ReactDOM.render(<div className="error">
            Unable to load config file: please refresh the page to try again.
        </div>, document.getElementById('matrixchat'));
    } else if (validBrowser) {
        const platform = PlatformPeg.get();
        platform.startUpdater();

        const MatrixChat = sdk.getComponent('structures.MatrixChat');
        window.matrixChat = ReactDOM.render(
            <MatrixChat
                onNewScreen={onNewScreen}
                makeRegistrationUrl={makeRegistrationUrl}
                ConferenceHandler={VectorConferenceHandler}
                config={configJson}
                realQueryParams={params}
                startingFragmentQueryParams={fragparts.params}
                enableGuest={!configJson.disable_guests}
                onTokenLoginCompleted={onTokenLoginCompleted}
                initialScreenAfterLogin={getScreenFromLocation(window.location)}
                defaultDeviceDisplayName={platform.getDefaultDeviceDisplayName()}
            />,
            document.getElementById('matrixchat')
        );
    } else {
        console.error("Browser is missing required features.");
        // take to a different landing page to AWOOOOOGA at the user
        var CompatibilityPage = sdk.getComponent("structures.CompatibilityPage");
        window.matrixChat = ReactDOM.render(
            <CompatibilityPage onAccept={function() {
                if (window.localStorage) window.localStorage.setItem('mx_accepts_unsupported_browser', true);
                validBrowser = true;
                console.log("User accepts the compatibility risks.");
                loadApp();
            }} />,
            document.getElementById('matrixchat')
        );
    }
}

async function loadLanguage() {
    const prefLang = SettingsStore.getValue("language");
    let langs = [];

    if (!prefLang) {
        languageHandler.getLanguagesFromBrowser().forEach((l) => {
            langs.push(...languageHandler.getNormalizedLanguageKeys(l));
        });
    } else {
        langs = [prefLang];
    }
    try {
        await languageHandler.setLanguage(langs);
    } catch (e) {
        console.error("Unable to set language", e);
    }
}

loadApp();
<|endoftext|>


<commit_before>import {metadata, Origin} from 'aurelia-metadata';
import {RelativeViewStrategy, ConventionalViewStrategy, viewStrategy} from './view-strategy';

/**
* Locates a view for an object.
*/
export class ViewLocator {
  /**
  * The metadata key for storing/finding view strategies associated with an class/object.
  */
  static viewStrategyMetadataKey = 'aurelia:view-strategy';

  /**
  * Gets the view strategy for the value.
  * @param value The value to locate the view strategy for.
  * @return The located ViewStrategy instance.
  */
  getViewStrategy(value: any): ViewStrategy {
    if (!value) {
      return null;
    }

    if (typeof value === 'object' && 'getViewStrategy' in value) {
      let origin = Origin.get(value.constructor);

      value = value.getViewStrategy();

      if (typeof value === 'string') {
        value = new RelativeViewStrategy(value);
      }

      viewStategy.assert(value);

      if (origin) {
        value.makeRelativeTo(origin.moduleId);
      }

      return value;
    }

    if (typeof value === 'string') {
      value = new RelativeViewStrategy(value);
    }

    if (viewStrategy.validate(value)) {
      return value;
    }

    if (typeof value !== 'function') {
      value = value.constructor;
    }

    let origin = Origin.get(value);
    let strategy = metadata.get(ViewLocator.viewStrategyMetadataKey, value);

    if (!strategy) {
      if (!origin) {
        throw new Error('Cannot determinte default view strategy for object.', value);
      }

      strategy = this.createFallbackViewStrategy(origin);
    } else if (origin) {
      strategy.moduleId = origin.moduleId;
    }

    return strategy;
  }

  /**
  * Creates a fallback View Strategy. Used when unable to locate a configured strategy.
  * The default implementation returns and instance of ConventionalViewStrategy.
  * @param origin The origin of the view model to return the strategy for.
  * @return The fallback ViewStrategy.
  */
  createFallbackViewStrategy(origin: Origin): ViewStrategy {
    return new ConventionalViewStrategy(this, origin);
  }

  /**
  * Conventionally converts a view model origin to a view url.
  * Used by the ConventionalViewStrategy.
  * @param origin The origin of the view model to convert.
  * @return The view url.
  */
  convertOriginToViewUrl(origin: Origin): string {
    let moduleId = origin.moduleId;
    let id = (moduleId.endsWith('.js') || moduleId.endsWith('.ts')) ? moduleId.substring(0, moduleId.length - 3) : moduleId;
    return id + '.html';
  }
}
<commit_msg>fix(view-locator): bad reference to viewStrategy<commit_after>import {metadata, Origin} from 'aurelia-metadata';
import {RelativeViewStrategy, ConventionalViewStrategy, viewStrategy} from './view-strategy';

/**
* Locates a view for an object.
*/
export class ViewLocator {
  /**
  * The metadata key for storing/finding view strategies associated with an class/object.
  */
  static viewStrategyMetadataKey = 'aurelia:view-strategy';

  /**
  * Gets the view strategy for the value.
  * @param value The value to locate the view strategy for.
  * @return The located ViewStrategy instance.
  */
  getViewStrategy(value: any): ViewStrategy {
    if (!value) {
      return null;
    }

    if (typeof value === 'object' && 'getViewStrategy' in value) {
      let origin = Origin.get(value.constructor);

      value = value.getViewStrategy();

      if (typeof value === 'string') {
        value = new RelativeViewStrategy(value);
      }

      viewStrategy.assert(value);

      if (origin) {
        value.makeRelativeTo(origin.moduleId);
      }

      return value;
    }

    if (typeof value === 'string') {
      value = new RelativeViewStrategy(value);
    }

    if (viewStrategy.validate(value)) {
      return value;
    }

    if (typeof value !== 'function') {
      value = value.constructor;
    }

    let origin = Origin.get(value);
    let strategy = metadata.get(ViewLocator.viewStrategyMetadataKey, value);

    if (!strategy) {
      if (!origin) {
        throw new Error('Cannot determinte default view strategy for object.', value);
      }

      strategy = this.createFallbackViewStrategy(origin);
    } else if (origin) {
      strategy.moduleId = origin.moduleId;
    }

    return strategy;
  }

  /**
  * Creates a fallback View Strategy. Used when unable to locate a configured strategy.
  * The default implementation returns and instance of ConventionalViewStrategy.
  * @param origin The origin of the view model to return the strategy for.
  * @return The fallback ViewStrategy.
  */
  createFallbackViewStrategy(origin: Origin): ViewStrategy {
    return new ConventionalViewStrategy(this, origin);
  }

  /**
  * Conventionally converts a view model origin to a view url.
  * Used by the ConventionalViewStrategy.
  * @param origin The origin of the view model to convert.
  * @return The view url.
  */
  convertOriginToViewUrl(origin: Origin): string {
    let moduleId = origin.moduleId;
    let id = (moduleId.endsWith('.js') || moduleId.endsWith('.ts')) ? moduleId.substring(0, moduleId.length - 3) : moduleId;
    return id + '.html';
  }
}
<|endoftext|>


<commit_before>define([
    "hr/hr",
    "hr/dom",
    "ace",
    "utils/dialogs",
    "text!resources/templates/editor.html"
], function(hr, $, ace, dialogs, templateFile) {
    var aceconfig = ace.require("ace/config");
    aceconfig.set("basePath", "static/ace");

    var textAction = function(before, after) {
        return function() {
            if (this.editor.selection.isEmpty()) {
                this.editor.insert(before+after);
                this.editor.selection.moveCursorBy(0, -(after.length));
            } else {
                var c = this.editor.session.getTextRange(this.editor.getSelectionRange());
                this.editor.session.replace(this.editor.getSelectionRange(), before+c+after);
            }

            this.editor.focus();
        };
    };

    var textInteractiveAction = function(title, fields, getter) {
        return function() {
            var that = this;

            dialogs.fields(title, fields)
            .then(getter.bind(this))
            .then(function(o) {
                return textAction(o[0], o[1]).call(that);
            })
            .fail(function(err) {
                console.error(err);
            })
        };
    };

    var Editor = hr.View.extend({
        className: "book-section editor",
        template: templateFile,
        events: {
            "click .action-save": "doSave",
            "click .action-text-bold": textAction("**", "**"),
            "click .action-text-italic": textAction("*", "*"),
            "click .action-text-strikethrough": textAction("~~", "~~"),
            "click .action-text-title-1": textAction("# ", "\n"),
            "click .action-text-title-2": textAction("## ", "\n"),
            "click .action-text-title-3": textAction("### ", "\n"),
            "click .action-text-title-4": textAction("#### ", "\n"),
            "click .action-text-list-ul": textAction("* ", "\n"),
            "click .action-text-list-ol": textAction("1. ", "\n"),
            "click .action-text-code": textAction("```\n", "```\n"),
            "click .action-text-table": textInteractiveAction("Add a table", {
                "rows": {
                    'label': "Rows",
                    'type': "number",
                    'default': 2
                },
                "columns": {
                    'label': "Columns",
                    'type': "number",
                    'default': 2
                }
            }, function(info) {
                var before = "";
                var after = "";

                for (var y = 0; y <= info.rows; y++) {
                    var line = "|";

                    for (var x = 0; x < info.columns; x++) {
                        line = line + (y == 1 ? " -- |": " "+x+":"+y+" |");
                    }

                    before = before+line+"\n";
                }

                return [after, before]
            }),
            "click .action-text-link": textInteractiveAction("Add a link", {
                "href": {
                    'label': "Link",
                    'type': "text",
                    'default': "http://"
                }
            }, function(info) {
                return ["[", "]("+info.href+")"];
            }),
            "click .action-text-image": textInteractiveAction("Add an image", {
                "href": {
                    'label': "Link",
                    'type': "text",
                    'default': "http://"
                }
            }, function(info) {
                return ["![", "]("+info.href+")"];
            }),
            "click .action-help": "doOpenHelp"
        },

        initialize: function() {
            Editor.__super__.initialize.apply(this, arguments);

            this.book = this.parent;

            this.$editor = $("<div>", {'class': "editor"});
            this.$editor.appendTo(this.$el);

            this.ignoreChange = false;

            this.editor = ace.edit(this.$editor.get(0));

            this.editor.on("change", function() {
                if (this.ignoreChange || !this.book.currentArticle) return;

                var content = this.editor.getValue();
                this.book.writeArticle(this.book.currentArticle, content);
            }.bind(this));

            this.editor.setTheme({
                'isDark': false,
                'cssClass': "ace-tm",
                'cssText': ""
            });
            this.editor.getSession().setMode("ace/mode/markdown");
            this.editor.setOption("showGutter", false);
            this.editor.setShowPrintMargin(false);
            this.editor.setHighlightActiveLine(false);
            this.editor.session.setUseWrapMode(true);
            this.editor.commands.addCommand({
                name: 'save',
                bindKey: {
                    win: 'Ctrl-S',
                    mac: 'Command-S'
                },
                exec: function(editor) {
                    this.doSave();
                }.bind(this),
                readOnly: false
            });

            this.on("grid:layout", function() {
                this.editor.resize();
                this.editor.renderer.updateFull();
            }, this);

            this.listenTo(this.book, "article:open", this.onArticleChange);
            this.listenTo(this.book, "article:state", this.onArticleState);
            this.listenTo(this.book, "article:save", this.onArticleSave);
        },

        finish: function() {
            this.$editor.appendTo(this.$(".content"));
            return Editor.__super__.finish.apply(this, arguments);
        },

        // When the user opens another article
        onArticleChange: function(article) {
            var that = this;

            this.book.readArticle(article)
            .then(function(content) {
                var state = that.book.getArticleState(article);
                that.onArticleState(article, state.saved);

                that.ignoreChange = true;
                that.editor.setValue(content);
                that.editor.gotoLine(0);
                that.ignoreChange = false;
            }, dialogs.error);
        },

        // When the state of the current article change
        onArticleState: function(article, state) {
            if (article.get("path") != this.book.currentArticle.get("path")) return;
            this.$(".action-save").toggleClass("disabled", state);
            this.$(".action-save").toggleClass("btn-warning", !state);
        },

        // Update editor if article has been normalized
        onArticleSave: function(article, content) {
            var pos = this.editor.getCursorPosition();

            // We need to have line to calculate new positions
            // and ensure they don't "overflow"
            var lines = content.split('\n');

            // Next positions
            var y = Math.min(pos.row, lines.length-1);
            var x = Math.min(pos.column, Math.max(lines[y].length, 0));

            this.ignoreChange = true;
            this.editor.setValue(content, 1);
            this.editor.moveCursorTo(y, p);
            this.ignoreChange = false;
        },

        // Save the article
        doSave: function(e) {
            if (e) e.preventDefault();
            this.book.saveArticle(this.book.currentArticle);
        },

        // Open the help about markdown
        doOpenHelp: function() {
            node.gui.Shell.openExternal("https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet");
        }
    });

    return Editor;
});<commit_msg>Fix typo breaking cursor repositioning<commit_after>define([
    "hr/hr",
    "hr/dom",
    "ace",
    "utils/dialogs",
    "text!resources/templates/editor.html"
], function(hr, $, ace, dialogs, templateFile) {
    var aceconfig = ace.require("ace/config");
    aceconfig.set("basePath", "static/ace");

    var textAction = function(before, after) {
        return function() {
            if (this.editor.selection.isEmpty()) {
                this.editor.insert(before+after);
                this.editor.selection.moveCursorBy(0, -(after.length));
            } else {
                var c = this.editor.session.getTextRange(this.editor.getSelectionRange());
                this.editor.session.replace(this.editor.getSelectionRange(), before+c+after);
            }

            this.editor.focus();
        };
    };

    var textInteractiveAction = function(title, fields, getter) {
        return function() {
            var that = this;

            dialogs.fields(title, fields)
            .then(getter.bind(this))
            .then(function(o) {
                return textAction(o[0], o[1]).call(that);
            })
            .fail(function(err) {
                console.error(err);
            })
        };
    };

    var Editor = hr.View.extend({
        className: "book-section editor",
        template: templateFile,
        events: {
            "click .action-save": "doSave",
            "click .action-text-bold": textAction("**", "**"),
            "click .action-text-italic": textAction("*", "*"),
            "click .action-text-strikethrough": textAction("~~", "~~"),
            "click .action-text-title-1": textAction("# ", "\n"),
            "click .action-text-title-2": textAction("## ", "\n"),
            "click .action-text-title-3": textAction("### ", "\n"),
            "click .action-text-title-4": textAction("#### ", "\n"),
            "click .action-text-list-ul": textAction("* ", "\n"),
            "click .action-text-list-ol": textAction("1. ", "\n"),
            "click .action-text-code": textAction("```\n", "```\n"),
            "click .action-text-table": textInteractiveAction("Add a table", {
                "rows": {
                    'label': "Rows",
                    'type': "number",
                    'default': 2
                },
                "columns": {
                    'label': "Columns",
                    'type': "number",
                    'default': 2
                }
            }, function(info) {
                var before = "";
                var after = "";

                for (var y = 0; y <= info.rows; y++) {
                    var line = "|";

                    for (var x = 0; x < info.columns; x++) {
                        line = line + (y == 1 ? " -- |": " "+x+":"+y+" |");
                    }

                    before = before+line+"\n";
                }

                return [after, before]
            }),
            "click .action-text-link": textInteractiveAction("Add a link", {
                "href": {
                    'label': "Link",
                    'type': "text",
                    'default': "http://"
                }
            }, function(info) {
                return ["[", "]("+info.href+")"];
            }),
            "click .action-text-image": textInteractiveAction("Add an image", {
                "href": {
                    'label': "Link",
                    'type': "text",
                    'default': "http://"
                }
            }, function(info) {
                return ["![", "]("+info.href+")"];
            }),
            "click .action-help": "doOpenHelp"
        },

        initialize: function() {
            Editor.__super__.initialize.apply(this, arguments);

            this.book = this.parent;

            this.$editor = $("<div>", {'class': "editor"});
            this.$editor.appendTo(this.$el);

            this.ignoreChange = false;

            this.editor = ace.edit(this.$editor.get(0));

            this.editor.on("change", function() {
                if (this.ignoreChange || !this.book.currentArticle) return;

                var content = this.editor.getValue();
                this.book.writeArticle(this.book.currentArticle, content);
            }.bind(this));

            this.editor.setTheme({
                'isDark': false,
                'cssClass': "ace-tm",
                'cssText': ""
            });
            this.editor.getSession().setMode("ace/mode/markdown");
            this.editor.setOption("showGutter", false);
            this.editor.setShowPrintMargin(false);
            this.editor.setHighlightActiveLine(false);
            this.editor.session.setUseWrapMode(true);
            this.editor.commands.addCommand({
                name: 'save',
                bindKey: {
                    win: 'Ctrl-S',
                    mac: 'Command-S'
                },
                exec: function(editor) {
                    this.doSave();
                }.bind(this),
                readOnly: false
            });

            this.on("grid:layout", function() {
                this.editor.resize();
                this.editor.renderer.updateFull();
            }, this);

            this.listenTo(this.book, "article:open", this.onArticleChange);
            this.listenTo(this.book, "article:state", this.onArticleState);
            this.listenTo(this.book, "article:save", this.onArticleSave);
        },

        finish: function() {
            this.$editor.appendTo(this.$(".content"));
            return Editor.__super__.finish.apply(this, arguments);
        },

        // When the user opens another article
        onArticleChange: function(article) {
            var that = this;

            this.book.readArticle(article)
            .then(function(content) {
                var state = that.book.getArticleState(article);
                that.onArticleState(article, state.saved);

                that.ignoreChange = true;
                that.editor.setValue(content);
                that.editor.gotoLine(0);
                that.ignoreChange = false;
            }, dialogs.error);
        },

        // When the state of the current article change
        onArticleState: function(article, state) {
            if (article.get("path") != this.book.currentArticle.get("path")) return;
            this.$(".action-save").toggleClass("disabled", state);
            this.$(".action-save").toggleClass("btn-warning", !state);
        },

        // Update editor if article has been normalized
        onArticleSave: function(article, content) {
            var pos = this.editor.getCursorPosition();

            // We need to have line to calculate new positions
            // and ensure they don't "overflow"
            var lines = content.split('\n');

            // Next positions
            var y = Math.min(pos.row, lines.length-1);
            var x = Math.min(pos.column, Math.max(lines[y].length, 0));

            this.ignoreChange = true;
            this.editor.setValue(content, 1);
            this.editor.moveCursorTo(y, x);
            this.ignoreChange = false;
        },

        // Save the article
        doSave: function(e) {
            if (e) e.preventDefault();
            this.book.saveArticle(this.book.currentArticle);
        },

        // Open the help about markdown
        doOpenHelp: function() {
            node.gui.Shell.openExternal("https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet");
        }
    });

    return Editor;
});<|endoftext|>


<commit_before>// Vuex store getters

export function metadata (state) {
  return state.metadata
}

export function selectedSeason (state) {
  return state.selected.season
}

export function selectedRegion (state) {
  return state.selected.region
}

export function selectedWeekIdx (state) {
  return state.selected.week.idx
}

export function selectedWeekName (state) {
  if (state.selected.week.name)
    return state.selected.week.name
  else
    return 'NA'
}

export function selectedChoropleth (state) {
  return state.selected.choropleth
}

export function seasons (state) {
  // Assuming each region has all the seasons
  return state.data[0].seasons.map(s => s.id)
}

export function regions (state) {
  return state.data.map(d => d.region)
}

/**
 * Scale given (week, data) pairs using the baseline
 */
function baselineScale (values, baseline) {
  return values.map(d => {
    return {
      week: d.week,
      data: baseline ? ((d.data / baseline) - 1) * 100 : -1
    }
  })
}

/**
 * Return choropleth data for model
 */
function modelChoroplethData (state, modelId, dataType, relative = false) {
  let seasonId = selectedSeason(state)

  let output = {
    data: [],
    type: 'sequential'
  }

  state.data.map(r => {
    let preds = r.seasons[seasonId].models[modelId].predictions
    let weeks = getMaxLagData(r.seasons[seasonId].actual).map(d => d.week)
  })
}

/**
 * Return data for choropleth using actual values
 */
function actualChoroplethData (state, relative = false) {
  let seasonId = selectedSeason(state)

  let output = {
    data: [],
    type: relative ? 'diverging' : 'sequential'
  }

  state.data.map(r => {
    let values = getMaxLagData(r.seasons[seasonId].actual)

    if (relative)
      values = baselineScale(values, r.seasons[seasonId].baseline)

    output.data.push({
      region: r.subId,
      states: r.states,
      value: values
    })
  })

  output.data = output.data.slice(1) // Remove national data

  return output
}

export function choropleths (state) {
  // TODO: Parse from data

  let actualChoropleths = [
    'Actual Weighted ILI (%)',
    'Relative Weighted ILI (%)'
  ]

  modelChoroplethData(state, 0)

  return actualChoropleths
}

export function timeChart (state) {
  return state.timeChart
}

export function choropleth (state) {
  return state.choropleth
}

/**
 * Get data with maximum lag
 * First element of the lag array
 */
function getMaxLagData (actual) {
  return actual.map(d => {
    let dataToReturn = -1
    // Handle zero length values
    if (d.data.length !== 0) {
      dataToReturn = d.data[0].value
    }
    return {
      week: d.week,
      data: dataToReturn
    }
  })
}

/**
 * Trim history data to fit in length 'numWeeks'
 */
function trimHistory (historyActual, numWeeks) {
  let historyTrimmed = historyActual.slice()

  if (numWeeks == 52) {
    // Clip everyone else to remove 53rd week
    historyTrimmed = historyTrimmed.filter(d => d.week % 100 != 53)
  } else if (historyTrimmed.length == 52) {
    // Expand to add 53rd week
    // Adding a dummy year 1000, this will also help identify the adjustment
    historyTrimmed.splice(23, 0, {
      week: 100053,
      data: (historyTrimmed[22].data + historyTrimmed[23].data) / 2.0
    })
  }

  return historyTrimmed
}

/**
 * Return data subset for chart as specified in region/season selected
 */
export function timeChartData (state) {

  let regionSubset = state.data[selectedRegion(state)]
  let currentSeasonId = selectedSeason(state)
  let seasonSubset = regionSubset.seasons[currentSeasonId]

  let selectedWeeksCount = seasonSubset.actual.length

  let historicalData = []

  // Add data from history store
  for (let season of Object.keys(regionSubset.history)) {
    historicalData.push({
      id: season,
      actual: trimHistory(regionSubset.history[season], selectedWeeksCount)
    })
  }

  for (let i = 0; i < currentSeasonId; i++) {
    historicalData.push({
      id: regionSubset.seasons[i].id,
      actual: trimHistory(getMaxLagData(regionSubset.seasons[i].actual),
                          selectedWeeksCount)
    })
  }

  return {
    region: regionSubset.subId, // Submission ids are concise
    observed: seasonSubset.actual,
    actual: getMaxLagData(seasonSubset.actual), // Using this to avoid confusion
    baseline: seasonSubset.baseline,
    models: seasonSubset.models, // All model predictions
    history: historicalData
  }
}

/**
 * Return formatted next week data
 */
export function nextWeek (state) {
  return timeChart(state).getNextWeekData()
}

/**
 * Return formatted previous week data
 */
export function previousWeek (state) {
  return timeChart(state).getPreviousWeekData()
}

/**
 * Return range for choropleth color scale
 */
function choroplethDataRange (state, choroplethId) {
  let maxVals = [],
      minVals = []

  state.data.map(region => {
    region.seasons.map(season => {

      let actual = getMaxLagData(season.actual).map(d => d.data).filter(d => d !== -1)

      if (choroplethId === 1) {
        // Use baseline scaled data
        maxVals.push(Math.max(...actual.map(d => ((d / season.baseline) - 1) * 100)))
        minVals.push(Math.min(...actual.map(d => ((d / season.baseline) - 1) * 100)))
      } else {
        maxVals.push(Math.max(...actual))
        minVals.push(Math.min(...actual))
      }
    })
  })

  return [Math.min(...minVals),
          Math.max(...maxVals)]
}

/**
 * Return actual data for all regions for current selections
 */
export function choroplethData (state) {

  let choroplethId = selectedChoropleth(state),
      seasonId = selectedSeason(state)

  let output = actualChoroplethData(state, choroplethId === 1)

  output.range = choroplethDataRange(state, choroplethId)
  return output
}

export function legendShow (state) {
  return state.toggles.legend
}

// Introduction getters
export function currentIntro (state) {
  return state.intro.data[introStep(state)]
}

export function introStep (state) {
  return state.intro.pointer
}

export function introLength (state) {
  return state.intro.data.length
}

export function introShow (state) {
  return state.toggles.intro
}

export function introAtFirst (state) {
  return introStep(state) === 0
}

export function introAtLast (state) {
  return introStep(state) === introLength(state) - 1
}
<commit_msg>model ILI data function for choropleth<commit_after>// Vuex store getters

export function metadata (state) {
  return state.metadata
}

export function selectedSeason (state) {
  return state.selected.season
}

export function selectedRegion (state) {
  return state.selected.region
}

export function selectedWeekIdx (state) {
  return state.selected.week.idx
}

export function selectedWeekName (state) {
  if (state.selected.week.name)
    return state.selected.week.name
  else
    return 'NA'
}

export function selectedChoropleth (state) {
  return state.selected.choropleth
}

export function seasons (state) {
  // Assuming each region has all the seasons
  return state.data[0].seasons.map(s => s.id)
}

export function regions (state) {
  return state.data.map(d => d.region)
}

/**
 * Scale given (week, data) pairs using the baseline
 */
function baselineScale (values, baseline) {
  return values.map(d => {
    return {
      week: d.week,
      data: baseline ? ((d.data / baseline) - 1) * 100 : -1
    }
  })
}

/**
 * Return choropleth data for model
 */
function modelChoroplethData (state, modelId, predictionType, relative = false) {
  let seasonId = selectedSeason(state)

  let output = {
    data: [],
    type: relative ? 'diverging' : 'sequential'
  }

  state.data.map(r => {
    let preds = {}

    r.seasons[seasonId].models[modelId].predictions
      .forEach(p => {
        preds[p.week] = p[predictionType].point
      })

    let weeks = getMaxLagData(r.seasons[seasonId].actual).map(d => d.week)

    let values = weeks.map(w => {
      return {
        week: w,
        data: preds[w] ? preds[w] : -1
      }
    })

    if (relative)
      values = baselineScale(values, r.seasons[seasonId].baseline)

    output.data.push({
      region: r.subId,
      states: r.states,
      value: values
    })
  })

  output.data = output.data.slice(1) // Remove national data

  return output
}

/**
 * Return data for choropleth using actual values
 */
function actualChoroplethData (state, relative = false) {
  let seasonId = selectedSeason(state)

  let output = {
    data: [],
    type: relative ? 'diverging' : 'sequential'
  }

  state.data.map(r => {
    let values = getMaxLagData(r.seasons[seasonId].actual)

    if (relative)
      values = baselineScale(values, r.seasons[seasonId].baseline)

    output.data.push({
      region: r.subId,
      states: r.states,
      value: values
    })
  })

  output.data = output.data.slice(1) // Remove national data

  return output
}

export function choropleths (state) {
  // TODO: Parse from data

  let actualChoropleths = [
    'Actual Weighted ILI (%)',
    'Relative Weighted ILI (%)'
  ]

  return actualChoropleths
}

export function timeChart (state) {
  return state.timeChart
}

export function choropleth (state) {
  return state.choropleth
}

/**
 * Get data with maximum lag
 * First element of the lag array
 */
function getMaxLagData (actual) {
  return actual.map(d => {
    let dataToReturn = -1
    // Handle zero length values
    if (d.data.length !== 0) {
      dataToReturn = d.data[0].value
    }
    return {
      week: d.week,
      data: dataToReturn
    }
  })
}

/**
 * Trim history data to fit in length 'numWeeks'
 */
function trimHistory (historyActual, numWeeks) {
  let historyTrimmed = historyActual.slice()

  if (numWeeks == 52) {
    // Clip everyone else to remove 53rd week
    historyTrimmed = historyTrimmed.filter(d => d.week % 100 != 53)
  } else if (historyTrimmed.length == 52) {
    // Expand to add 53rd week
    // Adding a dummy year 1000, this will also help identify the adjustment
    historyTrimmed.splice(23, 0, {
      week: 100053,
      data: (historyTrimmed[22].data + historyTrimmed[23].data) / 2.0
    })
  }

  return historyTrimmed
}

/**
 * Return data subset for chart as specified in region/season selected
 */
export function timeChartData (state) {

  let regionSubset = state.data[selectedRegion(state)]
  let currentSeasonId = selectedSeason(state)
  let seasonSubset = regionSubset.seasons[currentSeasonId]

  let selectedWeeksCount = seasonSubset.actual.length

  let historicalData = []

  // Add data from history store
  for (let season of Object.keys(regionSubset.history)) {
    historicalData.push({
      id: season,
      actual: trimHistory(regionSubset.history[season], selectedWeeksCount)
    })
  }

  for (let i = 0; i < currentSeasonId; i++) {
    historicalData.push({
      id: regionSubset.seasons[i].id,
      actual: trimHistory(getMaxLagData(regionSubset.seasons[i].actual),
                          selectedWeeksCount)
    })
  }

  return {
    region: regionSubset.subId, // Submission ids are concise
    observed: seasonSubset.actual,
    actual: getMaxLagData(seasonSubset.actual), // Using this to avoid confusion
    baseline: seasonSubset.baseline,
    models: seasonSubset.models, // All model predictions
    history: historicalData
  }
}

/**
 * Return formatted next week data
 */
export function nextWeek (state) {
  return timeChart(state).getNextWeekData()
}

/**
 * Return formatted previous week data
 */
export function previousWeek (state) {
  return timeChart(state).getPreviousWeekData()
}

/**
 * Return range for choropleth color scale
 */
function choroplethDataRange (state, choroplethId) {
  let maxVals = [],
      minVals = []

  state.data.map(region => {
    region.seasons.map(season => {

      let actual = getMaxLagData(season.actual).map(d => d.data).filter(d => d !== -1)

      if (choroplethId === 1) {
        // Use baseline scaled data
        maxVals.push(Math.max(...actual.map(d => ((d / season.baseline) - 1) * 100)))
        minVals.push(Math.min(...actual.map(d => ((d / season.baseline) - 1) * 100)))
      } else {
        maxVals.push(Math.max(...actual))
        minVals.push(Math.min(...actual))
      }
    })
  })

  return [Math.min(...minVals),
          Math.max(...maxVals)]
}

/**
 * Return actual data for all regions for current selections
 */
export function choroplethData (state) {

  let choroplethId = selectedChoropleth(state),
      seasonId = selectedSeason(state)

  let output = actualChoroplethData(state, choroplethId === 1)

  output.range = choroplethDataRange(state, choroplethId)
  return output
}

export function legendShow (state) {
  return state.toggles.legend
}

// Introduction getters
export function currentIntro (state) {
  return state.intro.data[introStep(state)]
}

export function introStep (state) {
  return state.intro.pointer
}

export function introLength (state) {
  return state.intro.data.length
}

export function introShow (state) {
  return state.toggles.intro
}

export function introAtFirst (state) {
  return introStep(state) === 0
}

export function introAtLast (state) {
  return introStep(state) === introLength(state) - 1
}
<|endoftext|>


<commit_before>var SpaceHipster = SpaceHipster || {};

SpaceHipster.Boot = function () {};

//setting game configuration and loading the asset for the loading screen
SpaceHipster.Boot.prototype = {
    preload: function () {
        //asset we'll use in the loading screen
        this.load.image('background', 'asset/images/background.png');
        this.load.image('preloadbar', 'asset/images/preloader-bar.png');
    },
    create: function () {
        //loading screen will have a white background
        this.game.stage.backgroundColor = '#fff';

        //scaling options
        this.scale.scaleMode = Phaser.ScaleManager.RESIZE;
        this.scale.minWidth = 240;
        this.scale.minHeight = 170;
        this.scale.maxWidth = 2880;
        this.scale.maxHeight = 1920;

        //have the game centered horizontally
        //this.scale.pageAlignHorizontally = true;
        //this.scale.pageAlignVertically = true;

        //physics system for movement
        this.game.physics.startSystem(Phaser.Physics.ARCADE);

        this.state.start('Preload');
    }
};<commit_msg>align test<commit_after>var SpaceHipster = SpaceHipster || {};

SpaceHipster.Boot = function () {};

//setting game configuration and loading the asset for the loading screen
SpaceHipster.Boot.prototype = {
    preload: function () {
        //asset we'll use in the loading screen
        this.load.image('background', 'asset/images/background.png');
        this.load.image('preloadbar', 'asset/images/preloader-bar.png');
    },
    create: function () {
        //loading screen will have a white background
        this.game.stage.backgroundColor = '#fff';

        //scaling options
        this.scale.scaleMode = Phaser.ScaleManager.RESIZE;
        this.scale.minWidth = 240;
        this.scale.minHeight = 170;
        this.scale.maxWidth = 2880;
        this.scale.maxHeight = 1920;

        //have the game centered horizontally
        this.scale.pageAlignHorizontally = true;
        this.scale.pageAlignVertically = true;

        //physics system for movement
        this.game.physics.startSystem(Phaser.Physics.ARCADE);

        this.state.start('Preload');
    }
};<|endoftext|>


<commit_before>// create BasicGame Class
BasicGame = {

};

// create Game function in BasicGame
BasicGame.Game = function (game) {
};

// set Game function prototype
BasicGame.Game.prototype = {

    init: function () {
        // set up input max pointers
        this.input.maxPointers = 1;
        // set up stage disable visibility change
        this.stage.disableVisibilityChange = true;
        // Set up the scaling method used by the ScaleManager
        // Valid values for scaleMode are:
        // * EXACT_FIT
        // * NO_SCALE
        // * SHOW_ALL
        // * RESIZE
        // See http://docs.phaser.io/Phaser.ScaleManager.html for full document
        this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
        // If you wish to align your game in the middle of the page then you can
        // set this value to true. It will place a re-calculated margin-left
        // pixel value onto the canvas element which is updated on orientation /
        // resizing events. It doesn't care about any other DOM element that may
        // be on the page, it literally just sets the margin.
        this.scale.pageAlignHorizontally = true;
        this.scale.pageAlignVertically = true;
        // Force the orientation in landscape or portrait.
        // * Set first to true to force landscape.
        // * Set second to true to force portrait.
        this.scale.forceOrientation(true, false);
        // Sets the callback that will be called when the window resize event
        // occurs, or if set the parent container changes dimensions. Use this
        // to handle responsive game layout options. Note that the callback will
        // only be called if the ScaleManager.scaleMode is set to RESIZE.
        this.scale.setResizeCallback(this.gameResized, this);
        // Set screen size automatically based on the scaleMode. This is only
        // needed if ScaleMode is not set to RESIZE.
        this.scale.setScreenSize(true);
        // Re-calculate scale mode and update screen size. This only applies if
        // ScaleMode is not set to RESIZE.
        this.scale.refresh();

    },

    preload: function () {

        // Here we load the assets required for our preloader (in this case a
        // background and a loading bar)
        this.load.image('logo', 'asset/phaser1.png');
    },

    create: function () {
        // Add logo to the center of the stage
        this.logo = this.add.sprite(
            this.world.centerX, // (centerX, centerY) is the center coordination
            this.world.centerY,
            'logo');
        // Set the anchor to the center of the sprite
        this.logo.anchor.setTo(0.5, 0.5);

    },

    gameResized: function (width, height) {

        // This could be handy if you need to do any extra processing if the
        // game resizes. A resize could happen if for example swapping
        // orientation on a device or resizing the browser window. Note that
        // this callback is only really useful if you use a ScaleMode of RESIZE
        // and place it inside your main game state.

    }

};
<commit_msg>Update Game.js<commit_after>// create BasicGame Class
BasicGame = {

};

// create Game function in BasicGame
BasicGame.Game = function (game) {
};

// set Game function prototype
BasicGame.Game.prototype = {

    init: function () {
        // set up input max pointers
        this.input.maxPointers = 1;
        // set up stage disable visibility change
        this.stage.disableVisibilityChange = true;
        // Set up the scaling method used by the ScaleManager
        // Valid values for scaleMode are:
        // * EXACT_FIT
        // * NO_SCALE
        // * SHOW_ALL
        // * RESIZE
        // See http://docs.phaser.io/Phaser.ScaleManager.html for full document
        this.scale.scaleMode = Phaser.ScaleManager.SHOW_ALL;
        // If you wish to align your game in the middle of the page then you can
        // set this value to true. It will place a re-calculated margin-left
        // pixel value onto the canvas element which is updated on orientation /
        // resizing events. It doesn't care about any other DOM element that may
        // be on the page, it literally just sets the margin.
        this.scale.pageAlignHorizontally = true;
        this.scale.pageAlignVertically = true;
        // Force the orientation in landscape or portrait.
        // * Set first to true to force landscape.
        // * Set second to true to force portrait.
        this.scale.forceOrientation(true, false);
        // Sets the callback that will be called when the window resize event
        // occurs, or if set the parent container changes dimensions. Use this
        // to handle responsive game layout options. Note that the callback will
        // only be called if the ScaleManager.scaleMode is set to RESIZE.
        this.scale.setResizeCallback(this.gameResized, this);
        // Set screen size automatically based on the scaleMode. This is only
        // needed if ScaleMode is not set to RESIZE.
        this.scale.setScreenSize(true);
        // Re-calculate scale mode and update screen size. This only applies if
        // ScaleMode is not set to RESIZE.
        this.scale.refresh();

    },

    preload: function () {

        // Here we load the assets required for our preloader (in this case a
        // background and a loading bar)
        this.load.image('logo', 'asset/phaser3.png');
    },

    create: function () {
        // Add logo to the center of the stage
        this.logo = this.add.sprite(
            this.world.centerX, // (centerX, centerY) is the center coordination
            this.world.centerY,
            'logo');
        // Set the anchor to the center of the sprite
        this.logo.anchor.setTo(0.5, 0.5);

    },

    gameResized: function (width, height) {

        // This could be handy if you need to do any extra processing if the
        // game resizes. A resize could happen if for example swapping
        // orientation on a device or resizing the browser window. Note that
        // this callback is only really useful if you use a ScaleMode of RESIZE
        // and place it inside your main game state.

    }

};
<|endoftext|>


<commit_before>'use strict';

const assert = require('assert');
const server = require('../server/server');

describe('server', function () {
  describe('#getRandom()', function() {
    it('should return between 0 and 5', function () {
      let r = server.getRandom(1, 4);
      assert(r > 0 && r < 5, 'return between 0 and 5!');
    });
  });

  describe('#getRandomInt()', function() {
    it('should return 1', function () {
      let r = server.getRandomInt(1, 1);
      assert(r === 1, 'return 1!');
    });

    it('should return between 0 and 5', function () {
      let r = server.getRandomInt(1, 4);
      assert(r > 0 && r < 5, 'return between 0 and 5!');
    });
  });

  const mapY = 300;
  const mapX = 300;

  describe('#getLineParams()', function() {
    it('should return Object with startY, startX and length', function () {
      const lineMaxLength = 3;
      let line = server.getLineParams(mapY, mapX, lineMaxLength);

      assert(line.length && line.length > 0 && line.length <= lineMaxLength,
            'return line with true legth!');

      assert(line.startY && line.startY >= 0 && line.startY < mapY - lineMaxLength,
            'return line with true startY');

      assert(line.startX && line.startX >= 0 && line.startX < mapX - lineMaxLength,
            'return line with true startX');
    });
  });

  describe('#initMap()', function() {
    it('should return Array mapY x mapX', function () {
      let labMap = server.initMap(mapY, mapX);
      let yCounter = 0;

      labMap.forEach(function eachY (mapY) {
        yCounter++;
        let xCounter = 0;

        mapY.forEach(function eachX () {
          xCounter++;
        });

        assert(xCounter === mapX, 'width of maps elem is true');
      });

      assert(yCounter === mapY, 'maps height is true');

    });
  });

  describe('#searchStartPosition()', function() {
    it('should return Object with x and y', function () {
      let labMap = server.initMap(mapY, mapX);
      let pos = server.searchStartPosition();
      let mapElem = labMap[pos.y][pos.x];

      assert(mapElem === 0, 'return true position');

    });
  });
});<commit_msg>Add blank line for awesome style<commit_after>'use strict';

const assert = require('assert');
const server = require('../server/server');

describe('server', function () {
  describe('#getRandom()', function() {
    it('should return between 0 and 5', function () {
      let r = server.getRandom(1, 4);
      assert(r > 0 && r < 5, 'return between 0 and 5!');
    });
  });

  describe('#getRandomInt()', function() {
    it('should return 1', function () {
      let r = server.getRandomInt(1, 1);
      assert(r === 1, 'return 1!');
    });

    it('should return between 0 and 5', function () {
      let r = server.getRandomInt(1, 4);
      assert(r > 0 && r < 5, 'return between 0 and 5!');
    });
  });

  const mapY = 300;
  const mapX = 300;

  describe('#getLineParams()', function() {
    it('should return Object with startY, startX and length', function () {
      const lineMaxLength = 3;
      let line = server.getLineParams(mapY, mapX, lineMaxLength);

      assert(line.length && line.length > 0 && line.length <= lineMaxLength,
            'return line with true legth!');

      assert(line.startY && line.startY >= 0 && line.startY < mapY - lineMaxLength,
            'return line with true startY');

      assert(line.startX && line.startX >= 0 && line.startX < mapX - lineMaxLength,
            'return line with true startX');
    });
  });

  describe('#initMap()', function() {
    it('should return Array mapY x mapX', function () {
      let labMap = server.initMap(mapY, mapX);
      let yCounter = 0;

      labMap.forEach(function eachY (mapY) {
        yCounter++;
        let xCounter = 0;

        mapY.forEach(function eachX () {
          xCounter++;
        });

        assert(xCounter === mapX, 'width of maps elem is true');
      });

      assert(yCounter === mapY, 'maps height is true');

    });
  });

  describe('#searchStartPosition()', function() {
    it('should return Object with x and y', function () {
      let labMap = server.initMap(mapY, mapX);
      let pos = server.searchStartPosition();
      let mapElem = labMap[pos.y][pos.x];

      assert(mapElem === 0, 'return true position');

    });
  });
});<|endoftext|>


<commit_before>/*!
 * OS.js - JavaScript Operating System
 *
 * Copyright (c) 2011-2014, Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */

(function(Utils, API) {
  'use strict';

  var indexedDB      = window.indexedDB      || window.mozIndexedDB         || window.webkitIndexedDB || window.msIndexedDB;
  var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
  var IDBKeyRange    = window.IDBKeyRange    || window.webkitIDBKeyRange    || window.msIDBKeyRange;

  /////////////////////////////////////////////////////////////////////////////
  // API
  /////////////////////////////////////////////////////////////////////////////

  function OIndexedDB(args, callback) {
    args = args || {};

    this.version = (args.version || 1).toString();
    this.dbName = args.dbName;
    this.storeName = args.storeName;
    this.primaryKey = args.primaryKey || null;
    this.db = null;

    if ( !this.dbName ) {
      throw new Error('Cannot create IndexedDB without Database Name');
    }
    if ( !this.storeName ) {
      throw new Error('Cannot create IndexedDB without Store Name');
    }

    console.info('OIndexedDB::construct()', this);

    this.init(callback);
  }

  OIndexedDB.prototype.init = function(callback) {
    console.info('OIndexedDB::init()');

    var self = this;
    var request = indexedDB.open(this.dbName, this.version);

    request.onupgradeneeded = function(e) {
      console.info('OIndexedDB::init()','=>','onupgradeneeded()', e);

      var db = e.target.result;
      if ( db.objectStoreNames.contains(self.storeName) ) {
        db.deleteObjectStore(self.storeName);
      }
      var sargs = self.primaryKey ? {keyPath: self.primaryKey} : {autoIncrement: true}
      var store = db.createObjectStore(self.storeName, sargs);

      callback(false, true);
    };

    request.onsuccess = function(e) {
      console.info('OIndexedDB::init()','=>','onsuccess()', e);

      self.db = e.target.result;
      callback(false, true);
    };

    request.onerror = function(e) {
      console.info('OIndexedDB::init()','=>','onerror()', e);

      callback(e.value);
    };
  };

  OIndexedDB.prototype._queue = function(queue, onItem, onFinished) {
    console.info('OIndexedDB::_queue()', queue);

    var index = 0;
    var trans = this.db.transaction([this.storeName], "readwrite");
    var store = trans.objectStore(this.storeName);

    function _process(item, cb) {
      console.info('OIndexedDB::_queue()','=>','_process()', index, item);
      if ( typeof item === 'undefined' ) {
        return cb('No such item');
      }
      onItem(item, trans, store, cb);
    }

    function _next() {
      if ( index >= queue.length ) {
        return onFinished(false, true);
      }

      console.info('OIndexedDB::_queue()','=>','_next()', index);
      _process(queue[index], function(error, result) {
        index++;

        _next();
      });
    }

    _next();
  };

  OIndexedDB.prototype.update = function(items, callback) {
    console.info('OIndexedDB::update()', items);

    var self = this;
    var nitems = [];
    Object.keys(items).forEach(function(k) {
      var rel = OSjs.Utils.cloneObject(items[k]);
      if ( self.primaryKey ) {
        rel[self.primaryKey] = k;
      }
      nitems.push(rel);
    });
    this.insert(nitems, callback);
  };

  OIndexedDB.prototype.insert = function(items, callback) {
    console.info('OIndexedDB::insert()', items);
    var queue = items instanceof Array ? items : [items];
    this._queue(queue, function(item, trans, store, cb) {
      var request = store.put(item);
      request.onsuccess = function(e) {
        console.info('OIndexedDB::insert()', '=>', 'onsuccess()', e);
        cb(false, e);
      };
      request.onerror = function(e) {
        console.info('OIndexedDB::insert()', '=>', 'onerror()', e);
        cb(e.value);
      };
    }, callback);
  };

  OIndexedDB.prototype.remove = function(items, callback) {
    console.info('OIndexedDB::remove()', items);
    var queue = items instanceof Array ? items : [items];
    this._queue(queue, function(item, trans, store, cb) {
      var request = store.delete(item);
      trans.oncomplete = function(e) {
        console.info('OIndexedDB::remove()', '=>', 'oncomplete()', e);
        cb(false, e);
      };
      request.onerror = function(e) {
        console.info('OIndexedDB::remove()', '=>', 'onerror()', e);
        cb(e.value);
      };
    }, callback);
  };

  OIndexedDB.prototype.list = function(args, callback) {
    args = args || {};
    console.info('OIndexedDB::list()', args);

    var list = [];
    var trans = this.db.transaction([this.storeName], "readwrite");
    var store = trans.objectStore(this.storeName);
    var keyRange = IDBKeyRange.lowerBound(0);
    var cursorRequest = store.openCursor(keyRange);

    cursorRequest.onsuccess = function(e) {
      console.info('OIndexedDB::list()', '=>', 'onsuccess()', e);

      var result = e.target.result;
      if ( !!result == false ) {
        console.info('OIndexedDB::list()', '=>', 'onsuccess()', 'FINISHED', list);
        callback(false, list);
        return;
      }
      if ( result ) {
        list.push(result.value);
      }
      result.continue();
    };

    cursorRequest.onerror = function(e) {
      console.info('OIndexedDB::list()', '=>', 'onerror()', e);

      callback(e.value);
    };
  };

  OIndexedDB.prototype.get = function(item, callback) {
    console.info('OIndexedDB::get()', item);

    var trans = this.db.transaction([this.storeName], "readonly");
    var store = trans.objectStore(this.storeName);
    var req = store.get(item);
    req.onsuccess = function(e) {
      console.info('OIndexedDB::get()', '=>', 'onsuccess()', e);

      callback(false, e.target.result, trans, store);
    };
    req.onerror = function(e) {
      console.info('OIndexedDB::get()', '=>', 'onerror()', e);

      callback(e.value);
    };
  };

  /////////////////////////////////////////////////////////////////////////////
  // EXPORTS
  /////////////////////////////////////////////////////////////////////////////

  OSjs.Drivers.IndexedDB = OSjs.Drivers.IndexedDB || {};

  OSjs.Drivers.IndexedDB.createInstance = function(args, callback) {
    return new OIndexedDB(args, callback);
  };

})(OSjs.Utils, OSjs.API);
<commit_msg>indexed-db: Better abstraction<commit_after>/*!
 * OS.js - JavaScript Operating System
 *
 * Copyright (c) 2011-2014, Anders Evenrud <andersevenrud@gmail.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice, this
 *    list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice,
 *    this list of conditions and the following disclaimer in the documentation
 *    and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @author  Anders Evenrud <andersevenrud@gmail.com>
 * @licence Simplified BSD License
 */

(function(Utils, API) {
  'use strict';

  var indexedDB      = window.indexedDB      || window.mozIndexedDB         || window.webkitIndexedDB || window.msIndexedDB;
  var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction;
  var IDBKeyRange    = window.IDBKeyRange    || window.webkitIDBKeyRange    || window.msIDBKeyRange;

  /////////////////////////////////////////////////////////////////////////////
  // API
  /////////////////////////////////////////////////////////////////////////////

  function OIndexedDB(args, callback) {
    args = args || {};

    this.version = (args.version || 1).toString();
    this.dbName = args.dbName;
    this.onupgrade = args.onupgrade || function __noop__() {};
    this.db = null;

    if ( !this.dbName ) {
      throw new Error('Cannot create IndexedDB without Database Name');
    }

    console.info('OIndexedDB::construct()', this);

    this.init(callback);
  }

  OIndexedDB.prototype.init = function(callback) {
    console.info('OIndexedDB::init()');

    var self = this;
    var request = indexedDB.open(this.dbName, this.version);

    request.onupgradeneeded = function(e) {
      console.info('OIndexedDB::init()','=>','onupgradeneeded()', e);

      var db = e.target.result;
      self.onupgrade(db, function() {
        callback(false, true);
      });
    };

    request.onsuccess = function(e) {
      console.info('OIndexedDB::init()','=>','onsuccess()', e);

      self.db = e.target.result;
      callback(false, true);
    };

    request.onerror = function(e) {
      console.info('OIndexedDB::init()','=>','onerror()', e);

      callback(e.value);
    };
  };

  OIndexedDB.prototype._queue = function(queue, onItem, onFinished) {
    console.info('OIndexedDB::_queue()', queue);

    var index = 0;

    function _process(item, cb) {
      console.info('OIndexedDB::_queue()','=>','_process()', index, item);
      if ( typeof item === 'undefined' ) {
        return cb('No such item');
      }
      onItem(item, cb);
    }

    function _next() {
      if ( index >= queue.length ) {
        return onFinished(false, true);
      }

      console.info('OIndexedDB::_queue()','=>','_next()', index);
      _process(queue[index], function(error, result) {
        index++;

        _next();
      });
    }

    _next();
  };

  OIndexedDB.prototype.update = function(args, items, callback) {
    args = args || {};

    console.info('OIndexedDB::update()', args, items);

    var self = this;
    var nitems = [];
    Object.keys(items).forEach(function(k) {
      var rel = OSjs.Utils.cloneObject(items[k]);
      if ( args.key ) {
        rel[args.key] = k;
      }
      nitems.push(rel);
    });
    this.insert(args, nitems, callback);
  };

  OIndexedDB.prototype.insert = function(args, items, callback) {
    args = args || {};

    console.info('OIndexedDB::insert()', args, items);

    var queue = items instanceof Array ? items : [items];
    var trans = this.db.transaction([args.store], "readwrite");
    var store = trans.objectStore(args.store);

    this._queue(queue, function(item, cb) {
      var request = store.put(item);
      request.onsuccess = function(e) {
        console.info('OIndexedDB::insert()', '=>', 'onsuccess()', e);
        cb(false, e);
      };
      request.onerror = function(e) {
        console.info('OIndexedDB::insert()', '=>', 'onerror()', e);
        cb(e.value);
      };
    }, callback);
  };

  OIndexedDB.prototype.remove = function(args, items, callback) {
    console.info('OIndexedDB::remove()', args, items);

    var queue = items instanceof Array ? items : [items];
    var trans = this.db.transaction([args.store], "readwrite");
    var store = trans.objectStore(args.store);

    this._queue(queue, function(item, cb) {
      var request = store.delete(item);
      trans.oncomplete = function(e) {
        console.info('OIndexedDB::remove()', '=>', 'oncomplete()', e);
        cb(false, e);
      };
      request.onerror = function(e) {
        console.info('OIndexedDB::remove()', '=>', 'onerror()', e);
        cb(e.value);
      };
    }, callback);
  };

  OIndexedDB.prototype.list = function(args, callback) {
    args = args || {};
    console.info('OIndexedDB::list()', args);

    var list = [];
    var trans = this.db.transaction([args.store], "readwrite");
    var store = trans.objectStore(args.store);
    var keyRange = IDBKeyRange.lowerBound(0);
    var cursorRequest = store.openCursor(keyRange);

    cursorRequest.onsuccess = function(e) {
      console.info('OIndexedDB::list()', '=>', 'onsuccess()', e);

      var result = e.target.result;
      if ( !!result == false ) {
        console.info('OIndexedDB::list()', '=>', 'onsuccess()', 'FINISHED', list);
        callback(false, list);
        return;
      }
      if ( result ) {
        list.push(result.value);
      }
      result.continue();
    };

    cursorRequest.onerror = function(e) {
      console.info('OIndexedDB::list()', '=>', 'onerror()', e);

      callback(e.value);
    };
  };

  OIndexedDB.prototype.get = function(args, item, callback) {
    args = args || {};

    console.info('OIndexedDB::get()', args, item);

    var trans = this.db.transaction([args.store], "readonly");
    var store = trans.objectStore(args.store);
    var req = store.get(item);
    req.onsuccess = function(e) {
      console.info('OIndexedDB::get()', '=>', 'onsuccess()', e);

      callback(false, e.target.result, trans, store);
    };
    req.onerror = function(e) {
      console.info('OIndexedDB::get()', '=>', 'onerror()', e);

      callback(e.value);
    };
  };

  /////////////////////////////////////////////////////////////////////////////
  // EXPORTS
  /////////////////////////////////////////////////////////////////////////////

  OSjs.Drivers.IndexedDB = OSjs.Drivers.IndexedDB || {};

  OSjs.Drivers.IndexedDB.createInstance = function(args, callback) {
    return new OIndexedDB(args, callback);
  };

})(OSjs.Utils, OSjs.API);
<|endoftext|>