<commit_before>import {
    combineReducers,
    createStore,
    applyMiddleware
} from 'redux';

import reduxThunk from 'redux-thunk';
import { createSelector } from 'reselect';
import semver from 'semver';

import * as Bank from './banks';
import * as Category from './categories';
import * as Settings from './settings';
import * as OperationType from './operation-types';
import * as Ui from './ui';

import { NEW_STATE } from './actions';

import {
    assert,
    assertHas,
    assertDefined,
    debug,
    MIN_WEBOOB_VERSION,
    normalizeVersion
} from '../helpers';

import * as backend from './backend';

import { genericErrorHandler } from '../errors';

// Augment basic reducers so that they can handle state reset:
// - if the event is a state reset, just pass the new sub-state.
// - otherwise, pass to the actual reducer.
function augmentReducer(reducer, field) {
    return (state, action) => {
        if (action.type === NEW_STATE) {
            return reducer(action.state[field], action);
        }
        return reducer(state, action);
    };
}

const rootReducer = combineReducers({
    banks: augmentReducer(Bank.reducer, 'banks'),
    categories: augmentReducer(Category.reducer, 'categories'),
    settings: augmentReducer(Settings.reducer, 'settings'),
    ui: augmentReducer(Ui.reducer, 'ui'),
    // Static information
    types: (state = {}) => state
});

// Store
export const rx = createStore(rootReducer, applyMiddleware(reduxThunk));

export const get = {

    // *** Banks **************************************************************
    // [Bank]
    banks(state) {
        assertDefined(state);
        return Bank.all(state.banks);
    },

    // String
    currentAccountId(state) {
        assertDefined(state);
        return Bank.getCurrentAccountId(state.banks);
    },

    // String
    currentAccessId(state) {
        assertDefined(state);
        return Bank.getCurrentAccessId(state.banks);
    },

    // Account
    currentAccount: createSelector(
        state => state.banks,
        state => get.currentAccountId(state),
        (banks, accountId) => {
            if (accountId === null) {
                debug('currentAccount: No account set.');
                return null;
            }
            return Bank.accountById(banks, accountId);
        }
    ),

    // Access
    currentAccess: createSelector(
        state => state.banks,
        state => get.currentAccessId(state),
        (banks, accessId) => {
            if (accessId === null) {
                debug('currentAccess: No access set.');
                return null;
            }
            return Bank.accessById(banks, accessId);
        }
    ),

    accessById(state, accessId) {
        assertDefined(state);
        return Bank.accessById(state.banks, accessId);
    },

    // [Access]
    accesses(state) {
        assertDefined(state);
        return Bank.getAccesses(state.banks);
    },

    // [Account]
    accountsByAccessId(state, accessId) {
        assertDefined(state);
        return Bank.accountsByAccessId(state.banks, accessId);
    },

    // [Account]
    currentAccounts(state) {
        assertDefined(state);
        let accessId = this.currentAccessId(state);
        if (accessId === null) {
            debug('currentAccounts: No current bank set.');
            return [];
        }
        return this.accountsByAccessId(state, accessId);
    },

    // [Operation]
    operationsByAccountIds(state, accountIds) {
        assertDefined(state);

        let accountIdsArray = accountIds;
        if (!(accountIdsArray instanceof Array)) {
            accountIdsArray = [accountIdsArray];
        }

        let operations = [];
        for (let accountId of accountIdsArray) {
            operations = operations.concat(Bank.operationsByAccountId(state.banks, accountId));
        }
        return operations;
    },

    // [Operation]
    currentOperations: createSelector(
        state => state.banks,
        state => get.currentAccountId(state),
        (banks, accountId) => Bank.operationsByAccountId(banks, accountId)
    ),

    // Operation
    operationById(state, id) {
        assertDefined(state);
        return Bank.operationById(state.banks, id);
    },

    // String
    defaultAccountId(state) {
        assertDefined(state);
        return Settings.getDefaultAccountId(state.settings);
    },

    // [Type]
    types(state) {
        assertDefined(state);
        return OperationType.all(state.types);
    },

    // *** UI *****************************************************************
    // { searchFields } (see ui.js)
    searchFields(state) {
        assertDefined(state);
        return Ui.getSearchFields(state.ui);
    },

    // Bool
    hasSearchFields(state) {
        assertDefined(state);
        return Ui.hasSearchFields(state.ui);
    },

    // Bool
    backgroundProcessingReason(state) {
        assertDefined(state);
        return Settings.backgroundProcessingReason(state.settings) ||
               Bank.backgroundProcessingReason(state.banks);
    },

    // *** Categories *********************************************************
    // Categories
    categories(state) {
        assertDefined(state);
        return Category.all(state.categories);
    },

    categoriesButNone(state) {
        assertDefined(state);
        return Category.allButNone(state.categories);
    },

    // Category
    categoryById(state, id) {
        assertDefined(state);
        return Category.fromId(state.categories, id);
    },

    // *** Settings ***********************************************************
    // String
    setting(state, key) {
        assertDefined(state);
        return Settings.get(state.settings, key);
    },

    // Bool
    boolSetting(state, key) {
        assertDefined(state);
        let val = this.setting(state, key);
        assert(val === 'true' || val === 'false', 'A bool setting must be true or false');
        return val === 'true';
    },

    // Bool
    isWeboobInstalled(state) {
        assertDefined(state);
        if (!this.boolSetting(state, 'weboob-installed'))
            return false;

        let version = normalizeVersion(this.setting(state, 'weboob-version'));

        return semver(version) && semver.gt(version, normalizeVersion(MIN_WEBOOB_VERSION));
    },

    // Bool
    isWeboobUpdating(state) {
        assertDefined(state);
        return Settings.isWeboobUpdating(state.settings);
    },

    // Bool
    isSendingTestEmail(state) {
        assertDefined(state);
        return Settings.isSendingTestEmail(state.settings);
    },

    // Returns [{account, alert}] of the given type.
    alerts(state, type) {
        assertDefined(state);
        return Bank.alertPairsByType(state.banks, type);
    }
};

export const actions = {

    // *** Banks **************************************************************
    runSync(dispatch) {
        assertDefined(dispatch);
        dispatch(Bank.runSync(get));
    },

    setOperationCategory(dispatch, operation, catId) {
        assertDefined(dispatch);
        dispatch(Bank.setOperationCategory(operation, catId));
    },

    setOperationType(dispatch, operation, type) {
        assertDefined(dispatch);
        dispatch(Bank.setOperationType(operation, type));
    },

    setOperationCustomLabel(dispatch, operation, label) {
        assertDefined(dispatch);
        dispatch(Bank.setOperationCustomLabel(operation, label));
    },

    mergeOperations(dispatch, toKeep, toRemove) {
        assertDefined(dispatch);
        dispatch(Bank.mergeOperations(toKeep, toRemove));
    },

    // *** Categories *********************************************************
    createCategory(dispatch, category) {
        assertDefined(dispatch);
        dispatch(Category.create(category));
    },

    updateCategory(dispatch, former, newer) {
        assertDefined(dispatch);
        dispatch(Category.update(former, newer));
    },

    deleteCategory(dispatch, former, replaceById) {
        assertDefined(dispatch);
        dispatch(Category.destroy(former, replaceById));
    },

    // *** UI *****************************************************************
    setCurrentAccountId(dispatch, id) {
        assertDefined(dispatch);
        dispatch(Bank.setCurrentAccountId(id));
    },

    setSearchField(dispatch, key, value) {
        assertDefined(dispatch);
        dispatch(Ui.setSearchField(key, value));
    },

    setSearchFields(dispatch, map) {
        assertDefined(dispatch);
        dispatch(Ui.setSearchFields(map));
    },

    resetSearch(dispatch) {
        assertDefined(dispatch);
        dispatch(Ui.resetSearch());
    },

    // *** Settings ***********************************************************
    updateWeboob(dispatch) {
        assertDefined(dispatch);
        dispatch(Settings.updateWeboob());
    },

    setSetting(dispatch, key, value) {
        assertDefined(dispatch);
        dispatch(Settings.set(key, value));
    },

    setBoolSetting(dispatch, key, value) {
        assertDefined(dispatch);
        assert(typeof value === 'boolean', 'value must be a boolean');
        this.setSetting(dispatch, key, value.toString());
    },

    sendTestEmail(dispatch, config) {
        assertDefined(dispatch);
        dispatch(Settings.sendTestEmail(config));
    },

    runAccountsSync(dispatch, accessId) {
        assertDefined(dispatch);
        dispatch(Bank.runAccountsSync(accessId));
    },

    resyncBalance(dispatch, accountId) {
        assertDefined(dispatch);
        dispatch(Bank.resyncBalance(accountId));
    },

    deleteAccount(dispatch, accountId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteAccount(accountId, get));
    },

    createAccess(dispatch, uuid, login, password, fields) {
        assertDefined(dispatch);
        dispatch(Bank.createAccess(get, uuid, login, password, fields));
    },

    updateAccess(dispatch, accessId, login, password, customFields) {
        assertDefined(dispatch);

        assert(typeof accessId === 'string', 'second param accessId must be a string');
        assert(typeof password === 'string', 'third param must be the password');

        if (typeof login !== 'undefined') {
            assert(typeof login === 'string', 'fourth param must be the login');
        }

        if (typeof customFields !== 'undefined') {
            assert(customFields instanceof Array &&
                   customFields.every(f => assertHas(f, 'name') && assertHas(f, 'value')),
                   'if not omitted, third param must have the shape [{name, value}]');
        }

        dispatch(Settings.updateAccess(accessId, login, password, customFields));
    },

    deleteAccess(dispatch, accessId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteAccess(accessId, get));
    },

    createOperation(dispatch, newOperation) {
        assertDefined(dispatch);
        dispatch(Bank.createOperation(newOperation));
    },

    deleteOperation(dispatch, operationId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteOperation(operationId));
    },

    importInstance(dispatch, content) {
        assertDefined(dispatch);
        dispatch(Settings.importInstance(content));
    },

    exportInstance(dispatch, maybePassword) {
        assertDefined(dispatch);
        dispatch(Settings.exportInstance(maybePassword));
    },

    createAlert(dispatch, newAlert) {
        assertDefined(dispatch);
        dispatch(Bank.createAlert(newAlert));
    },

    updateAlert(dispatch, alertId, newFields) {
        assertDefined(dispatch);
        dispatch(Bank.updateAlert(alertId, newFields));
    },

    deleteAlert(dispatch, alertId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteAlert(alertId));
    }
};

export const store = {};

export function init() {
    return backend.init().then(world => {
        let state = {};

        // Settings need to be loaded first, because locale information depends
        // upon them.
        assertHas(world, 'settings');
        state.settings = Settings.initialState(world.settings);

        assertHas(world, 'categories');
        state.categories = Category.initialState(world.categories);

        // Define external values for the Bank initialState:
        let external = {
            defaultCurrency: get.setting(state, 'defaultCurrency'),
            defaultAccountId: get.defaultAccountId(state)
        };

        assertHas(world, 'accounts');
        assertHas(world, 'operations');
        assertHas(world, 'alerts');
        state.banks = Bank.initialState(external, world.accounts, world.operations, world.alerts);
        state.types = OperationType.initialState();
        // The UI must be computed at the end.
        state.ui = Ui.initialState();

        return new Promise(accept => {
            accept(state);
        });
    })
    .catch(genericErrorHandler);
}
<commit_msg>Allow weboob version equal to '1.2'<commit_after>import {
    combineReducers,
    createStore,
    applyMiddleware
} from 'redux';

import reduxThunk from 'redux-thunk';
import { createSelector } from 'reselect';
import semver from 'semver';

import * as Bank from './banks';
import * as Category from './categories';
import * as Settings from './settings';
import * as OperationType from './operation-types';
import * as Ui from './ui';

import { NEW_STATE } from './actions';

import {
    assert,
    assertHas,
    assertDefined,
    debug,
    MIN_WEBOOB_VERSION,
    normalizeVersion
} from '../helpers';

import * as backend from './backend';

import { genericErrorHandler } from '../errors';

// Augment basic reducers so that they can handle state reset:
// - if the event is a state reset, just pass the new sub-state.
// - otherwise, pass to the actual reducer.
function augmentReducer(reducer, field) {
    return (state, action) => {
        if (action.type === NEW_STATE) {
            return reducer(action.state[field], action);
        }
        return reducer(state, action);
    };
}

const rootReducer = combineReducers({
    banks: augmentReducer(Bank.reducer, 'banks'),
    categories: augmentReducer(Category.reducer, 'categories'),
    settings: augmentReducer(Settings.reducer, 'settings'),
    ui: augmentReducer(Ui.reducer, 'ui'),
    // Static information
    types: (state = {}) => state
});

// Store
export const rx = createStore(rootReducer, applyMiddleware(reduxThunk));

export const get = {

    // *** Banks **************************************************************
    // [Bank]
    banks(state) {
        assertDefined(state);
        return Bank.all(state.banks);
    },

    // String
    currentAccountId(state) {
        assertDefined(state);
        return Bank.getCurrentAccountId(state.banks);
    },

    // String
    currentAccessId(state) {
        assertDefined(state);
        return Bank.getCurrentAccessId(state.banks);
    },

    // Account
    currentAccount: createSelector(
        state => state.banks,
        state => get.currentAccountId(state),
        (banks, accountId) => {
            if (accountId === null) {
                debug('currentAccount: No account set.');
                return null;
            }
            return Bank.accountById(banks, accountId);
        }
    ),

    // Access
    currentAccess: createSelector(
        state => state.banks,
        state => get.currentAccessId(state),
        (banks, accessId) => {
            if (accessId === null) {
                debug('currentAccess: No access set.');
                return null;
            }
            return Bank.accessById(banks, accessId);
        }
    ),

    accessById(state, accessId) {
        assertDefined(state);
        return Bank.accessById(state.banks, accessId);
    },

    // [Access]
    accesses(state) {
        assertDefined(state);
        return Bank.getAccesses(state.banks);
    },

    // [Account]
    accountsByAccessId(state, accessId) {
        assertDefined(state);
        return Bank.accountsByAccessId(state.banks, accessId);
    },

    // [Account]
    currentAccounts(state) {
        assertDefined(state);
        let accessId = this.currentAccessId(state);
        if (accessId === null) {
            debug('currentAccounts: No current bank set.');
            return [];
        }
        return this.accountsByAccessId(state, accessId);
    },

    // [Operation]
    operationsByAccountIds(state, accountIds) {
        assertDefined(state);

        let accountIdsArray = accountIds;
        if (!(accountIdsArray instanceof Array)) {
            accountIdsArray = [accountIdsArray];
        }

        let operations = [];
        for (let accountId of accountIdsArray) {
            operations = operations.concat(Bank.operationsByAccountId(state.banks, accountId));
        }
        return operations;
    },

    // [Operation]
    currentOperations: createSelector(
        state => state.banks,
        state => get.currentAccountId(state),
        (banks, accountId) => Bank.operationsByAccountId(banks, accountId)
    ),

    // Operation
    operationById(state, id) {
        assertDefined(state);
        return Bank.operationById(state.banks, id);
    },

    // String
    defaultAccountId(state) {
        assertDefined(state);
        return Settings.getDefaultAccountId(state.settings);
    },

    // [Type]
    types(state) {
        assertDefined(state);
        return OperationType.all(state.types);
    },

    // *** UI *****************************************************************
    // { searchFields } (see ui.js)
    searchFields(state) {
        assertDefined(state);
        return Ui.getSearchFields(state.ui);
    },

    // Bool
    hasSearchFields(state) {
        assertDefined(state);
        return Ui.hasSearchFields(state.ui);
    },

    // Bool
    backgroundProcessingReason(state) {
        assertDefined(state);
        return Settings.backgroundProcessingReason(state.settings) ||
               Bank.backgroundProcessingReason(state.banks);
    },

    // *** Categories *********************************************************
    // Categories
    categories(state) {
        assertDefined(state);
        return Category.all(state.categories);
    },

    categoriesButNone(state) {
        assertDefined(state);
        return Category.allButNone(state.categories);
    },

    // Category
    categoryById(state, id) {
        assertDefined(state);
        return Category.fromId(state.categories, id);
    },

    // *** Settings ***********************************************************
    // String
    setting(state, key) {
        assertDefined(state);
        return Settings.get(state.settings, key);
    },

    // Bool
    boolSetting(state, key) {
        assertDefined(state);
        let val = this.setting(state, key);
        assert(val === 'true' || val === 'false', 'A bool setting must be true or false');
        return val === 'true';
    },

    // Bool
    isWeboobInstalled(state) {
        assertDefined(state);
        if (!this.boolSetting(state, 'weboob-installed'))
            return false;

        let version = normalizeVersion(this.setting(state, 'weboob-version'));

        return semver(version) && semver.gte(version, normalizeVersion(MIN_WEBOOB_VERSION));
    },

    // Bool
    isWeboobUpdating(state) {
        assertDefined(state);
        return Settings.isWeboobUpdating(state.settings);
    },

    // Bool
    isSendingTestEmail(state) {
        assertDefined(state);
        return Settings.isSendingTestEmail(state.settings);
    },

    // Returns [{account, alert}] of the given type.
    alerts(state, type) {
        assertDefined(state);
        return Bank.alertPairsByType(state.banks, type);
    }
};

export const actions = {

    // *** Banks **************************************************************
    runSync(dispatch) {
        assertDefined(dispatch);
        dispatch(Bank.runSync(get));
    },

    setOperationCategory(dispatch, operation, catId) {
        assertDefined(dispatch);
        dispatch(Bank.setOperationCategory(operation, catId));
    },

    setOperationType(dispatch, operation, type) {
        assertDefined(dispatch);
        dispatch(Bank.setOperationType(operation, type));
    },

    setOperationCustomLabel(dispatch, operation, label) {
        assertDefined(dispatch);
        dispatch(Bank.setOperationCustomLabel(operation, label));
    },

    mergeOperations(dispatch, toKeep, toRemove) {
        assertDefined(dispatch);
        dispatch(Bank.mergeOperations(toKeep, toRemove));
    },

    // *** Categories *********************************************************
    createCategory(dispatch, category) {
        assertDefined(dispatch);
        dispatch(Category.create(category));
    },

    updateCategory(dispatch, former, newer) {
        assertDefined(dispatch);
        dispatch(Category.update(former, newer));
    },

    deleteCategory(dispatch, former, replaceById) {
        assertDefined(dispatch);
        dispatch(Category.destroy(former, replaceById));
    },

    // *** UI *****************************************************************
    setCurrentAccountId(dispatch, id) {
        assertDefined(dispatch);
        dispatch(Bank.setCurrentAccountId(id));
    },

    setSearchField(dispatch, key, value) {
        assertDefined(dispatch);
        dispatch(Ui.setSearchField(key, value));
    },

    setSearchFields(dispatch, map) {
        assertDefined(dispatch);
        dispatch(Ui.setSearchFields(map));
    },

    resetSearch(dispatch) {
        assertDefined(dispatch);
        dispatch(Ui.resetSearch());
    },

    // *** Settings ***********************************************************
    updateWeboob(dispatch) {
        assertDefined(dispatch);
        dispatch(Settings.updateWeboob());
    },

    setSetting(dispatch, key, value) {
        assertDefined(dispatch);
        dispatch(Settings.set(key, value));
    },

    setBoolSetting(dispatch, key, value) {
        assertDefined(dispatch);
        assert(typeof value === 'boolean', 'value must be a boolean');
        this.setSetting(dispatch, key, value.toString());
    },

    sendTestEmail(dispatch, config) {
        assertDefined(dispatch);
        dispatch(Settings.sendTestEmail(config));
    },

    runAccountsSync(dispatch, accessId) {
        assertDefined(dispatch);
        dispatch(Bank.runAccountsSync(accessId));
    },

    resyncBalance(dispatch, accountId) {
        assertDefined(dispatch);
        dispatch(Bank.resyncBalance(accountId));
    },

    deleteAccount(dispatch, accountId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteAccount(accountId, get));
    },

    createAccess(dispatch, uuid, login, password, fields) {
        assertDefined(dispatch);
        dispatch(Bank.createAccess(get, uuid, login, password, fields));
    },

    updateAccess(dispatch, accessId, login, password, customFields) {
        assertDefined(dispatch);

        assert(typeof accessId === 'string', 'second param accessId must be a string');
        assert(typeof password === 'string', 'third param must be the password');

        if (typeof login !== 'undefined') {
            assert(typeof login === 'string', 'fourth param must be the login');
        }

        if (typeof customFields !== 'undefined') {
            assert(customFields instanceof Array &&
                   customFields.every(f => assertHas(f, 'name') && assertHas(f, 'value')),
                   'if not omitted, third param must have the shape [{name, value}]');
        }

        dispatch(Settings.updateAccess(accessId, login, password, customFields));
    },

    deleteAccess(dispatch, accessId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteAccess(accessId, get));
    },

    createOperation(dispatch, newOperation) {
        assertDefined(dispatch);
        dispatch(Bank.createOperation(newOperation));
    },

    deleteOperation(dispatch, operationId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteOperation(operationId));
    },

    importInstance(dispatch, content) {
        assertDefined(dispatch);
        dispatch(Settings.importInstance(content));
    },

    exportInstance(dispatch, maybePassword) {
        assertDefined(dispatch);
        dispatch(Settings.exportInstance(maybePassword));
    },

    createAlert(dispatch, newAlert) {
        assertDefined(dispatch);
        dispatch(Bank.createAlert(newAlert));
    },

    updateAlert(dispatch, alertId, newFields) {
        assertDefined(dispatch);
        dispatch(Bank.updateAlert(alertId, newFields));
    },

    deleteAlert(dispatch, alertId) {
        assertDefined(dispatch);
        dispatch(Bank.deleteAlert(alertId));
    }
};

export const store = {};

export function init() {
    return backend.init().then(world => {
        let state = {};

        // Settings need to be loaded first, because locale information depends
        // upon them.
        assertHas(world, 'settings');
        state.settings = Settings.initialState(world.settings);

        assertHas(world, 'categories');
        state.categories = Category.initialState(world.categories);

        // Define external values for the Bank initialState:
        let external = {
            defaultCurrency: get.setting(state, 'defaultCurrency'),
            defaultAccountId: get.defaultAccountId(state)
        };

        assertHas(world, 'accounts');
        assertHas(world, 'operations');
        assertHas(world, 'alerts');
        state.banks = Bank.initialState(external, world.accounts, world.operations, world.alerts);
        state.types = OperationType.initialState();
        // The UI must be computed at the end.
        state.ui = Ui.initialState();

        return new Promise(accept => {
            accept(state);
        });
    })
    .catch(genericErrorHandler);
}
<|endoftext|>


<commit_before>import { createStore, applyMiddleware, compose } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import TebukuroApp from '../reducers'

export default (preloadState = {}, history) => {
  const middlewares = [
    routerMiddleware(history)
  ]

  const composeEnhancers =
    process.env.NODE_ENV !== 'production' &&
    typeof window === 'object' &&
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose

  const enhancer = composeEnhancers(applyMiddleware(...middlewares))

  const store = createStore(TebukuroApp, {}, enhancer)

  if (module.hot) {
    module.hot.accept('../reducers', () =>
      store.replaceReducer(require('../reducers').default)
    )
  }

  return store
}
<commit_msg>Add redux-promise middleware<commit_after>import { createStore, applyMiddleware, compose } from 'redux'
import { routerMiddleware } from 'react-router-redux'
import promiseMiddleware from 'redux-promise'
import TebukuroApp from '../reducers'

export default (preloadState = {}, history) => {
  const middlewares = [
    routerMiddleware(history),
    promiseMiddleware
  ]

  const composeEnhancers =
    process.env.NODE_ENV !== 'production' &&
    typeof window === 'object' &&
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
      window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose

  const enhancer = composeEnhancers(applyMiddleware(...middlewares))

  const store = createStore(TebukuroApp, {}, enhancer)

  if (module.hot) {
    module.hot.accept('../reducers', () =>
      store.replaceReducer(require('../reducers').default)
    )
  }

  return store
}
<|endoftext|>


<commit_before>function Swipe(container, options) {
	options = options || {};

	// utilities
	var noop = function () {}; // simple no operation function
	var offloadFn = function (fn) {
		setTimeout(fn || noop, 0)
	}; // offload a functions execution

	// check browser capabilities
	var browser = {
		addEventListener: !!window.addEventListener,
		touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
		transitions: (function (temp) {
			var props = ['transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform'];
			for (var i in props) if (temp.style[ props[i] ] !== undefined) return true;
			return false;
		})(document.createElement('swipe'))
	};

	// quit if no root element
	if (!container) return;
	var element = container.children[0], slides, slidePos, width, height, vertical = options.vertical,
		index = parseInt(options.startSlide, 10) || 0, speed = options.speed || 300;

	function setup() {
		// cache slides
		slides = element.children;
		// create an array to store current positions of each slide
		slidePos = new Array(slides.length);
		if (vertical) {
			// determine width of each slide
			height = container.getBoundingClientRect().height || container.offsetHeight;
			element.style.height = (slides.length * height) + 'px';
		} else {
			// determine width of each slide
			width = container.getBoundingClientRect().width || container.offsetWidth;
			element.style.width = (slides.length * width) + 'px';
		}

		// stack elements
		var pos = slides.length;
		while (pos--) {
			var slide = slides[pos];
			if (vertical) {
				slide.style.height = height + 'px';
				if (browser.transitions) {
					slide.style.top = (pos * -height) + 'px';
					moveY(pos, index > pos ? -height : (index < pos ? height : 0), 0);
				}
			} else {
				slide.style.width = width + 'px';
				if (browser.transitions) {
					slide.style.left = (pos * -width) + 'px';
					move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
				}
			}

			slide.setAttribute('data-index', pos);
		}

		if (!browser.transitions)
			if (vertical)
				element.style.top = (index * -height) + 'px';
			else
				element.style.left = (index * -width) + 'px';

		container.style.visibility = 'visible';
	}

	function prev() {
		if (index)
			slide(index - 1);
		else if (options.continuous)
			slide(slides.length - 1);
	}

	function next() {
		if (index < slides.length - 1)
			slide(index+1);
		else if (options.continuous)
			slide(0);
	}

	function slide(to, slideSpeed) {
		// do nothing if already on requested slide
		if (index == to) return;

		if (browser.transitions) {
			var diff = Math.abs(index-to) - 1;
			var direction = Math.abs(index-to) / (index-to); // 1:right -1:left

			while (diff--) move((to > index ? to : index) - diff - 1, width * direction, 0);

			move(index, width * direction, slideSpeed || speed);
			move(to, 0, slideSpeed || speed);
		} else {
			animate(index * -width, to * -width, slideSpeed || speed);
		}

		index = to;

		offloadFn(options.callback && options.callback(index, slides[index]));
	}

	function move(index, dist, speed) {
		translate(index, dist, speed);
		slidePos[index] = dist;
	}

	function translate(index, dist, speed) {
		var slide = slides[index], style = slide && slide.style;

		if (!style) return;

		style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.transitionDuration = speed + 'ms';

		style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
		style.msTransform = style.MozTransform = 'translateX(' + dist + 'px)';
	}

	function moveY(index, dist, speed) {
		translateY(index, dist, speed);
		slidePos[index] = dist;
	}

	function translateY(index, dist, speed) {
		var slide = slides[index], style = slide && slide.style;

		if (!style) return;

		style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.transitionDuration = speed + 'ms';

		style.webkitTransform = 'translate(0,' + dist + 'px)' + 'translateZ(0)';
		style.msTransform = style.MozTransform = 'translateY(' + dist + 'px)';
	}

	// setup initial vars
	var start = {}, delta = {}, isScrolling;

	// setup event capturing
	var events = {
		handleEvent: function (event) {
			switch (event.type) {
				case 'touchstart':
				case 'mousedown':
					this.start(event);
					break;
				case 'touchmove':
				case 'mousemove':
					this.move(event);
					break;
				case 'touchend':
				case 'mouseup':
					offloadFn(this.end(event));
					dorado.util.DISABLE_SCROLLER_DRAG = false;
					break;
				case 'webkitTransitionEnd':
				case 'msTransitionEnd':
				case 'oTransitionEnd':
				case 'otransitionend':
				case 'transitionend':
					offloadFn(this.transitionEnd(event));
					break;
				case 'resize':
					offloadFn(setup.call());
					break;
			}
			if (options.stopPropagation) event.stopPropagation();
		},
		start: function (event) {
			var touches = browser.touch ? event.touches[0] : event;

			// measure start values
			start = {
				// get initial touch coords
				x: touches.pageX,
				y: touches.pageY,

				// store time to determine touch duration
				time: +new Date
			};

			// used for testing first move event
			isScrolling = undefined;

			// reset delta and end measurements
			delta = {};

			// attach touchmove and touchend listeners
			if (browser.touch) {
				element.addEventListener('touchmove', this, false);
				element.addEventListener('touchend', this, false);
			} else {
				document.addEventListener('mousemove', this, false);
				document.addEventListener('mouseup', this, false);
			}
		},
		move: function (event) {
			// ensure swiping with one touch and not pinching
			if (browser.touch && ((event.touches.length > 1) || (event.scale && event.scale !== 1))) return;

			if (options.disableScroll) event.preventDefault();

			var touches = browser.touch ? event.touches[0] : event;
			// measure change in x and y
			delta = {
				x: touches.pageX - start.x,
				y: touches.pageY - start.y
			};

			// determine if scrolling test has run - one time test
			if (typeof isScrolling == 'undefined') {
				if (vertical)
					isScrolling = !!( isScrolling || Math.abs(delta.x) > Math.abs(delta.y) );
				else
					isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
			}

			// if user is not trying to scroll vertically
			if (!isScrolling) {
				// prevent native scrolling
				event.preventDefault();
				if (!vertical) {
					// increase resistance if first or last slide
					delta.x =
						delta.x /
						( (!index && delta.x > 0               // if first slide and sliding left
							|| index == slides.length - 1        // or if last slide and sliding right
							&& delta.x < 0                       // and if sliding at all
						) ?
							( Math.abs(delta.x) / width + 1 )      // determine resistance level
							: 1 );                                 // no resistance if false

					if (Math.abs(delta.x) > 5) {
						dorado.util.DISABLE_SCROLLER_DRAG = true;
					}

					// translate 1:1
					translate(index - 1, delta.x + slidePos[index - 1], 0);
					translate(index, delta.x + slidePos[index], 0);
					translate(index + 1, delta.x + slidePos[index + 1], 0);
				} else {
					// increase resistance if first or last slide
					delta.y =
						delta.y /
						( (!index && delta.y > 0               // if first slide and sliding left
							|| index == slides.length - 1        // or if last slide and sliding right
							&& delta.y < 0                       // and if sliding at all
						) ?
							( Math.abs(delta.y) / height + 1 )      // determine resistance level
							: 1 );                                 // no resistance if false

					if (Math.abs(delta.y) > 5) {
						dorado.util.DISABLE_SCROLLER_DRAG = true;
					}

					// translate 1:1
					translateY(index - 1, delta.y + slidePos[index - 1], 0);
					translateY(index, delta.y + slidePos[index], 0);
					translateY(index + 1, delta.y + slidePos[index + 1], 0);
				}
			}
		},
		end: function (event) {
			// measure duration
			var duration = +new Date - start.time, isValidSlide, isPastBounds, direction;
			if (!vertical) {
				// determine if slide attempt triggers next/prev slide
				isValidSlide =
					Number(duration) < 250               // if slide duration is less than 250ms
					&& Math.abs(delta.x) > 20            // and if slide amt is greater than 20px
					|| Math.abs(delta.x) > width / 2;      // or if slide amt is greater than half the width

				// determine if slide attempt is past start and end
				isPastBounds =
					!index && delta.x > 0                            // if first slide and slide amt is greater than 0
					|| index == slides.length - 1 && delta.x < 0;    // or if last slide and slide amt is less than 0

				// determine direction of swipe (true:right, false:left)
				direction = delta.x < 0;

				// if not scrolling vertically
				if (!isScrolling) {
					if (isValidSlide && !isPastBounds) {
						if (direction) {
							move(index - 1, -width, 0);
							move(index, slidePos[index] - width, speed);
							move(index + 1, slidePos[index + 1] - width, speed);
							index += 1;
						} else {
							move(index + 1, width, 0);
							move(index, slidePos[index] + width, speed);
							move(index - 1, slidePos[index - 1] + width, speed);
							index += -1;
						}

						options.callback && options.callback(index, slides[index]);
					} else {
						move(index - 1, -width, speed);
						move(index, 0, speed);
						move(index + 1, width, speed);
					}
				}
			} else {
				// determine if slide attempt triggers next/prev slide
				isValidSlide =
					Number(duration) < 250               // if slide duration is less than 250ms
					&& Math.abs(delta.y) > 20            // and if slide amt is greater than 20px
					|| Math.abs(delta.y) > height / 2;      // or if slide amt is greater than half the width

				// determine if slide attempt is past start and end
				isPastBounds =
					!index && delta.y > 0                            // if first slide and slide amt is greater than 0
					|| index == slides.length - 1 && delta.y < 0;    // or if last slide and slide amt is less than 0

				// determine direction of swipe (true:bottom, false:top)
				direction = delta.y < 0;

				// if not scrolling vertically
				if (!isScrolling) {
					if (isValidSlide && !isPastBounds) {
						if (direction) {
							moveY(index - 1, -height, 0);
							moveY(index, slidePos[index] - height, speed);
							moveY(index + 1, slidePos[index + 1] - height, speed);
							index += 1;
						} else {
							moveY(index + 1, height, 0);
							moveY(index, slidePos[index] + height, speed);
							moveY(index - 1, slidePos[index - 1] + height, speed);
							index += -1;
						}

						options.callback && options.callback(index, slides[index]);
					} else {
						moveY(index - 1, -height, speed);
						moveY(index, 0, speed);
						moveY(index + 1, height, speed);
					}
				}
			}

			// kill touchmove and touchend event listeners until touchstart called again
			if (browser.touch) {
				element.removeEventListener('touchmove', events, false);
				element.removeEventListener('touchend', events, false);
			} else {
				document.removeEventListener('mousemove', events, false);
				document.removeEventListener('mouseup', events, false);
			}
		},
		transitionEnd: function (event) {
			if (parseInt(event.target.getAttribute('data-index'), 10) == index) {
				options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
			}
		}
	};

	// trigger setup
	setup();

	// add event listeners
	if (browser.addEventListener) {
		// set touchstart event on element
		if (browser.touch)
			element.addEventListener('touchstart', events, false);
		else
			element.addEventListener('mousedown', events, false);

		if (browser.transitions) {
			element.addEventListener('webkitTransitionEnd', events, false);
			element.addEventListener('msTransitionEnd', events, false);
			element.addEventListener('oTransitionEnd', events, false);
			element.addEventListener('otransitionend', events, false);
			element.addEventListener('transitionend', events, false);
		}

		// set resize event on window
		window.addEventListener('resize', events, false);
	}

	// expose the Swipe API
	return {
		setup: function () {
			setup();
		},
		refresh: function() {
			setup();
		},
		getPos: function () {
			// return current index position
			return index;
		},
		setPos: function(pos) {
			index = pos;
			setup();
		},
		prev: function() {
			prev();
		},
		next: function() {
			next();
		},
		kill: function () {
			// reset element
			element.style.width = 'auto';
			element.style.left = 0;

			// reset slides
			var pos = slides.length;
			while (pos--) {
				var slide = slides[pos];
				if (!vertical) {
					slide.style.width = '100%';
					slide.style.left = 0;
				} else {
					slide.style.height = '100%';
					slide.style.top = 0;
				}

				if (browser.transitions) translate(pos, 0, 0);
			}

			// removed event listeners
			if (browser.addEventListener) {
				// remove current event listeners
				element.removeEventListener('touchstart', events, false);
				element.removeEventListener('webkitTransitionEnd', events, false);
				element.removeEventListener('msTransitionEnd', events, false);
				element.removeEventListener('oTransitionEnd', events, false);
				element.removeEventListener('otransitionend', events, false);
				element.removeEventListener('transitionend', events, false);
				window.removeEventListener('resize', events, false);
			} else {
				window.onresize = null;
			}
		}
	}
}<commit_msg>重名为cola<commit_after>function Swipe(container, options) {
	options = options || {};

	// utilities
	var noop = function () {}; // simple no operation function
	var offloadFn = function (fn) {
		setTimeout(fn || noop, 0)
	}; // offload a functions execution

	// check browser capabilities
	var browser = {
		addEventListener: !!window.addEventListener,
		touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch,
		transitions: (function (temp) {
			var props = ['transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform'];
			for (var i in props) if (temp.style[ props[i] ] !== undefined) return true;
			return false;
		})(document.createElement('swipe'))
	};

	// quit if no root element
	if (!container) return;
	var element = container.children[0], slides, slidePos, width, height, vertical = options.vertical,
		index = parseInt(options.startSlide, 10) || 0, speed = options.speed || 300;

	function setup() {
		// cache slides
		slides = element.children;
		// create an array to store current positions of each slide
		slidePos = new Array(slides.length);
		if (vertical) {
			// determine width of each slide
			height = container.getBoundingClientRect().height || container.offsetHeight;
			element.style.height = (slides.length * height) + 'px';
		} else {
			// determine width of each slide
			width = container.getBoundingClientRect().width || container.offsetWidth;
			element.style.width = (slides.length * width) + 'px';
		}

		// stack elements
		var pos = slides.length;
		while (pos--) {
			var slide = slides[pos];
			if (vertical) {
				slide.style.height = height + 'px';
				if (browser.transitions) {
					slide.style.top = (pos * -height) + 'px';
					moveY(pos, index > pos ? -height : (index < pos ? height : 0), 0);
				}
			} else {
				slide.style.width = width + 'px';
				if (browser.transitions) {
					slide.style.left = (pos * -width) + 'px';
					move(pos, index > pos ? -width : (index < pos ? width : 0), 0);
				}
			}

			slide.setAttribute('data-index', pos);
		}

		if (!browser.transitions)
			if (vertical)
				element.style.top = (index * -height) + 'px';
			else
				element.style.left = (index * -width) + 'px';

		container.style.visibility = 'visible';
	}

	function prev() {
		if (index)
			slide(index - 1);
		else if (options.continuous)
			slide(slides.length - 1);
	}

	function next() {
		if (index < slides.length - 1)
			slide(index+1);
		else if (options.continuous)
			slide(0);
	}

	function slide(to, slideSpeed) {
		// do nothing if already on requested slide
		if (index == to) return;

		if (browser.transitions) {
			var diff = Math.abs(index-to) - 1;
			var direction = Math.abs(index-to) / (index-to); // 1:right -1:left

			while (diff--) move((to > index ? to : index) - diff - 1, width * direction, 0);

			move(index, width * direction, slideSpeed || speed);
			move(to, 0, slideSpeed || speed);
		} else {
			animate(index * -width, to * -width, slideSpeed || speed);
		}

		index = to;

		offloadFn(options.callback && options.callback(index, slides[index]));
	}

	function move(index, dist, speed) {
		translate(index, dist, speed);
		slidePos[index] = dist;
	}

	function translate(index, dist, speed) {
		var slide = slides[index], style = slide && slide.style;

		if (!style) return;

		style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.transitionDuration = speed + 'ms';

		style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)';
		style.msTransform = style.MozTransform = 'translateX(' + dist + 'px)';
	}

	function moveY(index, dist, speed) {
		translateY(index, dist, speed);
		slidePos[index] = dist;
	}

	function translateY(index, dist, speed) {
		var slide = slides[index], style = slide && slide.style;

		if (!style) return;

		style.webkitTransitionDuration = style.MozTransitionDuration = style.msTransitionDuration = style.transitionDuration = speed + 'ms';

		style.webkitTransform = 'translate(0,' + dist + 'px)' + 'translateZ(0)';
		style.msTransform = style.MozTransform = 'translateY(' + dist + 'px)';
	}

	// setup initial vars
	var start = {}, delta = {}, isScrolling;

	// setup event capturing
	var events = {
		handleEvent: function (event) {
			switch (event.type) {
				case 'touchstart':
				case 'mousedown':
					this.start(event);
					break;
				case 'touchmove':
				case 'mousemove':
					this.move(event);
					break;
				case 'touchend':
				case 'mouseup':
					offloadFn(this.end(event));
					cola.util.DISABLE_SCROLLER_DRAG = false;
					break;
				case 'webkitTransitionEnd':
				case 'msTransitionEnd':
				case 'oTransitionEnd':
				case 'otransitionend':
				case 'transitionend':
					offloadFn(this.transitionEnd(event));
					break;
				case 'resize':
					offloadFn(setup.call());
					break;
			}
			if (options.stopPropagation) event.stopPropagation();
		},
		start: function (event) {
			var touches = browser.touch ? event.touches[0] : event;

			// measure start values
			start = {
				// get initial touch coords
				x: touches.pageX,
				y: touches.pageY,

				// store time to determine touch duration
				time: +new Date
			};

			// used for testing first move event
			isScrolling = undefined;

			// reset delta and end measurements
			delta = {};

			// attach touchmove and touchend listeners
			if (browser.touch) {
				element.addEventListener('touchmove', this, false);
				element.addEventListener('touchend', this, false);
			} else {
				document.addEventListener('mousemove', this, false);
				document.addEventListener('mouseup', this, false);
			}
		},
		move: function (event) {
			// ensure swiping with one touch and not pinching
			if (browser.touch && ((event.touches.length > 1) || (event.scale && event.scale !== 1))) return;

			if (options.disableScroll) event.preventDefault();

			var touches = browser.touch ? event.touches[0] : event;
			// measure change in x and y
			delta = {
				x: touches.pageX - start.x,
				y: touches.pageY - start.y
			};

			// determine if scrolling test has run - one time test
			if (typeof isScrolling == 'undefined') {
				if (vertical)
					isScrolling = !!( isScrolling || Math.abs(delta.x) > Math.abs(delta.y) );
				else
					isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) );
			}

			// if user is not trying to scroll vertically
			if (!isScrolling) {
				// prevent native scrolling
				event.preventDefault();
				if (!vertical) {
					// increase resistance if first or last slide
					delta.x =
						delta.x /
						( (!index && delta.x > 0               // if first slide and sliding left
							|| index == slides.length - 1        // or if last slide and sliding right
							&& delta.x < 0                       // and if sliding at all
						) ?
							( Math.abs(delta.x) / width + 1 )      // determine resistance level
							: 1 );                                 // no resistance if false

					if (Math.abs(delta.x) > 5) {
						cola.util.DISABLE_SCROLLER_DRAG = true;
					}

					// translate 1:1
					translate(index - 1, delta.x + slidePos[index - 1], 0);
					translate(index, delta.x + slidePos[index], 0);
					translate(index + 1, delta.x + slidePos[index + 1], 0);
				} else {
					// increase resistance if first or last slide
					delta.y =
						delta.y /
						( (!index && delta.y > 0               // if first slide and sliding left
							|| index == slides.length - 1        // or if last slide and sliding right
							&& delta.y < 0                       // and if sliding at all
						) ?
							( Math.abs(delta.y) / height + 1 )      // determine resistance level
							: 1 );                                 // no resistance if false

					if (Math.abs(delta.y) > 5) {
						cola.util.DISABLE_SCROLLER_DRAG = true;
					}

					// translate 1:1
					translateY(index - 1, delta.y + slidePos[index - 1], 0);
					translateY(index, delta.y + slidePos[index], 0);
					translateY(index + 1, delta.y + slidePos[index + 1], 0);
				}
			}
		},
		end: function (event) {
			// measure duration
			var duration = +new Date - start.time, isValidSlide, isPastBounds, direction;
			if (!vertical) {
				// determine if slide attempt triggers next/prev slide
				isValidSlide =
					Number(duration) < 250               // if slide duration is less than 250ms
					&& Math.abs(delta.x) > 20            // and if slide amt is greater than 20px
					|| Math.abs(delta.x) > width / 2;      // or if slide amt is greater than half the width

				// determine if slide attempt is past start and end
				isPastBounds =
					!index && delta.x > 0                            // if first slide and slide amt is greater than 0
					|| index == slides.length - 1 && delta.x < 0;    // or if last slide and slide amt is less than 0

				// determine direction of swipe (true:right, false:left)
				direction = delta.x < 0;

				// if not scrolling vertically
				if (!isScrolling) {
					if (isValidSlide && !isPastBounds) {
						if (direction) {
							move(index - 1, -width, 0);
							move(index, slidePos[index] - width, speed);
							move(index + 1, slidePos[index + 1] - width, speed);
							index += 1;
						} else {
							move(index + 1, width, 0);
							move(index, slidePos[index] + width, speed);
							move(index - 1, slidePos[index - 1] + width, speed);
							index += -1;
						}

						options.callback && options.callback(index, slides[index]);
					} else {
						move(index - 1, -width, speed);
						move(index, 0, speed);
						move(index + 1, width, speed);
					}
				}
			} else {
				// determine if slide attempt triggers next/prev slide
				isValidSlide =
					Number(duration) < 250               // if slide duration is less than 250ms
					&& Math.abs(delta.y) > 20            // and if slide amt is greater than 20px
					|| Math.abs(delta.y) > height / 2;      // or if slide amt is greater than half the width

				// determine if slide attempt is past start and end
				isPastBounds =
					!index && delta.y > 0                            // if first slide and slide amt is greater than 0
					|| index == slides.length - 1 && delta.y < 0;    // or if last slide and slide amt is less than 0

				// determine direction of swipe (true:bottom, false:top)
				direction = delta.y < 0;

				// if not scrolling vertically
				if (!isScrolling) {
					if (isValidSlide && !isPastBounds) {
						if (direction) {
							moveY(index - 1, -height, 0);
							moveY(index, slidePos[index] - height, speed);
							moveY(index + 1, slidePos[index + 1] - height, speed);
							index += 1;
						} else {
							moveY(index + 1, height, 0);
							moveY(index, slidePos[index] + height, speed);
							moveY(index - 1, slidePos[index - 1] + height, speed);
							index += -1;
						}

						options.callback && options.callback(index, slides[index]);
					} else {
						moveY(index - 1, -height, speed);
						moveY(index, 0, speed);
						moveY(index + 1, height, speed);
					}
				}
			}

			// kill touchmove and touchend event listeners until touchstart called again
			if (browser.touch) {
				element.removeEventListener('touchmove', events, false);
				element.removeEventListener('touchend', events, false);
			} else {
				document.removeEventListener('mousemove', events, false);
				document.removeEventListener('mouseup', events, false);
			}
		},
		transitionEnd: function (event) {
			if (parseInt(event.target.getAttribute('data-index'), 10) == index) {
				options.transitionEnd && options.transitionEnd.call(event, index, slides[index]);
			}
		}
	};

	// trigger setup
	setup();

	// add event listeners
	if (browser.addEventListener) {
		// set touchstart event on element
		if (browser.touch)
			element.addEventListener('touchstart', events, false);
		else
			element.addEventListener('mousedown', events, false);

		if (browser.transitions) {
			element.addEventListener('webkitTransitionEnd', events, false);
			element.addEventListener('msTransitionEnd', events, false);
			element.addEventListener('oTransitionEnd', events, false);
			element.addEventListener('otransitionend', events, false);
			element.addEventListener('transitionend', events, false);
		}

		// set resize event on window
		window.addEventListener('resize', events, false);
	}

	// expose the Swipe API
	return {
		setup: function () {
			setup();
		},
		refresh: function() {
			setup();
		},
		getPos: function () {
			// return current index position
			return index;
		},
		setPos: function(pos) {
			index = pos;
			setup();
		},
		prev: function() {
			prev();
		},
		next: function() {
			next();
		},
		kill: function () {
			// reset element
			element.style.width = 'auto';
			element.style.left = 0;

			// reset slides
			var pos = slides.length;
			while (pos--) {
				var slide = slides[pos];
				if (!vertical) {
					slide.style.width = '100%';
					slide.style.left = 0;
				} else {
					slide.style.height = '100%';
					slide.style.top = 0;
				}

				if (browser.transitions) translate(pos, 0, 0);
			}

			// removed event listeners
			if (browser.addEventListener) {
				// remove current event listeners
				element.removeEventListener('touchstart', events, false);
				element.removeEventListener('webkitTransitionEnd', events, false);
				element.removeEventListener('msTransitionEnd', events, false);
				element.removeEventListener('oTransitionEnd', events, false);
				element.removeEventListener('otransitionend', events, false);
				element.removeEventListener('transitionend', events, false);
				window.removeEventListener('resize', events, false);
			} else {
				window.onresize = null;
			}
		}
	}
}<|endoftext|>


<commit_before>import Util from 'client/util'

/**
 * Object with helper methods to handle file upload
 */
const UploadUtil = {
    uploadData: ({dataUrl, fileName, uploadTo, onProgress, onError, onLoad, data}) => {

        UploadUtil.dataURLtoBlob(dataUrl, (blob) => {

            const xhr = new XMLHttpRequest()
            xhr.timeout = 1000 * 60 * 10

            xhr.responseType = 'json'
            // Progress bar
            xhr.upload.addEventListener('progress', onProgress, false)
            xhr.onload = onLoad
            xhr.onerror = onError

            xhr.open('POST', uploadTo, true)
            xhr.setRequestHeader('Authorization', Util.getAuthToken())

            const fd = new FormData()
            if (data) {
                Object.keys(data).forEach(key => {
                    fd.append(key, JSON.stringify(data[key]))
                })
            }
            fd.append('blob', blob, fileName)
            xhr.send(fd)

        })
    },
    dataURLtoBlob: (dataUrl, callback) => {
        const req = new XMLHttpRequest

        req.open('GET', dataUrl)
        req.responseType = 'arraybuffer' // Can't use blob directly because of https://crbug.com/412752

        req.onload = () => {
            callback(new Blob([req.response], {type: req.getResponseHeader('content-type')}))
        }

        req.send()
    },
    resizeImageFromFile: ({file, conversion, onSuccess}) => {

        if (!conversion || !conversion.length) {

            // nothing to convert
            onSuccess(URL.createObjectURL(file))
            return
        }

        const oriImg = new Image()
        oriImg.onload = () => {

            //TODO support for multiple conversions
            const {maxWidth, maxHeight, quality, type} = conversion[0]

            let width = oriImg.width, height = oriImg.height

            if (width > height) {
                if (width > maxWidth) {
                    height *= maxWidth / width
                    width = maxWidth
                }
            } else {
                if (height > maxHeight) {
                    width *= maxHeight / height
                    height = maxHeight
                }
            }

            const canvas = document.createElement('canvas')
            canvas.width = width
            canvas.height = height

            const ctx = canvas.getContext('2d')
            ctx.clearRect(0, 0, width, height)
            ctx.drawImage(oriImg, 0, 0, width, height)

            //Some update on canvas
            /*  ctx.font = '30px serif'
             ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'
             ctx.fillText(file.name, 5, height - 8)*/

            const dataUrl = canvas.toDataURL(type || file.type, quality)
            onSuccess(dataUrl)

        }
        oriImg.src = URL.createObjectURL(file)
    },
    isImage: (fileName) => {
        return (/\.(?=gif|jpg|png|jpeg)/gi).test(fileName)
    },
    validateFiles: ({files, accept, maxFileSize}) => {
        const results = {validFiles: [], invalidFiles: []}
        if (files) {
            const accepts = accept.split('|'), acceptsType = [], acceptsExt = []
            accepts.forEach(i => {
                const a = i.split('/')
                if (a.length > 1) {
                    acceptsType.push(a[0])
                    acceptsExt.push(a[1])
                } else {
                    acceptsExt.push(a[0])
                }

            })

            // Process all File objects
            const images = [], filteredFiles = []
            for (let i = 0, file; file = files[i]; i++) {

                if (maxFileSize && file.size > maxFileSize) {
                    results.invalidFiles.push({
                        file,
                        message: `File size of ${file.name} exceeds the max file size of ${maxFileSize / 1024 / 1024}MB.`
                    })
                    continue
                }

                // validate
                const aType = file.type.split('/')

                const ext = aType.length > 1 ? aType[1] : aType[0]
                const isImage = UploadUtil.isImage(file.name)

                let isValid = false
                acceptsExt.forEach(e => {
                    if (ext === e) {
                        isValid = true
                        return
                    } else if (e === '*') {
                        acceptsType.forEach(type => {
                            if (type === '*' || (type === 'image' && isImage)) {
                                isValid = true
                                return
                            }
                        })
                        if (isValid) {
                            return
                        }
                    }
                })

                if (isValid) {
                    results.validFiles.push(file)
                } else {
                    results.invalidFiles.push({file, message: `File format ${file.type} is not accepted`})
                }
            }
        }
        return results
    }
}
export default UploadUtil
<commit_msg>Small improvements<commit_after>import Util from 'client/util'

/**
 * Object with helper methods to handle file upload
 */
const UploadUtil = {
    uploadData: ({dataUrl, fileName, uploadTo, onProgress, onError, onLoad, data}) => {

        UploadUtil.dataURLtoBlob(dataUrl, (blob) => {

            const xhr = new XMLHttpRequest()
            xhr.timeout = 1000 * 60 * 60

            xhr.responseType = 'json'
            // Progress bar
            xhr.upload.addEventListener('progress', onProgress, false)
            xhr.onload = onLoad
            xhr.onerror = onError

            xhr.open('POST', uploadTo, true)
            xhr.setRequestHeader('Authorization', Util.getAuthToken())

            const fd = new FormData()
            if (data) {
                Object.keys(data).forEach(key => {
                    fd.append(key, JSON.stringify(data[key]))
                })
            }
            fd.append('blob', blob, fileName)
            xhr.send(fd)

        })
    },
    dataURLtoBlob: (dataUrl, callback) => {
        const req = new XMLHttpRequest

        req.open('GET', dataUrl)
        req.responseType = 'arraybuffer' // Can't use blob directly because of https://crbug.com/412752

        req.onload = () => {
            callback(new Blob([req.response], {type: req.getResponseHeader('content-type')}))
        }

        req.send()
    },
    resizeImageFromFile: ({file, conversion, onSuccess}) => {

        if (!conversion || !conversion.length) {

            // nothing to convert
            onSuccess(URL.createObjectURL(file))
            return
        }

        const oriImg = new Image()
        oriImg.onload = () => {

            //TODO support for multiple conversions
            const {maxWidth, maxHeight, quality, type} = conversion[0]

            let width = oriImg.width, height = oriImg.height

            if (width > height) {
                if (width > maxWidth) {
                    height *= maxWidth / width
                    width = maxWidth
                }
            } else {
                if (height > maxHeight) {
                    width *= maxHeight / height
                    height = maxHeight
                }
            }

            const canvas = document.createElement('canvas')
            canvas.width = width
            canvas.height = height

            const ctx = canvas.getContext('2d')
            ctx.clearRect(0, 0, width, height)
            ctx.drawImage(oriImg, 0, 0, width, height)

            //Some update on canvas
            /*  ctx.font = '30px serif'
             ctx.fillStyle = 'rgba(0, 0, 0, 0.4)'
             ctx.fillText(file.name, 5, height - 8)*/

            const dataUrl = canvas.toDataURL(type || file.type, quality)
            onSuccess(dataUrl)

        }
        oriImg.src = URL.createObjectURL(file)
    },
    isImage: (fileName) => {
        return (/\.(?=gif|jpg|png|jpeg)/gi).test(fileName)
    },
    validateFiles: ({files, accept, maxFileSize}) => {
        const results = {validFiles: [], invalidFiles: []}
        if (files) {
            const accepts = accept.split('|'), acceptsType = [], acceptsExt = []
            accepts.forEach(i => {
                const a = i.split('/')
                if (a.length > 1) {
                    acceptsType.push(a[0])
                    acceptsExt.push(a[1])
                } else {
                    acceptsExt.push(a[0])
                }

            })

            // Process all File objects
            const images = [], filteredFiles = []
            for (let i = 0, file; file = files[i]; i++) {

                if (maxFileSize && file.size > maxFileSize) {
                    results.invalidFiles.push({
                        file,
                        message: `File size of ${file.name} exceeds the max file size of ${maxFileSize / 1024 / 1024}MB.`
                    })
                    continue
                }

                // validate
                const aType = file.type.split('/')

                const ext = aType.length > 1 ? aType[1] : aType[0]
                const isImage = UploadUtil.isImage(file.name)

                let isValid = false
                acceptsExt.forEach(e => {
                    if (ext === e) {
                        isValid = true
                        return
                    } else if (e === '*') {
                        acceptsType.forEach(type => {
                            if (type === '*' || (type === 'image' && isImage)) {
                                isValid = true
                                return
                            }
                        })
                        if (isValid) {
                            return
                        }
                    }
                })

                if (isValid) {
                    results.validFiles.push(file)
                } else {
                    results.invalidFiles.push({file, message: `File format ${file.type} is not accepted`})
                }
            }
        }
        return results
    }
}
export default UploadUtil
<|endoftext|>


<commit_before>// All of this should be executed after the DOM is ready and the entire skin has been loaded.

// Image size used in the map.
var MAX_X = 28314;
var MAX_Y = 23560;
// How the image was extracted from the game:
// http://forum.scssoft.com/viewtopic.php?p=405122#p405122

// Based on http://forum.scssoft.com/viewtopic.php?f=41&t=186779
function calculatePixelCoordinate(x, y, pointsPerPixel, x0, y0) {
    return [
        (x / pointsPerPixel + x0) | 0,
        (y / pointsPerPixel + y0) | 0
    ];
}
function calculatePixelCoordinateEu(x, y) {
    return calculatePixelCoordinate(x, y, 7.278, 13164, 16260); //x+16, y+4
}
function calculatePixelCoordinateUk(x, y) {
    //return calculatePixelCoordinate(x, y, 9.69522, 11446, 14422);
    return calculatePixelCoordinate(x, y, 7.278, 13164, 16260); //x-550, y-1540 //Original pointsPerPixel: 9.69522, own calculated: 8.62552985346, x0: 12093, y0: 15148
}

function game_coord_to_pixels(x, y) {
    // http://forum.scssoft.com/viewtopic.php?p=402836#p402836
    var r = null;
    //if (x < -31812 && y < -5618 && x > -61000 && y > -67000) {
    //    r = calculatePixelCoordinateUk(x, y);
    //} else {
        r = calculatePixelCoordinateEu(x, y);
    //}

    // Inverting Y axis, because of OpenLayers coordinates.
    r[1] = MAX_Y - r[1];
    return r;
}

function buildMap(target_element_id){
    var projection = new ol.proj.Projection({
        // Any name here. I chose "Funbit" because we are using funbit's image coordinates.
        code: 'Funbit',
        units: 'pixels',
        extent: [0, 0, MAX_X, MAX_Y],
        worldExtent: [0, 0, MAX_X, MAX_Y]
    });
    ol.proj.addProjection(projection);

    // Adding a marker for the player position/rotation.
    g_playerIcon = g_runningGame === 'ETS2' ? new ol.style.Icon({
        anchor: [0.5, 39],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        rotateWithView: true,
        src: g_pathPrefix + '/img/player_proportions.png'
    }) : new ol.style.Icon();
    var playerIconStyle = new ol.style.Style({
        image: g_playerIcon
    });
    g_playerFeature = new ol.Feature({
        geometry: new ol.geom.Point([MAX_X / 2, MAX_Y / 2])
    });
    // For some reason, we cannot pass the style in the constructor.
    g_playerFeature.setStyle(playerIconStyle);

    // Adding a layer for features overlaid on the map.
    var featureSource = new ol.source.Vector({
        features: [g_playerFeature],
        wrapX: false
    });
    var vectorLayer = new ol.layer.Vector({
        source: featureSource
    });

    // Configuring the custom map tiles.
    var custom_tilegrid = new ol.tilegrid.TileGrid({
        extent: [0, 0, MAX_X, MAX_Y],
        minZoom: 0,
        origin: [0, MAX_Y],
        tileSize: [256, 256],
        resolutions: (function(){
            var r = [];
            for (var z = 0; z <= 7; ++z) {
                r[z] = Math.pow(2, 7 - z);
            }
            return r;
        })()
    });

    // Creating custom controls.
    var rotate_control = new ol.control.Control({
        element: document.getElementById('rotate-button-div')
    });
    var speed_limit_control = new ol.control.Control({
        element: document.getElementById('speed-limit')
    });
    var text_control = new ol.control.Control({
        element: document.getElementById('map-text')
    });

    // Creating the map.
    g_map = new ol.Map({
        target: target_element_id,
        controls: [
            // new ol.control.ZoomSlider(),
            // new ol.control.OverviewMap(),
            // new ol.control.Rotate(),
            // new ol.control.MousePosition(),  // DEBUG
            new ol.control.Zoom(),
            rotate_control,
            speed_limit_control,
            text_control
            // TODO: Set 'tipLabel' on both zoom and rotate controls to language-specific translations.
        ],
        interactions: ol.interaction.defaults().extend([
            // Rotating by using two fingers is implemented in PinchRotate(), which is enabled by default.
            // With DragRotateAndZoom(), it is possible to use Shift+mouse-drag to rotate the map.
            // Without it, Shift+mouse-drag creates a rectangle to zoom to an area.
            new ol.interaction.DragRotateAndZoom()
        ]),
        layers: [
            getMapTilesLayer(projection, custom_tilegrid),
            getTextLayer(),
            // Debug layer below.
            // new ol.layer.Tile({
            //     extent: [0, 0, MAX_X, MAX_Y],
            //     source: new ol.source.TileDebug({
            //         projection: projection,
            //         tileGrid: custom_tilegrid,
            //         // tileGrid: ol.tilegrid.createXYZ({
            //         //  extent: [0, 0, MAX_X, MAX_Y],
            //         //  minZoom: 0,
            //         //  maxZoom: 7,
            //         //  tileSize: [256, 256]
            //         // }),
            //         wrapX: false
            //     })
            // }),
            vectorLayer
        ],
        view: new ol.View({
            projection: projection,
            extent: [0, 0, MAX_X, MAX_Y],
            //center: ol.proj.transform([37.41, 8.82], 'EPSG:4326', 'EPSG:3857'),
            center: [MAX_X/2, MAX_Y/2],
            minZoom: 0,
            maxZoom: 9,
            zoom: 7
        })
    });

    // Adding behavior to the custom button.
    var rotate_button = document.getElementById('rotate-button');
    var rotate_arrow = rotate_button.firstElementChild;
    g_map.getView().on('change:rotation', function(ev) {
        rotate_arrow.style.transform = 'rotate(' + ev.target.getRotation() + 'rad)';
    });
    rotate_button.addEventListener('click', function(ev) {
        if (g_behavior_center_on_player) {
            g_behavior_rotate_with_player = ! g_behavior_rotate_with_player;
        } else {
            g_behavior_center_on_player = true;
        }
    });

    // Detecting when the user interacts with the map.
    // https://stackoverflow.com/q/32868671/
    g_map.getView().on(['change:center', 'change:rotation'], function(ev) {
        if (g_ignore_view_change_events) {
            return;
        }

        // The user has moved or rotated the map.
        g_behavior_center_on_player = false;
        // Not needed:
        // g_behavior_rotate_with_player = false;
    });

    // Debugging.
    // map.on('singleclick', function(evt) {
    //     var coordinate = evt.coordinate;
    //     console.log(coordinate);
    // });
    // map.getView().on('change:center', function(ev) {
    //   console.log(ev);
    // });
    // map.getView().on('change:rotation', function(ev) {
    //   console.log(ev);
    // });
}

function getMapTilesLayer(projection, tileGrid) {
    if (g_runningGame === 'ETS2') {
        return new ol.layer.Tile({
            extent: [0, 0, MAX_X, MAX_Y],
            source: new ol.source.XYZ({
                projection: projection,
                url: g_pathPrefix + '/maps/promods-rusmap/tiles/{z}/{x}/{y}.png',
                tileSize: [256, 256],
                // Using createXYZ() makes the vector layer (with the features) unaligned.
                // It also tries loading non-existent tiles.
                //
                // Using custom_tilegrid causes rescaling of all image tiles before drawing
                // (i.e. no image will be rendered at 1:1 pixels), But fixes all other issues.
                tileGrid: tileGrid,
                // tileGrid: ol.tilegrid.createXYZ({
                //     extent: [0, 0, MAX_X, MAX_Y],
                //     minZoom: 0,
                //     maxZoom: 7,
                //     tileSize: [256, 256]
                // }),
                wrapX: false,
                minZoom: 4,
                maxZoom: 7
            })
        });
    }

    return new ol.layer.Tile();
}
var COUNTRY_NAME_TO_CODE = {
    "andorra": "ad",
    "austria": "at",
    "belarus": "by",
    "belgium": "be",
    "bulgaria": "bg",
    "czech": "cz",
    "denmark": "dk",
    "estonia": "ee",
    "faroe": "fo",
    "finland": "fi",
    "france": "fr",
    "germany": "de",
    "hungary": "hu",
    "iceland": "is",
    "iom": "im",
    "isle of man": "im",
    "italy": "it",
    "latvia": "lv",
    "liecht": "li",
    "liechtenstein": "li",
    "lithuania": "lt",
    "luxembourg": "lu",
    "moldova": "md",
    "netherlands": "nl",
    "norway": "no",
    "poland": "pl",
    "romania": "ro",
    "russia": "ru",
    "slovakia": "sk",
    "slovenia": "si",
    "spain": "es",
    "sweden": "se",
    "switzerland": "ch",
    "uk": "gb",
    "united kingdom": "gb",
    "ukraine": "ua"
};

// http://codepen.io/denilsonsa/pen/BKWNgB
function country_code_to_unicode(cc) {
    cc = cc.toLowerCase();
    var flagA = 0x1F1E6;
    var letter_a = 0x61;
    var a = cc.charCodeAt(0) - letter_a;
    var b = cc.charCodeAt(1) - letter_a;
    return String.fromCodePoint(flagA + a, flagA + b);
}

function getTextFeatures() {
    var fill = new ol.style.Fill();
    fill.setColor('#fff');
    var stroke = new ol.style.Stroke();
    stroke.setColor('#000');
    stroke.setWidth(2);
    var createTextStyle = function(resolution) {
        var scale = Math.min(1, Math.max(0, 1.0 / Math.log2(resolution + 1) - 0.125));
        return [new ol.style.Style({
			//Creating a new image layer
            image: new ol.style.Icon(({
                rotateWithView: false,
                anchor: [0.5, 1],
                anchorXUnits: 'fraction',
                anchorYUnits: 'fraction',
                snapToPixel: false,
                // Flag images from: http://lipis.github.io/flag-icon-css/
                src: g_pathPrefix + '/flags/' + this.get('cc') + '.svg',
                scale: 4 / 48 * scale
            })),
            text: new ol.style.Text({
                text: this.get('realName'),
                font: '32px "Helvetica Neue", "Helvetica", "Arial", sans-serif',
                textAlign: 'center',
                fill: fill,
                stroke: stroke,
                scale: scale
            })
        })];
    };
    var features = g_cities_json.map(function(city) {
        var map_coords = game_coord_to_pixels(city.x, city.z);
        var feature = new ol.Feature(city);
        feature.setGeometry(new ol.geom.Point(map_coords));
        feature.setStyle(createTextStyle);
        return feature;
    });
    return features;
}

function getTextLayer() {
    var textSource = new ol.source.Vector({
        features: getTextFeatures(),
        wrapX: false
    });
    var vectorLayer = new ol.layer.Vector({
        source: textSource
    });

    return vectorLayer;
}

// Global vars.
var g_map;
var g_playerFeature;
var g_playerIcon;
var g_behavior_center_on_player = true;
var g_behavior_rotate_with_player = true;
var g_ignore_view_change_events = false;

function updatePlayerPositionAndRotation(lon, lat, rot, speed) {
    var map_coords = game_coord_to_pixels(lon, lat);
    var rad = rot * Math.PI * 2;

    g_playerFeature.getGeometry().setCoordinates(map_coords);
    g_playerIcon.setRotation(-rad);

    g_ignore_view_change_events = true;
    if (g_behavior_center_on_player) {

        if (g_behavior_rotate_with_player) {
            var height = g_map.getSize()[1];
            var max_ahead_amount = height / 3.0 * g_map.getView().getResolution();

            var amount_ahead = speed * 0.25;
            amount_ahead = Math.max(-max_ahead_amount, Math.min(amount_ahead, max_ahead_amount));

            var ahead_coords = [
                map_coords[0] + Math.sin(-rad) * amount_ahead,
                map_coords[1] + Math.cos(-rad) * amount_ahead
            ];
            g_map.getView().setCenter(ahead_coords);
            g_map.getView().setRotation(rad);
        } else {
            g_map.getView().setCenter(map_coords);
            g_map.getView().setRotation(0);
        }
    }
    g_ignore_view_change_events = false;
}
<commit_msg>Fixed city names<commit_after>// All of this should be executed after the DOM is ready and the entire skin has been loaded.

// Image size used in the map.
var MAX_X = 28314;
var MAX_Y = 23560;
// How the image was extracted from the game:
// http://forum.scssoft.com/viewtopic.php?p=405122#p405122

// Based on http://forum.scssoft.com/viewtopic.php?f=41&t=186779
function calculatePixelCoordinate(x, y, pointsPerPixel, x0, y0) {
    return [
        (x / pointsPerPixel + x0) | 0,
        (y / pointsPerPixel + y0) | 0
    ];
}
function calculatePixelCoordinateEu(x, y) {
    return calculatePixelCoordinate(x, y, 7.278, 13164, 16260); //x+16, y+4
}
function calculatePixelCoordinateUk(x, y) {
    //return calculatePixelCoordinate(x, y, 9.69522, 11446, 14422);
    return calculatePixelCoordinate(x, y, 7.278, 13164, 16260); //x-550, y-1540 //Original pointsPerPixel: 9.69522, own calculated: 8.62552985346, x0: 12093, y0: 15148
}

function game_coord_to_pixels(x, y) {
    // http://forum.scssoft.com/viewtopic.php?p=402836#p402836
    var r = null;
    //if (x < -31812 && y < -5618 && x > -61000 && y > -67000) {
    //    r = calculatePixelCoordinateUk(x, y);
    //} else {
        r = calculatePixelCoordinateEu(x, y);
    //}

    // Inverting Y axis, because of OpenLayers coordinates.
    r[1] = MAX_Y - r[1];
    return r;
}

function buildMap(target_element_id){
    var projection = new ol.proj.Projection({
        // Any name here. I chose "Funbit" because we are using funbit's image coordinates.
        code: 'Funbit',
        units: 'pixels',
        extent: [0, 0, MAX_X, MAX_Y],
        worldExtent: [0, 0, MAX_X, MAX_Y]
    });
    ol.proj.addProjection(projection);

    // Adding a marker for the player position/rotation.
    g_playerIcon = g_runningGame === 'ETS2' ? new ol.style.Icon({
        anchor: [0.5, 39],
        anchorXUnits: 'fraction',
        anchorYUnits: 'pixels',
        rotateWithView: true,
        src: g_pathPrefix + '/img/player_proportions.png'
    }) : new ol.style.Icon();
    var playerIconStyle = new ol.style.Style({
        image: g_playerIcon
    });
    g_playerFeature = new ol.Feature({
        geometry: new ol.geom.Point([MAX_X / 2, MAX_Y / 2])
    });
    // For some reason, we cannot pass the style in the constructor.
    g_playerFeature.setStyle(playerIconStyle);

    // Adding a layer for features overlaid on the map.
    var featureSource = new ol.source.Vector({
        features: [g_playerFeature],
        wrapX: false
    });
    var vectorLayer = new ol.layer.Vector({
        source: featureSource
    });

    // Configuring the custom map tiles.
    var custom_tilegrid = new ol.tilegrid.TileGrid({
        extent: [0, 0, MAX_X, MAX_Y],
        minZoom: 0,
        origin: [0, MAX_Y],
        tileSize: [256, 256],
        resolutions: (function(){
            var r = [];
            for (var z = 0; z <= 7; ++z) {
                r[z] = Math.pow(2, 7 - z);
            }
            return r;
        })()
    });

    // Creating custom controls.
    var rotate_control = new ol.control.Control({
        element: document.getElementById('rotate-button-div')
    });
    var speed_limit_control = new ol.control.Control({
        element: document.getElementById('speed-limit')
    });
    var text_control = new ol.control.Control({
        element: document.getElementById('map-text')
    });

    // Creating the map.
    g_map = new ol.Map({
        target: target_element_id,
        controls: [
            // new ol.control.ZoomSlider(),
            // new ol.control.OverviewMap(),
            // new ol.control.Rotate(),
            // new ol.control.MousePosition(),  // DEBUG
            new ol.control.Zoom(),
            rotate_control,
            speed_limit_control,
            text_control
            // TODO: Set 'tipLabel' on both zoom and rotate controls to language-specific translations.
        ],
        interactions: ol.interaction.defaults().extend([
            // Rotating by using two fingers is implemented in PinchRotate(), which is enabled by default.
            // With DragRotateAndZoom(), it is possible to use Shift+mouse-drag to rotate the map.
            // Without it, Shift+mouse-drag creates a rectangle to zoom to an area.
            new ol.interaction.DragRotateAndZoom()
        ]),
        layers: [
            getMapTilesLayer(projection, custom_tilegrid),
            getTextLayer(),
            // Debug layer below.
            // new ol.layer.Tile({
            //     extent: [0, 0, MAX_X, MAX_Y],
            //     source: new ol.source.TileDebug({
            //         projection: projection,
            //         tileGrid: custom_tilegrid,
            //         // tileGrid: ol.tilegrid.createXYZ({
            //         //  extent: [0, 0, MAX_X, MAX_Y],
            //         //  minZoom: 0,
            //         //  maxZoom: 7,
            //         //  tileSize: [256, 256]
            //         // }),
            //         wrapX: false
            //     })
            // }),
            vectorLayer
        ],
        view: new ol.View({
            projection: projection,
            extent: [0, 0, MAX_X, MAX_Y],
            //center: ol.proj.transform([37.41, 8.82], 'EPSG:4326', 'EPSG:3857'),
            center: [MAX_X/2, MAX_Y/2],
            minZoom: 0,
            maxZoom: 9,
            zoom: 7
        })
    });

    // Adding behavior to the custom button.
    var rotate_button = document.getElementById('rotate-button');
    var rotate_arrow = rotate_button.firstElementChild;
    g_map.getView().on('change:rotation', function(ev) {
        rotate_arrow.style.transform = 'rotate(' + ev.target.getRotation() + 'rad)';
    });
    rotate_button.addEventListener('click', function(ev) {
        if (g_behavior_center_on_player) {
            g_behavior_rotate_with_player = ! g_behavior_rotate_with_player;
        } else {
            g_behavior_center_on_player = true;
        }
    });

    // Detecting when the user interacts with the map.
    // https://stackoverflow.com/q/32868671/
    g_map.getView().on(['change:center', 'change:rotation'], function(ev) {
        if (g_ignore_view_change_events) {
            return;
        }

        // The user has moved or rotated the map.
        g_behavior_center_on_player = false;
        // Not needed:
        // g_behavior_rotate_with_player = false;
    });

    // Debugging.
    // map.on('singleclick', function(evt) {
    //     var coordinate = evt.coordinate;
    //     console.log(coordinate);
    // });
    // map.getView().on('change:center', function(ev) {
    //   console.log(ev);
    // });
    // map.getView().on('change:rotation', function(ev) {
    //   console.log(ev);
    // });
}

function getMapTilesLayer(projection, tileGrid) {
    if (g_runningGame === 'ETS2') {
        return new ol.layer.Tile({
            extent: [0, 0, MAX_X, MAX_Y],
            source: new ol.source.XYZ({
                projection: projection,
                url: g_pathPrefix + '/maps/promods-rusmap/tiles/{z}/{x}/{y}.png',
                tileSize: [256, 256],
                // Using createXYZ() makes the vector layer (with the features) unaligned.
                // It also tries loading non-existent tiles.
                //
                // Using custom_tilegrid causes rescaling of all image tiles before drawing
                // (i.e. no image will be rendered at 1:1 pixels), But fixes all other issues.
                tileGrid: tileGrid,
                // tileGrid: ol.tilegrid.createXYZ({
                //     extent: [0, 0, MAX_X, MAX_Y],
                //     minZoom: 0,
                //     maxZoom: 7,
                //     tileSize: [256, 256]
                // }),
                wrapX: false,
                minZoom: 4,
                maxZoom: 7
            })
        });
    }

    return new ol.layer.Tile();
}
var COUNTRY_NAME_TO_CODE = {
    "andorra": "ad",
    "austria": "at",
    "belarus": "by",
    "belgium": "be",
    "bulgaria": "bg",
    "czech": "cz",
    "denmark": "dk",
    "estonia": "ee",
    "faroe": "fo",
    "finland": "fi",
    "france": "fr",
    "germany": "de",
    "hungary": "hu",
    "iceland": "is",
    "iom": "im",
    "isle of man": "im",
    "italy": "it",
    "latvia": "lv",
    "liecht": "li",
    "liechtenstein": "li",
    "lithuania": "lt",
    "luxembourg": "lu",
    "moldova": "md",
    "netherlands": "nl",
    "norway": "no",
    "poland": "pl",
    "romania": "ro",
    "russia": "ru",
    "slovakia": "sk",
    "slovenia": "si",
    "spain": "es",
    "sweden": "se",
    "switzerland": "ch",
    "uk": "gb",
    "united kingdom": "gb",
    "ukraine": "ua"
};

// http://codepen.io/denilsonsa/pen/BKWNgB
function country_code_to_unicode(cc) {
    cc = cc.toLowerCase();
    var flagA = 0x1F1E6;
    var letter_a = 0x61;
    var a = cc.charCodeAt(0) - letter_a;
    var b = cc.charCodeAt(1) - letter_a;
    return String.fromCodePoint(flagA + a, flagA + b);
}

function getTextFeatures() {
    var fill = new ol.style.Fill();
    fill.setColor('#fff');
    var stroke = new ol.style.Stroke();
    stroke.setColor('#000');
    stroke.setWidth(2);
    var createTextStyle = function(resolution) {
        var scale = Math.min(1, Math.max(0, 1.0 / Math.log2(resolution + 1) - 0.125));
        return [new ol.style.Style({
			//Creating a new image layer
            image: new ol.style.Icon(({
                rotateWithView: false,
                anchor: [0.5, 1],
                anchorXUnits: 'fraction',
                anchorYUnits: 'fraction',
                snapToPixel: false,
                // Flag images from: http://lipis.github.io/flag-icon-css/
                src: g_pathPrefix + '/flags/' + this.get('cc') + '.svg',
                scale: 4 / 48 * scale
            })),
            text: new ol.style.Text({
                text: this.get('realName'),
                font: '32px "Helvetica Neue", "Helvetica", "Arial", sans-serif',
                textAlign: 'center',
                fill: fill,
                stroke: stroke,
                scale: scale,
				offsetY: 14 * scale
            })
        })];
    };
    var features = g_cities_json.map(function(city) {
        var map_coords = game_coord_to_pixels(city.x, city.z);
		city.cc = COUNTRY_NAME_TO_CODE[city.country.toLowerCase()];
        var feature = new ol.Feature(city);
        feature.setGeometry(new ol.geom.Point(map_coords));
        feature.setStyle(createTextStyle);
        return feature;
    });
    return features;
}

function getTextLayer() {
    var textSource = new ol.source.Vector({
        features: getTextFeatures(),
        wrapX: false
    });
    var vectorLayer = new ol.layer.Vector({
        source: textSource
    });

    return vectorLayer;
}

// Global vars.
var g_map;
var g_playerFeature;
var g_playerIcon;
var g_behavior_center_on_player = true;
var g_behavior_rotate_with_player = true;
var g_ignore_view_change_events = false;

function updatePlayerPositionAndRotation(lon, lat, rot, speed) {
    var map_coords = game_coord_to_pixels(lon, lat);
    var rad = rot * Math.PI * 2;

    g_playerFeature.getGeometry().setCoordinates(map_coords);
    g_playerIcon.setRotation(-rad);

    g_ignore_view_change_events = true;
    if (g_behavior_center_on_player) {

        if (g_behavior_rotate_with_player) {
            var height = g_map.getSize()[1];
            var max_ahead_amount = height / 3.0 * g_map.getView().getResolution();

            var amount_ahead = speed * 0.25;
            amount_ahead = Math.max(-max_ahead_amount, Math.min(amount_ahead, max_ahead_amount));

            var ahead_coords = [
                map_coords[0] + Math.sin(-rad) * amount_ahead,
                map_coords[1] + Math.cos(-rad) * amount_ahead
            ];
            g_map.getView().setCenter(ahead_coords);
            g_map.getView().setRotation(rad);
        } else {
            g_map.getView().setCenter(map_coords);
            g_map.getView().setRotation(0);
        }
    }
    g_ignore_view_change_events = false;
}
<|endoftext|>