<commit_before>/**
    ListRemoteModel
    Utility class for common code for a data list entity from a remote source,
    with local copy and cache, where the list is managed will all the data,
    without paging/cursor, with indexed access to each item by its ID.
    Is good for lists that keep small in the time.

    TODO IMPORTANT The getList method, reused as sync, is returning an observable rather
    than direct data. There are uses right now, double check if this chas any sense and update
    affected code.

    TODO To implement single item update mode, not full list each time, by set-up or method
**/
'use strict';

var ko = require('knockout'),
    IndexedListCache = require('./IndexedListCache');

function required(val, msg) {
    if (val === null || typeof(val) === 'undefined') throw new Error(msg || 'Required parameter');
    else return val;
}

function ListRemoteModel(settings) {
    /*jshint maxstatements:50*/

    settings = settings || {};
    settings.listTtl = required(settings.listTtl, 'listTtl is required');
    settings.itemIdField = required(settings.itemIdField, 'itemIdField is required');
    // Optional model
    settings.Model = settings.Model || null;
    // Required for API additions
    this.settings = settings;

    this.state = {
        isLoading: ko.observable(false),
        isSyncing: ko.observable(false),
        isSaving: ko.observable(false),
        isDeleting: ko.observable(false)
    };

    // Items are managed as plain object by default, but as permanent, updated
    // model instances if the Model class was specified.
    // This adapter is passed to the cache constructor too keep the in-memory
    // objects up to date with the correct structure.
    function itemAdapter(oldItem, newItem) {
        if (settings.Model) {
            // If the model item already exists, update with new values
            if (oldItem && oldItem instanceof settings.Model) {
                oldItem.model.updateWith(newItem);
                return oldItem;
            }
            else {
                // New created item.
                // If there was a previous, no-model, value, they are discarded
                // (that situation can only happens if there are irregular modifications
                // of the internal behavior).
                return new settings.Model(newItem);
            }
        }
        else {
            return newItem;
        }
    }

    var cache = new IndexedListCache({
        listTtl: settings.listTtl,
        itemIdField: settings.itemIdField,
        itemAdapter: itemAdapter
    });

    this.clearCache = cache.clearCache;

    this.state.isLocked = ko.pureComputed(function() {
        return this.isLoading() || this.isSaving() || this.isDeleting();
    }, this.state);

    /** Data Stores Management: implementation must be replaced, with custom code or using
        the helpers added to the class (see addXxSupport prototype methods).
    **/
    function notImplemented() { throw new Error('Not Implemented'); }
    this.fetchListFromLocal = notImplemented;
    this.fetchListFromRemote = notImplemented;
    this.pushListToLocal = notImplemented;
    this.pushListToRemote = notImplemented;
    this.removeItemFromRemote = notImplemented;

    /**
        Retrieves a plain array-objects from the cached list
    **/
    function getPlainCachedList() {
        var arr = cache.list();
        return arr.map(function(item) {
            if (item && settings.Model && item instanceof settings.Model) {
                return item.model.toPlainObject();
            }
            else {
                return item;
            }
        });
    }

    /** API definition **/
    var api = this;

    // Direct access to the observable cached list.
    api.list = cache.list;

    // Currently, just a wrapper for getList.
    api.sync = function sync() {
        return api.getList();
    };

    /**
        Promise based request to get the list (from cache, local or remote).
        It updates the observable list if new data is fetched.
        A general approach is to use the observable list and call the 'sync' method
        rather than wait this promise to finish ('sync' performs this load really).
    **/
    api.getList = function getList() {

        if (cache.control.mustRevalidate()) {
            // Cache still not used, then is first load, try load from local
            if (cache.unused) {
                api.state.isLoading(true);
                // From local
                return this.fetchListFromLocal()
                .then(function(data) {
                    // launch remote for sync
                    api.state.isSyncing(true);
                    var remotePromise = this.fetchListFromRemote()
                    .then(function(serverData) {
                        cache.list = serverData;
                        this.pushListToLocal(serverData);
                        api.state.isSyncing(false);
                        return serverData;
                    }.bind(this))
                    .catch(function(err) {
                        // If there was local data, catch error and
                        // stop sync since this promise will not
                        // be available to any consumer
                        if (data) {
                            api.state.isSyncing(false);
                            // Log to console
                            console.error('ListRemoteModel: remote synchronization failed', err);
                        }
                        else {
                            // This promise is returned so will be consumed,
                            // just rethrow and let the other catch-blocks do the common stuff
                            throw err;
                        }
                    });
                    // Remote fallback: If no local, wait for remote
                    return data ? data : remotePromise;
                }.bind(this))
                .then(function(data) {
                    // Ever a list, even if empty
                    data = data || [];
                    cache.list = data;
                    this.pushListToLocal(data);
                    api.state.isLoading(false);

                    return cache.list;
                }.bind(this))
                .catch(function(err) {
                    api.state.isLoading(false);
                    api.state.isSyncing(false);
                    // rethrow error
                    throw err;
                });
            } else {
                api.state.isSyncing(true);
                // From remote
                return this.fetchListFromRemote()
                .then(function(data) {
                    // Ever a list, even if empty
                    data = data || [];
                    cache.list = data;
                    this.pushListToLocal(data);
                    api.state.isLoading(false);
                    api.state.isSyncing(false);

                    return cache.list;
                }.bind(this))
                .catch(function(err) {
                    api.state.isLoading(false);
                    api.state.isSyncing(false);
                    // rethrow error
                    throw err;
                });
            }
        }
        else {
            // From cache
            return Promise.resolve(cache.list);
        }
    };

    api.getItem = function getItem(itemID) {
        // IMPORTANT: To simplify, load all the list (is a short list)
        // and look from its cached index
        // TODO Implement item server look-up. Be careful with cache update,
        // list sorting and state flags.
        return api.getList()
        .then(function() {
            // Get from cached index
            var cacheItem = cache.getItemCache(itemID);

            // TODO: Enhance on future with actual look-up by API itemID
            // if not cached, throwing not found from the server (just to avoid
            // minor cases when a new item is not still in the cache if linked
            // from other app data). And keep updated list cache with that
            // items lookup
            if (!cacheItem) {
                console.warn('ListRemoteModel Not found', itemID, settings.Model);
                throw new Error('Not Found');
            }
            return cacheItem.item;
        });
    };

    /**
        Generates and returns an observable inmediately,
        with the cached value or undefined,
        launching an item load that will update the observable
        on ready if there is no cached value.
        A method 'sync' is added to the observable so can be requested
        a data sync/reload on demand.
    **/
    api.getObservableItem = function getObservableItem(itemID) {
        // Get first value
        var firstValue = cache.getItemCache(itemID);
        firstValue = firstValue && firstValue.item || undefined;
        var obs = ko.observable(firstValue);
        // Create method 'sync'
        obs.sync = function syncObservableItem() {
            return api.getItem(itemID)
            .then(function(itemModel) {
                obs(itemModel);
            });
        };
        // First load if no cached value
        if (!firstValue)
            obs.sync();
        // Return
        return obs;
    };

    /**
        Similar to getObservableItem, it allows to get
        an observable to an item model synchronously that
        it triggers an item load when its method 'sync'
        is called. The itemID is passed to the sync item,
        since the observable is meant to hold any item/itemID
        (its a wildcard).
        This way, a reference to an observable can be get on initialization
        even if there is no data still, even no itemID, and load
        it later lazily, on demand, while keeping the content of the
        previous outdated or different item.

        NOTE: API alternative names: getLazyItem, createMutableItem
        NOTE: Maybe can get state observables (loading, syncing..)?
        NOTE: On update a same itemID, maybe update the model with updateWith
                rather than change the reference model?? (double check:
                since the model is the same in cache, already updated
                with 'updateWith', there is no need to re-apply and no
                need to change the item observable because is the same
                updated already, right?
    **/
    api.createWildcardItem = function createWildcardItem() {
        // Utility for reuse in 'sync'
        var hasID = function(id) {
            return id !== null && typeof(id) !== 'undefined';
        };

        // Create observable, with initial undefined value
        var obs = ko.observable(undefined);

        // Create method 'sync'
        var lastID;
        /**
            Sync method to load an item, from cache ASAP and
            from local or remote if required by the cache control.
            It returns the Promise for fetching the value (getItem)
            so load/sync ending and error can be catched.
        **/
        obs.sync = function syncObservableItem(itemID) {

            var idChanged = hasID(itemID) && itemID !== lastID;
            lastID = hasID(itemID) ? itemID : lastID;

            // ASAP Get from cache if any and requested item changed
            if (idChanged) {
                var cachedItem = cache.getItemCache(lastID);
                if (cachedItem && cachedItem.item)
                    obs(cachedItem.item);
            }

            // Request updated value
            return api.getItem(lastID)
            .then(function(itemModel) {
                obs(itemModel);
                return itemModel;
            });
        };

        /**
            Sets the observable value to a new item instance
        **/
        obs.newItem = function newItem(defaults) {
            if (settings.Model)
                obs(new settings.Model(defaults));
            else
                obs(defaults || {});
        };

        // Return
        return obs;
    };

    /**
        Save an item in cache, local and remote.
        Can be new or updated.
        The IDs goes with all the other data, being
        groupID required, itemID required for updates
        but falsy for insertions.
        @param data:object Plain object
    **/
    api.setItem = function setItem(data) {
        api.state.isSaving(true);
        // Send to remote first
        return this.pushListToRemote(data)
        .then(function(serverData) {
            // Success! update local copy with returned data
            // IMPORTANT: to use server data here so we get values set
            // by the server, as updates dates and itemID when creating
            // a new item.
            if (serverData) {
                // Save in cache
                cache.setItemCache(serverData);
                // Save in local storage
                // In local need to be saved all the list, not just
                // the item; since we have the cache list updated, use that
                // full list to save local
                this.pushListToLocal(getPlainCachedList());
            }
            api.state.isSaving(false);

            return serverData;
        }.bind(this))
        .catch(function(err) {
            api.state.isSaving(false);
            // Rethrow error
            throw err;
        });
    };

    api.delItem = function delItem(itemID) {

        api.state.isDeleting(true);

        // Remove in remote first
        return this.removeItemFromRemote(itemID)
        .then(function(removedData) {
            // Update cache
            cache.delItemCache(itemID);
            // Save in local storage
            // In local need to be saved all the list;
            // since we have the cache list updated, use that
            // full list to save local
            this.pushListToLocal(getPlainCachedList());

            api.state.isDeleting(false);

            return removedData;
        }.bind(this))
        .catch(function(err) {
            api.state.isDeleting(false);
            // Rethrow error
            throw err;
        });
    };

    /** Some Utils **/

    var ModelVersion = require('../utils/ModelVersion');
    /**
        It creates a new ModelVersion for the requested item ID
        after load the item.
        The promise returns the ModelVersion ready, or null
        if the item does not exists.
    **/
    api.createItemVersion = function createItemVersion(itemID) {
        return api.getItem(itemID)
        .then(function(model) {
            return model ? new ModelVersion(model) : null;
        });
    };

    /**
        It creates a new Model instance with the given initial values,
        returning a ModelVersion object.
        The versioning allows to track the initial
        state (if comes from a set of defaults or clone) with
        the changes done; the internal version notifies itself
        as 'unsaved' ever.
        Its useful to keep the same ModelVersion aware code for
        editions and additions.
    **/
    api.newItem = function newItem(values) {
        // New original and version for the model
        var version = new ModelVersion(new settings.Model(values));
        // To be sure that the version appear as something 'new', unsaved,
        // we update its timestamp to be different to the original.
        version.version.model.touch();
        return version;
    };
}

module.exports = ListRemoteModel;

ListRemoteModel.prototype.addLocalforageSupport = function addLocalforageSupport(baseName) {
    var localforage = require('localforage');

    this.fetchListFromLocal = function fetchListFromLocal() {
        return localforage.getItem(baseName);
    };
    this.pushListToLocal = function pushListToLocal(data) {
        return localforage.setItem(baseName, data);
    };
};

ListRemoteModel.prototype.addRestSupport = function addRestSupport(restClient, baseUrl) {

    this.fetchListFromRemote = function fetchListFromRemote() {
        return restClient.get(baseUrl);
    };
    this.pushListToRemote = function pushListToRemote(data) {

        var itemID = data[this.settings.itemIdField],
            method = itemID ? 'put' : 'post';

        var url = baseUrl + (
            itemID ? '/' + itemID : ''
        );
        return restClient[method](url, data);
    };
    this.removeItemFromRemote = function removeItemFromRemote(itemID) {
        return restClient.delete(baseUrl + '/' + itemID);
    };
};

// For testing purposes, emulate a remote providing a static list for the data:
ListRemoteModel.prototype.addMockedRemote = function addMockedRemote(dataList) {
    this.fetchListFromRemote = function fetchListFromRemote() {
        return Promise.resolve(dataList);
    };
    this.pushListToRemote = function pushListToRemote(data) {
        return Promise.resolve(data);
    };
    this.removeItemFromRemote = function removeItemFromRemote(itemID) {
        return Promise.resolve(itemID);
    };
};<commit_msg>Fixed the 'concurrent requests' problem at the ListRemoteModel utility, and so fixed for the excesive calls of 'clients' API at the dashboard.<commit_after>/**
    ListRemoteModel
    Utility class for common code for a data list entity from a remote source,
    with local copy and cache, where the list is managed will all the data,
    without paging/cursor, with indexed access to each item by its ID.
    Is good for lists that keep small in the time.

    TODO IMPORTANT The getList method, reused as sync, is returning an observable rather
    than direct data. There are uses right now, double check if this chas any sense and update
    affected code.

    TODO To implement single item update mode, not full list each time, by set-up or method
**/
'use strict';

var ko = require('knockout'),
    IndexedListCache = require('./IndexedListCache');

function required(val, msg) {
    if (val === null || typeof(val) === 'undefined') throw new Error(msg || 'Required parameter');
    else return val;
}

function ListRemoteModel(settings) {
    /*jshint maxstatements:50*/

    settings = settings || {};
    settings.listTtl = required(settings.listTtl, 'listTtl is required');
    settings.itemIdField = required(settings.itemIdField, 'itemIdField is required');
    // Optional model
    settings.Model = settings.Model || null;
    // Required for API additions
    this.settings = settings;

    this.state = {
        isLoading: ko.observable(false),
        isSyncing: ko.observable(false),
        isSaving: ko.observable(false),
        isDeleting: ko.observable(false)
    };

    // Items are managed as plain object by default, but as permanent, updated
    // model instances if the Model class was specified.
    // This adapter is passed to the cache constructor too keep the in-memory
    // objects up to date with the correct structure.
    function itemAdapter(oldItem, newItem) {
        if (settings.Model) {
            // If the model item already exists, update with new values
            if (oldItem && oldItem instanceof settings.Model) {
                oldItem.model.updateWith(newItem);
                return oldItem;
            }
            else {
                // New created item.
                // If there was a previous, no-model, value, they are discarded
                // (that situation can only happens if there are irregular modifications
                // of the internal behavior).
                return new settings.Model(newItem);
            }
        }
        else {
            return newItem;
        }
    }

    var cache = new IndexedListCache({
        listTtl: settings.listTtl,
        itemIdField: settings.itemIdField,
        itemAdapter: itemAdapter
    });

    this.clearCache = cache.clearCache;

    this.state.isLocked = ko.pureComputed(function() {
        return this.isLoading() || this.isSaving() || this.isDeleting();
    }, this.state);

    /** Data Stores Management: implementation must be replaced, with custom code or using
        the helpers added to the class (see addXxSupport prototype methods).
    **/
    function notImplemented() { throw new Error('Not Implemented'); }
    this.fetchListFromLocal = notImplemented;
    this.fetchListFromRemote = notImplemented;
    this.pushListToLocal = notImplemented;
    this.pushListToRemote = notImplemented;
    this.removeItemFromRemote = notImplemented;

    /**
        Retrieves a plain array-objects from the cached list
    **/
    function getPlainCachedList() {
        var arr = cache.list();
        return arr.map(function(item) {
            if (item && settings.Model && item instanceof settings.Model) {
                return item.model.toPlainObject();
            }
            else {
                return item;
            }
        });
    }

    /** API definition **/
    var api = this;

    // Direct access to the observable cached list.
    api.list = cache.list;

    // Currently, just a wrapper for getList.
    api.sync = function sync() {
        return api.getList();
    };

    /**
        Promise based request to get the list (from cache, local or remote).
        It updates the observable list if new data is fetched.
        A general approach is to use the observable list and call the 'sync' method
        rather than wait this promise to finish ('sync' performs this load really).
    **/
    api.getList = function getList() {

        // First, check if there is a running/pending request already for this
        if (cache.request) {
            // reuse the request, rather than overload the app and server.
            return cache.request;
        }
        // Cache needs fresh data:
        else if (cache.control.mustRevalidate()) {
            // Cache still not used, then is first load, try to load from local
            if (cache.unused) {
                api.state.isLoading(true);
                // From local
                cache.request = this.fetchListFromLocal()
                .then(function(data) {
                    // launch remote for sync
                    api.state.isSyncing(true);
                    var remotePromise = this.fetchListFromRemote()
                    .then(function(serverData) {
                        cache.list = serverData;
                        this.pushListToLocal(serverData);
                        api.state.isSyncing(false);
                        return serverData;
                    }.bind(this))
                    .catch(function(err) {
                        // If there was local data, catch error and
                        // stop sync since this promise will not
                        // be available to any consumer
                        if (data) {
                            api.state.isSyncing(false);
                            // Log to console
                            console.error('ListRemoteModel: remote synchronization failed', err);
                        }
                        else {
                            // This promise is returned so will be consumed,
                            // just rethrow and let the other catch-blocks do the common stuff
                            throw err;
                        }
                    });
                    // Remote fallback: If no local, wait for remote
                    return data ? data : remotePromise;
                }.bind(this))
                .then(function(data) {
                    // Ever a list, even if empty
                    data = data || [];
                    cache.list = data;
                    this.pushListToLocal(data);
                    api.state.isLoading(false);
                    cache.request = null;

                    return cache.list;
                }.bind(this))
                .catch(function(err) {
                    api.state.isLoading(false);
                    api.state.isSyncing(false);
                    cache.request = null;
                    // rethrow error
                    throw err;
                });
                return cache.request;
            } else {
                api.state.isSyncing(true);
                // From remote
                cache.request = this.fetchListFromRemote()
                .then(function(data) {
                    // Ever a list, even if empty
                    data = data || [];
                    cache.list = data;
                    this.pushListToLocal(data);
                    api.state.isLoading(false);
                    api.state.isSyncing(false);
                    cache.request = null;

                    return cache.list;
                }.bind(this))
                .catch(function(err) {
                    api.state.isLoading(false);
                    api.state.isSyncing(false);
                    cache.request = null;
                    // rethrow error
                    throw err;
                });
                return cache.request;
            }
        }
        else {
            // From cache
            return Promise.resolve(cache.list);
        }
    };

    api.getItem = function getItem(itemID) {
        // IMPORTANT: To simplify, load all the list (is a short list)
        // and look from its cached index
        // TODO Implement item server look-up. Be careful with cache update,
        // list sorting and state flags.
        return api.getList()
        .then(function() {
            // Get from cached index
            var cacheItem = cache.getItemCache(itemID);

            // TODO: Enhance on future with actual look-up by API itemID
            // if not cached, throwing not found from the server (just to avoid
            // minor cases when a new item is not still in the cache if linked
            // from other app data). And keep updated list cache with that
            // items lookup
            if (!cacheItem) {
                console.warn('ListRemoteModel Not found', itemID, settings.Model);
                throw new Error('Not Found');
            }
            return cacheItem.item;
        });
    };

    /**
        Generates and returns an observable inmediately,
        with the cached value or undefined,
        launching an item load that will update the observable
        on ready if there is no cached value.
        A method 'sync' is added to the observable so can be requested
        a data sync/reload on demand.
    **/
    api.getObservableItem = function getObservableItem(itemID) {
        // Get first value
        var firstValue = cache.getItemCache(itemID);
        firstValue = firstValue && firstValue.item || undefined;
        var obs = ko.observable(firstValue);
        // Create method 'sync'
        obs.sync = function syncObservableItem() {
            return api.getItem(itemID)
            .then(function(itemModel) {
                obs(itemModel);
            });
        };
        // First load if no cached value
        if (!firstValue)
            obs.sync();
        // Return
        return obs;
    };

    /**
        Similar to getObservableItem, it allows to get
        an observable to an item model synchronously that
        it triggers an item load when its method 'sync'
        is called. The itemID is passed to the sync item,
        since the observable is meant to hold any item/itemID
        (its a wildcard).
        This way, a reference to an observable can be get on initialization
        even if there is no data still, even no itemID, and load
        it later lazily, on demand, while keeping the content of the
        previous outdated or different item.

        NOTE: API alternative names: getLazyItem, createMutableItem
        NOTE: Maybe can get state observables (loading, syncing..)?
        NOTE: On update a same itemID, maybe update the model with updateWith
                rather than change the reference model?? (double check:
                since the model is the same in cache, already updated
                with 'updateWith', there is no need to re-apply and no
                need to change the item observable because is the same
                updated already, right?
    **/
    api.createWildcardItem = function createWildcardItem() {
        // Utility for reuse in 'sync'
        var hasID = function(id) {
            return id !== null && typeof(id) !== 'undefined';
        };

        // Create observable, with initial undefined value
        var obs = ko.observable(undefined);

        // Create method 'sync'
        var lastID;
        /**
            Sync method to load an item, from cache ASAP and
            from local or remote if required by the cache control.
            It returns the Promise for fetching the value (getItem)
            so load/sync ending and error can be catched.
        **/
        obs.sync = function syncObservableItem(itemID) {

            var idChanged = hasID(itemID) && itemID !== lastID;
            lastID = hasID(itemID) ? itemID : lastID;

            // ASAP Get from cache if any and requested item changed
            if (idChanged) {
                var cachedItem = cache.getItemCache(lastID);
                if (cachedItem && cachedItem.item)
                    obs(cachedItem.item);
            }

            // Request updated value
            return api.getItem(lastID)
            .then(function(itemModel) {
                obs(itemModel);
                return itemModel;
            });
        };

        /**
            Sets the observable value to a new item instance
        **/
        obs.newItem = function newItem(defaults) {
            if (settings.Model)
                obs(new settings.Model(defaults));
            else
                obs(defaults || {});
        };

        // Return
        return obs;
    };

    /**
        Save an item in cache, local and remote.
        Can be new or updated.
        The IDs goes with all the other data, being
        groupID required, itemID required for updates
        but falsy for insertions.
        @param data:object Plain object
    **/
    api.setItem = function setItem(data) {
        api.state.isSaving(true);
        // Send to remote first
        return this.pushListToRemote(data)
        .then(function(serverData) {
            // Success! update local copy with returned data
            // IMPORTANT: to use server data here so we get values set
            // by the server, as updates dates and itemID when creating
            // a new item.
            if (serverData) {
                // Save in cache
                cache.setItemCache(serverData);
                // Save in local storage
                // In local need to be saved all the list, not just
                // the item; since we have the cache list updated, use that
                // full list to save local
                this.pushListToLocal(getPlainCachedList());
            }
            api.state.isSaving(false);

            return serverData;
        }.bind(this))
        .catch(function(err) {
            api.state.isSaving(false);
            // Rethrow error
            throw err;
        });
    };

    api.delItem = function delItem(itemID) {

        api.state.isDeleting(true);

        // Remove in remote first
        return this.removeItemFromRemote(itemID)
        .then(function(removedData) {
            // Update cache
            cache.delItemCache(itemID);
            // Save in local storage
            // In local need to be saved all the list;
            // since we have the cache list updated, use that
            // full list to save local
            this.pushListToLocal(getPlainCachedList());

            api.state.isDeleting(false);

            return removedData;
        }.bind(this))
        .catch(function(err) {
            api.state.isDeleting(false);
            // Rethrow error
            throw err;
        });
    };

    /** Some Utils **/

    var ModelVersion = require('../utils/ModelVersion');
    /**
        It creates a new ModelVersion for the requested item ID
        after load the item.
        The promise returns the ModelVersion ready, or null
        if the item does not exists.
    **/
    api.createItemVersion = function createItemVersion(itemID) {
        return api.getItem(itemID)
        .then(function(model) {
            return model ? new ModelVersion(model) : null;
        });
    };

    /**
        It creates a new Model instance with the given initial values,
        returning a ModelVersion object.
        The versioning allows to track the initial
        state (if comes from a set of defaults or clone) with
        the changes done; the internal version notifies itself
        as 'unsaved' ever.
        Its useful to keep the same ModelVersion aware code for
        editions and additions.
    **/
    api.newItem = function newItem(values) {
        // New original and version for the model
        var version = new ModelVersion(new settings.Model(values));
        // To be sure that the version appear as something 'new', unsaved,
        // we update its timestamp to be different to the original.
        version.version.model.touch();
        return version;
    };
}

module.exports = ListRemoteModel;

ListRemoteModel.prototype.addLocalforageSupport = function addLocalforageSupport(baseName) {
    var localforage = require('localforage');

    this.fetchListFromLocal = function fetchListFromLocal() {
        return localforage.getItem(baseName);
    };
    this.pushListToLocal = function pushListToLocal(data) {
        return localforage.setItem(baseName, data);
    };
};

ListRemoteModel.prototype.addRestSupport = function addRestSupport(restClient, baseUrl) {

    this.fetchListFromRemote = function fetchListFromRemote() {
        return restClient.get(baseUrl);
    };
    this.pushListToRemote = function pushListToRemote(data) {

        var itemID = data[this.settings.itemIdField],
            method = itemID ? 'put' : 'post';

        var url = baseUrl + (
            itemID ? '/' + itemID : ''
        );
        return restClient[method](url, data);
    };
    this.removeItemFromRemote = function removeItemFromRemote(itemID) {
        return restClient.delete(baseUrl + '/' + itemID);
    };
};

// For testing purposes, emulate a remote providing a static list for the data:
ListRemoteModel.prototype.addMockedRemote = function addMockedRemote(dataList) {
    this.fetchListFromRemote = function fetchListFromRemote() {
        return Promise.resolve(dataList);
    };
    this.pushListToRemote = function pushListToRemote(data) {
        return Promise.resolve(data);
    };
    this.removeItemFromRemote = function removeItemFromRemote(itemID) {
        return Promise.resolve(itemID);
    };
};<|endoftext|>


<commit_before>/* Copyright 2014 Krzysztof Daniel

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.*/

var logger = require('./util/log.js').log.getLogger('exportmap');
var db = require('./db').database;
var Canvas = require('canvas')
, Image = Canvas.Image;

var draw = function(res, filename, map){
	var x = 1280;
	var y = 800;
	var canvas = new Canvas(x,y);
	var stream = canvas.pngStream();
	var ctx = canvas.getContext('2d');

	// background
	ctx.fillStyle = "#FFFFFF";
	ctx.fillRect(0,0,x,y);

	ctx.fillStyle = "#000000";
	ctx.font = 'bold 12px Impact';

	var name = ctx.measureText(map.name);
	ctx.fillText(map.name, 30, 15);

	ctx.font = '10px Impact';
	var description = ctx.measureText(map.description);
	ctx.fillText(map.description, 30, 30);

	var createdAt = 'Created at www.wardleymaps.com';
	var createdAtMeasure = ctx.measureText(createdAt);
	ctx.fillText(createdAt, x - 30 - createdAtMeasure.width, 15);

	//TODO: draw axes here (30px from each border)

	// calculate nodes

	var availableWidth = x - 2 * 30;
	var availableHeight = y - 2 * 30;

	var calculatedNodes = [];
	for(var index in map.nodes){
		var node = map.nodes[index];
		var cX = Math.floor(node.positionX * availableWidth) + 30;
		var cY = Math.floor(node.positionY * availableHeight) + 30;
		calculatedNodes.push({
			x : cX,
			y : cY,
			name : node.name,
			componentId : node.componentId
		});
	}

	//draw connections
	for(var index in map.connections){
		var connection = map.connections[index];
		var source = connection.pageSourceId;
		var target = connection.pageTargetId;
		var start;
		var end;
		for(var nodeIndex in calculatedNodes){

			if(calculatedNodes[nodeIndex].componentId == source){
				start = calculatedNodes[nodeIndex];
			}
			if(calculatedNodes[nodeIndex].componentId == target){
				end = calculatedNodes[nodeIndex];
			}

		}
		ctx.moveTo(start.x, start.y);
		ctx.lineTo(end.x,end.y);
		ctx.stroke();
	}

	//and now draw nodes
	for ( var index in calculatedNodes) {
		ctx.beginPath();
		ctx.arc(calculatedNodes[index].x, calculatedNodes[index].y, 10, 0,
				2 * Math.PI);
		ctx.stroke();

		//background
		ctx.fillStyle = 'silver';
		ctx.fill();

		ctx.fillStyle = 'black';
		ctx.fillText(calculatedNodes[index].name,
				calculatedNodes[index].x + 10, calculatedNodes[index].y - 10);
	}

	res.setHeader('Content-Type', 'image/png');
	stream.on('data', function(chunk){
		  res.write(chunk);
	});
	stream.on('end', function(){
		res.end();
	});
};

var thumbnail_draw = function(res, filename, map){
	var x = 100;
	var y = 100;
	var canvas = new Canvas(x,y);
	var stream = canvas.pngStream();
	var ctx = canvas.getContext('2d');

	// background
	ctx.fillStyle = "#FFFFFF";
	ctx.fillRect(0,0,x,y);

	// calculate nodes

	var availableWidth = x ;
	var availableHeight = y ;

	var calculatedNodes = [];
	for(var index in map.nodes){
		var node = map.nodes[index];
		var cX = Math.floor(node.positionX * availableWidth);
		var cY = Math.floor(node.positionY * availableHeight);
		calculatedNodes.push({
			x : cX,
			y : cY,
			name : node.name,
			componentId : node.componentId
		});
	}

	ctx.strokeStyle = 'silver';
	ctx.fillStyle = 'silver';

	//draw connections
	for(var index in map.connections){
		var connection = map.connections[index];
		var source = connection.pageSourceId;
		var target = connection.pageTargetId;
		var start;
		var end;
		for(var nodeIndex in calculatedNodes){

			if(calculatedNodes[nodeIndex].componentId == source){
				start = calculatedNodes[nodeIndex];
			}
			if(calculatedNodes[nodeIndex].componentId == target){
				end = calculatedNodes[nodeIndex];
			}

		}
		ctx.moveTo(start.x, start.y);
		ctx.lineTo(end.x,end.y);
		ctx.stroke();
	}

	//and now draw nodes
	for ( var index in calculatedNodes) {
		ctx.beginPath();
		ctx.arc(calculatedNodes[index].x, calculatedNodes[index].y, 1, 0,
				2 * Math.PI);
		ctx.stroke();

		//background
		ctx.fill();
	}

	res.setHeader('Content-Type', 'image/png');
	stream.on('data', function(chunk){
		  res.write(chunk);
	});
	stream.on('end', function(){
		res.end();
	});
};

var exportmap = function(req, res, mapId, filename) {
	var userId = require('./db').toDatabaseId(req.user);
	mapId = require('./db').toDatabaseId(mapId);
	logger.debug("drawing map", mapId, "for user", userId);

	db.maps.find({
		"userId" : userId,
		"_id" : mapId
	}).toArray(function(err, maps) {
		res.setHeader('Content-Type', 'application/json');
		if (err !== null) {
			logger.error(err);
			res.statusCode = 500;
			res.send(JSON.stringify(err));
		} else {
			draw(res, mapId, maps[0]);
		}
	});
};

var thumbnail = function(req, res, mapId, filename) {
	var userId = require('./db').toDatabaseId(req.user);
	mapId = require('./db').toDatabaseId(mapId);
	logger.debug("drawing thumbnail", mapId, "for user", userId);

	db.maps.find({
		"userId" : userId,
		"_id" : mapId
	}).toArray(function(err, maps) {
		res.setHeader('Content-Type', 'application/json');
		if (err !== null) {
			logger.error(err);
			res.statusCode = 500;
			res.send(JSON.stringify(err));
		} else {
			thumbnail_draw(res, mapId, maps[0]);
		}
	});
};

exports.exportmap = exportmap;
exports.thumbnail = thumbnail;<commit_msg>Add described axes to the exported map.<commit_after>/* Copyright 2014 Krzysztof Daniel

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.*/

var logger = require('./util/log.js').log.getLogger('exportmap');
var db = require('./db').database;
var Canvas = require('canvas')
, Image = Canvas.Image;

var arrow = [
             [ 2, 0 ],
             [ -10, -4 ],
             [ -10, 4]
         ];

function drawFilledPolygon(ctx, shape) {
    ctx.beginPath();
    ctx.moveTo(shape[0][0],shape[0][1]);

    for(p in shape)
        if (p > 0) ctx.lineTo(shape[p][0],shape[p][1]);

    ctx.lineTo(shape[0][0],shape[0][1]);
    ctx.fill();
};

function translateShape(shape,x,y) {
    var rv = [];
    for(p in shape)
        rv.push([ shape[p][0] + x, shape[p][1] + y ]);
    return rv;
};

function rotateShape(shape,ang) {
    var rv = [];
    for(p in shape)
        rv.push(rotatePoint(ang,shape[p][0],shape[p][1]));
    return rv;
};
function rotatePoint(ang,x,y) {
    return [
        (x * Math.cos(ang)) - (y * Math.sin(ang)),
        (x * Math.sin(ang)) + (y * Math.cos(ang))
    ];
};

function drawLineArrow(ctx, x1,y1,x2,y2) {
    ctx.beginPath();
    ctx.moveTo(x1,y1);
    ctx.lineTo(x2,y2);
    ctx.stroke();
    var ang = Math.atan2(y2-y1,x2-x1);
    drawFilledPolygon(ctx, translateShape(rotateShape(arrow,ang),x2,y2));
};

var draw = function(res, filename, map){
	var x = 1280;
	var y = 800;
	var canvas = new Canvas(x,y);
	var stream = canvas.pngStream();
	var ctx = canvas.getContext('2d');

	// background
	ctx.fillStyle = "#FFFFFF";
	ctx.fillRect(0,0,x,y);

	ctx.strokeStyle = "#000000";
	ctx.fillStyle = "#000000";
	ctx.font = 'bold 12px Impact';

	var name = ctx.measureText(map.name);
	ctx.fillText(map.name, 30, 15);

	ctx.font = '10px Impact';
	var description = ctx.measureText(map.description);
	ctx.fillText(map.description, 30, 30);

	var createdAt = 'Created at www.wardleymaps.com';
	var createdAtMeasure = ctx.measureText(createdAt);
	ctx.fillText(createdAt, x - 30 - createdAtMeasure.width, 15);

	//TODO: draw axes here (30px from each border)
	drawLineArrow(ctx, 30, y - 30, 30, 40);
	drawLineArrow(ctx, 30, y - 30, x - 30, y - 30);

	ctx.fillText('Ubiquity', x - 60, y - 15);
	ctx.fillText('Value', 33, 60);

	// calculate nodes

	var availableWidth = x - 2 * 30;
	var availableHeight = y - 2 * 30;

	var calculatedNodes = [];
	for(var index in map.nodes){
		var node = map.nodes[index];
		var cX = Math.floor(node.positionX * availableWidth) + 30;
		var cY = Math.floor(node.positionY * availableHeight) + 30;
		calculatedNodes.push({
			x : cX,
			y : cY,
			name : node.name,
			componentId : node.componentId
		});
	}
	ctx.stroke();
	//draw connections
	for(var index in map.connections){
		var connection = map.connections[index];
		var source = connection.pageSourceId;
		var target = connection.pageTargetId;
		var start;
		var end;
		for(var nodeIndex in calculatedNodes){

			if(calculatedNodes[nodeIndex].componentId == source){
				start = calculatedNodes[nodeIndex];
			}
			if(calculatedNodes[nodeIndex].componentId == target){
				end = calculatedNodes[nodeIndex];
			}

		}
		ctx.strokeStyle = 'silver';
		ctx.moveTo(start.x, start.y);
		ctx.lineTo(end.x,end.y);
		ctx.stroke();
	}

	ctx.strokeStyle = 'black';
	//and now draw nodes
	for ( var index in calculatedNodes) {
		ctx.beginPath();
		ctx.arc(calculatedNodes[index].x, calculatedNodes[index].y, 10, 0,
				2 * Math.PI);
		ctx.stroke();

		//background
		ctx.fillStyle = 'silver';
		ctx.fill();

		ctx.fillStyle = 'black';
		ctx.fillText(calculatedNodes[index].name,
				calculatedNodes[index].x + 10, calculatedNodes[index].y - 10);
	}

	res.setHeader('Content-Type', 'image/png');
	stream.on('data', function(chunk){
		  res.write(chunk);
	});
	stream.on('end', function(){
		res.end();
	});
};

var thumbnail_draw = function(res, filename, map){
	var x = 100;
	var y = 100;
	var canvas = new Canvas(x,y);
	var stream = canvas.pngStream();
	var ctx = canvas.getContext('2d');

	// background
	ctx.fillStyle = "#FFFFFF";
	ctx.fillRect(0,0,x,y);

	// calculate nodes

	var availableWidth = x ;
	var availableHeight = y ;

	var calculatedNodes = [];
	for(var index in map.nodes){
		var node = map.nodes[index];
		var cX = Math.floor(node.positionX * availableWidth);
		var cY = Math.floor(node.positionY * availableHeight);
		calculatedNodes.push({
			x : cX,
			y : cY,
			name : node.name,
			componentId : node.componentId
		});
	}

	ctx.strokeStyle = 'silver';
	ctx.fillStyle = 'silver';

	//draw connections
	for(var index in map.connections){
		var connection = map.connections[index];
		var source = connection.pageSourceId;
		var target = connection.pageTargetId;
		var start;
		var end;
		for(var nodeIndex in calculatedNodes){

			if(calculatedNodes[nodeIndex].componentId == source){
				start = calculatedNodes[nodeIndex];
			}
			if(calculatedNodes[nodeIndex].componentId == target){
				end = calculatedNodes[nodeIndex];
			}

		}
		ctx.moveTo(start.x, start.y);
		ctx.lineTo(end.x,end.y);
		ctx.stroke();
	}

	//and now draw nodes
	for ( var index in calculatedNodes) {
		ctx.beginPath();
		ctx.arc(calculatedNodes[index].x, calculatedNodes[index].y, 1, 0,
				2 * Math.PI);
		ctx.stroke();

		//background
		ctx.fill();
	}

	res.setHeader('Content-Type', 'image/png');
	stream.on('data', function(chunk){
		  res.write(chunk);
	});
	stream.on('end', function(){
		res.end();
	});
};

var exportmap = function(req, res, mapId, filename) {
	var userId = require('./db').toDatabaseId(req.user);
	mapId = require('./db').toDatabaseId(mapId);
	logger.debug("drawing map", mapId, "for user", userId);

	db.maps.find({
		"userId" : userId,
		"_id" : mapId
	}).toArray(function(err, maps) {
		res.setHeader('Content-Type', 'application/json');
		if (err !== null) {
			logger.error(err);
			res.statusCode = 500;
			res.send(JSON.stringify(err));
		} else {
			draw(res, mapId, maps[0]);
		}
	});
};

var thumbnail = function(req, res, mapId, filename) {
	var userId = require('./db').toDatabaseId(req.user);
	mapId = require('./db').toDatabaseId(mapId);
	logger.debug("drawing thumbnail", mapId, "for user", userId);

	db.maps.find({
		"userId" : userId,
		"_id" : mapId
	}).toArray(function(err, maps) {
		res.setHeader('Content-Type', 'application/json');
		if (err !== null) {
			logger.error(err);
			res.statusCode = 500;
			res.send(JSON.stringify(err));
		} else {
			thumbnail_draw(res, mapId, maps[0]);
		}
	});
};

exports.exportmap = exportmap;
exports.thumbnail = thumbnail;<|endoftext|>


<commit_before>  Cropper.DEFAULTS = {

    // Defines the aspect ratio of the crop box
    aspectRatio: NaN,

    // Defines the percentage of automatic cropping area when initializes
    autoCropArea: 0.8,

    // Outputs the cropping results
    crop: null,

    // Previous cropping result data
    data: null,

    // Add extra containers (jQuery selector) for previewing
    preview: '',

    // Strict mode, the image cannot zoom out less than the container
    strict: true,

    // Rebuild when resize the window
    responsive: true,

    // Check if the target image is cross origin
    checkImageOrigin: true,

    // Show the black modal
    modal: true,

    // Show the dashed lines for guiding
    guides: true,

    // Show the center indicator for guiding
    center: true,

    // Show the white modal to highlight the crop box
    highlight: true,

    // Show the grid background
    background: true,

    // Enable to crop the image automatically when initialize
    autoCrop: true,

    // Enable to create new crop box by dragging over the image
    dragCrop: true,

    // Enable to move the image
    movable: true,

    // Enable to rotate the image
    rotatable: true,

    // Enable to zoom the image
    zoomable: true,

    // Enable to zoom the image by wheeling mouse
    touchDragZoom: true,

    // Enable to zoom the image by dragging touch
    mouseWheelZoom: true,

    // Enable to move the crop box
    cropBoxMovable: true,

    // Enable to resize the crop box
    cropBoxResizable: true,

    // Toggle drag mode between "crop" and "move" when double click on the cropper
    doubleClickToggle: true,

    // Size limitation
    minCanvasWidth: 0,
    minCanvasHeight: 0,
    minCropBoxWidth: 0,
    minCropBoxHeight: 0,
    minContainerWidth: 200,
    minContainerHeight: 100,

    // Shortcuts of events
    build: null,
    built: null,
    cropstart: null,
    cropmove: null,
    cropend: null,
    change: null,
    zoom: null
  };

  Cropper.setDefaults = function (options) {
    $.extend(Cropper.DEFAULTS, options);
  };
<commit_msg>Update comments<commit_after>  Cropper.DEFAULTS = {

    // Defines the aspect ratio of the crop box
    aspectRatio: NaN,

    // Defines the percentage of automatic cropping area when initializes
    autoCropArea: 0.8,

    // A function for outputting the cropping results
    crop: null,

    // An object with the previous cropping result data
    data: null,

    // A jQuery selector for adding extra containers to preview
    preview: '',

    // Strict mode, the image cannot zoom out less than the container
    strict: true,

    // Rebuild when resize the window
    responsive: true,

    // Check if the target image is cross origin
    checkImageOrigin: true,

    // Show the black modal
    modal: true,

    // Show the dashed lines for guiding
    guides: true,

    // Show the center indicator for guiding
    center: true,

    // Show the white modal to highlight the crop box
    highlight: true,

    // Show the grid background
    background: true,

    // Enable to crop the image automatically when initialize
    autoCrop: true,

    // Enable to create new crop box by dragging over the image
    dragCrop: true,

    // Enable to move the image
    movable: true,

    // Enable to rotate the image
    rotatable: true,

    // Enable to zoom the image
    zoomable: true,

    // Enable to zoom the image by wheeling mouse
    touchDragZoom: true,

    // Enable to zoom the image by dragging touch
    mouseWheelZoom: true,

    // Enable to move the crop box
    cropBoxMovable: true,

    // Enable to resize the crop box
    cropBoxResizable: true,

    // Toggle drag mode between "crop" and "move" when double click on the cropper
    doubleClickToggle: true,

    // Size limitation
    minCanvasWidth: 0,
    minCanvasHeight: 0,
    minCropBoxWidth: 0,
    minCropBoxHeight: 0,
    minContainerWidth: 200,
    minContainerHeight: 100,

    // Shortcuts of events
    build: null,
    built: null,
    cropstart: null,
    cropmove: null,
    cropend: null,
    change: null,
    zoom: null
  };

  Cropper.setDefaults = function (options) {
    $.extend(Cropper.DEFAULTS, options);
  };
<|endoftext|>


<commit_before>import del from 'del'
import cp from 'recursive-copy'
import mkdirp from 'mkdirp-then'
import walk from 'walk'
import { resolve, join, dirname, sep } from 'path'
import { existsSync, readFileSync, writeFileSync } from 'fs'
import getConfig from './config'
import { renderToHTML } from './render'
import { getAvailableChunks } from './utils'
import { printAndExit } from '../lib/utils'

export default async function (dir, options) {
  dir = resolve(dir)
  const config = getConfig(dir)
  const nextDir = join(dir, config.distDir)

  log(`  using build directory: ${nextDir}`)

  if (!existsSync(nextDir)) {
    console.error(
      `Build directory ${nextDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
    )
    process.exit(1)
  }

  const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8')
  const buildStats = require(join(nextDir, 'build-stats.json'))

  // Initialize the output directory
  const outDir = options.outdir
  await del(join(outDir, '*'))
  await mkdirp(join(outDir, '_next', buildStats['app.js'].hash))
  await mkdirp(join(outDir, '_next', buildId))

  // Copy files
  await cp(
    join(nextDir, 'app.js'),
    join(outDir, '_next', buildStats['app.js'].hash, 'app.js')
  )

  // Copy static directory
  if (existsSync(join(dir, 'static'))) {
    log('  copying "static" directory')
    await cp(
      join(dir, 'static'),
      join(outDir, 'static')
    )
  }

  // Copy dynamic import chunks
  if (existsSync(join(nextDir, 'chunks'))) {
    log('  copying dynamic import chunks')

    await mkdirp(join(outDir, '_next', buildId, 'webpack'))
    await cp(
      join(nextDir, 'chunks'),
      join(outDir, '_next', buildId, 'webpack', 'chunks')
    )
  }

  await copyPages(nextDir, outDir, buildId)

  // Get the exportPathMap from the `next.config.js`
  if (typeof config.exportPathMap !== 'function') {
    printAndExit(
      '> Could not found "exportPathMap" function inside "next.config.js"\n' +
      '> "next export" uses that function build html pages.'
    )
  }

  const exportPathMap = await config.exportPathMap()
  const exportPaths = Object.keys(exportPathMap)

  // Start the rendering process
  const renderOpts = {
    dir,
    buildStats,
    buildId,
    nextExport: true,
    assetPrefix: config.assetPrefix.replace(/\/$/, ''),
    dev: false,
    staticMarkup: false,
    hotReloader: null,
    availableChunks: getAvailableChunks(dir, config.distDir)
  }

  // We need this for server rendering the Link component.
  global.__NEXT_DATA__ = {
    nextExport: true
  }

  for (const path of exportPaths) {
    log(`  exporting path: ${path}`)

    const { page, query = {} } = exportPathMap[path]
    const req = { url: path }
    const res = {}

    const htmlFilename = path === '/' ? 'index.html' : `${path}${sep}index.html`
    const baseDir = join(outDir, dirname(htmlFilename))
    const htmlFilepath = join(outDir, htmlFilename)

    await mkdirp(baseDir)

    const html = await renderToHTML(req, res, page, query, renderOpts)
    writeFileSync(htmlFilepath, html, 'utf8')
  }

  // Add an empty line to the console for the better readability.
  log('')

  function log (message) {
    if (options.silent) return
    console.log(message)
  }
}

function copyPages (nextDir, outDir, buildId) {
  // TODO: do some proper error handling
  return new Promise((resolve, reject) => {
    const nextBundlesDir = join(nextDir, 'bundles', 'pages')
    const walker = walk.walk(nextBundlesDir, { followLinks: false })

    walker.on('file', (root, stat, next) => {
      const filename = stat.name
      const fullFilePath = `${root}${sep}${filename}`
      const relativeFilePath = fullFilePath.replace(nextBundlesDir, '')

      // We should not expose this page to the client side since
      // it has no use in the client side.
      if (relativeFilePath === '/_document.js') {
        next()
        return
      }

      let destFilePath = null
      if (/index\.js$/.test(filename)) {
        destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
      } else {
        const newRelativeFilePath = relativeFilePath.replace(/\.js/, `${sep}index.js`)
        destFilePath = join(outDir, '_next', buildId, 'page', newRelativeFilePath)
      }

      cp(fullFilePath, destFilePath)
        .then(next)
        .catch(reject)
    })

    walker.on('end', resolve)
  })
}
<commit_msg>Fix error messages in server/export.js (#2933)<commit_after>import del from 'del'
import cp from 'recursive-copy'
import mkdirp from 'mkdirp-then'
import walk from 'walk'
import { resolve, join, dirname, sep } from 'path'
import { existsSync, readFileSync, writeFileSync } from 'fs'
import getConfig from './config'
import { renderToHTML } from './render'
import { getAvailableChunks } from './utils'
import { printAndExit } from '../lib/utils'

export default async function (dir, options) {
  dir = resolve(dir)
  const config = getConfig(dir)
  const nextDir = join(dir, config.distDir)

  log(`  using build directory: ${nextDir}`)

  if (!existsSync(nextDir)) {
    console.error(
      `Build directory ${nextDir} does not exist. Make sure you run "next build" before running "next start" or "next export".`
    )
    process.exit(1)
  }

  const buildId = readFileSync(join(nextDir, 'BUILD_ID'), 'utf8')
  const buildStats = require(join(nextDir, 'build-stats.json'))

  // Initialize the output directory
  const outDir = options.outdir
  await del(join(outDir, '*'))
  await mkdirp(join(outDir, '_next', buildStats['app.js'].hash))
  await mkdirp(join(outDir, '_next', buildId))

  // Copy files
  await cp(
    join(nextDir, 'app.js'),
    join(outDir, '_next', buildStats['app.js'].hash, 'app.js')
  )

  // Copy static directory
  if (existsSync(join(dir, 'static'))) {
    log('  copying "static" directory')
    await cp(
      join(dir, 'static'),
      join(outDir, 'static')
    )
  }

  // Copy dynamic import chunks
  if (existsSync(join(nextDir, 'chunks'))) {
    log('  copying dynamic import chunks')

    await mkdirp(join(outDir, '_next', buildId, 'webpack'))
    await cp(
      join(nextDir, 'chunks'),
      join(outDir, '_next', buildId, 'webpack', 'chunks')
    )
  }

  await copyPages(nextDir, outDir, buildId)

  // Get the exportPathMap from the `next.config.js`
  if (typeof config.exportPathMap !== 'function') {
    printAndExit(
      '> Could not find "exportPathMap" function inside "next.config.js"\n' +
      '> "next export" uses that function to build html pages.'
    )
  }

  const exportPathMap = await config.exportPathMap()
  const exportPaths = Object.keys(exportPathMap)

  // Start the rendering process
  const renderOpts = {
    dir,
    buildStats,
    buildId,
    nextExport: true,
    assetPrefix: config.assetPrefix.replace(/\/$/, ''),
    dev: false,
    staticMarkup: false,
    hotReloader: null,
    availableChunks: getAvailableChunks(dir, config.distDir)
  }

  // We need this for server rendering the Link component.
  global.__NEXT_DATA__ = {
    nextExport: true
  }

  for (const path of exportPaths) {
    log(`  exporting path: ${path}`)

    const { page, query = {} } = exportPathMap[path]
    const req = { url: path }
    const res = {}

    const htmlFilename = path === '/' ? 'index.html' : `${path}${sep}index.html`
    const baseDir = join(outDir, dirname(htmlFilename))
    const htmlFilepath = join(outDir, htmlFilename)

    await mkdirp(baseDir)

    const html = await renderToHTML(req, res, page, query, renderOpts)
    writeFileSync(htmlFilepath, html, 'utf8')
  }

  // Add an empty line to the console for the better readability.
  log('')

  function log (message) {
    if (options.silent) return
    console.log(message)
  }
}

function copyPages (nextDir, outDir, buildId) {
  // TODO: do some proper error handling
  return new Promise((resolve, reject) => {
    const nextBundlesDir = join(nextDir, 'bundles', 'pages')
    const walker = walk.walk(nextBundlesDir, { followLinks: false })

    walker.on('file', (root, stat, next) => {
      const filename = stat.name
      const fullFilePath = `${root}${sep}${filename}`
      const relativeFilePath = fullFilePath.replace(nextBundlesDir, '')

      // We should not expose this page to the client side since
      // it has no use in the client side.
      if (relativeFilePath === '/_document.js') {
        next()
        return
      }

      let destFilePath = null
      if (/index\.js$/.test(filename)) {
        destFilePath = join(outDir, '_next', buildId, 'page', relativeFilePath)
      } else {
        const newRelativeFilePath = relativeFilePath.replace(/\.js/, `${sep}index.js`)
        destFilePath = join(outDir, '_next', buildId, 'page', newRelativeFilePath)
      }

      cp(fullFilePath, destFilePath)
        .then(next)
        .catch(reject)
    })

    walker.on('end', resolve)
  })
}
<|endoftext|>


<commit_before>'use strict';

var config = require('./config');
var leagues = require('../data/leagues');

// Sanitizes a string to be a filename
function stringSanitize(str) {
    return str
        .toLowerCase()
        .replace(/[àáâãäå]/g, 'a')
        .replace(/[èéêë]/g, 'e')
        .replace(/[ìíîï]/g, 'i')
        .replace(/[òóôõö]/g, 'o')
        .replace(/[ùúûü]/g, 'u')
        .replace(/æ/g, 'ae')
        .replace(/ç/g, 'c')
        .replace(/ñ/g, 'n')
        .replace(/œ/g, 'oe')
        .replace(/[ýÿ]/g, 'y')
        .replace(/[^a-z0-9]/gi, '_');
}

// Formats a string with arguments
function stringFormat(str) {
    for (var i = 0; i + 1 < arguments.length; i++) {
        str = str.replace('{' + i + '}', arguments[i + 1]);
    }

    return str;
}

// Reads a json file and call the callback with its content
function readJsonFile(path, callback) {
    var jsonfile = require('jsonfile');
    jsonfile.readFile(path, { throws: false }, (err, data) => {
        callback(data || []);
    });
}

// Writes content in a json file
function writeJsonFile(path, content) {
    var fs = require('fs');
    fs.writeFile(path, JSON.stringify(content, null, 4), (err) => {
        if (err) {
            console.log(err);
        } else {
            console.log('File updated: ' + path);
        }
    });
}

// Scrapes an url and call the callback with its content
function scrapeUrl(url, callback) {
    var request = require('request');
    var cheerio = require('cheerio');
    request(url, (err, resp, body) => {
        if (err) {
            console.log(err);
        } else {
            var $ = cheerio.load(body);
            callback($);
        }
    });
}

// Gets the team object with the closest name match
function getClosestTeam(teams, name) {
    return teams[1];
}

module.exports = {
    stringSanitize: stringSanitize,
    stringFormat: stringFormat,
    readJsonFile: readJsonFile,
    writeJsonFile: writeJsonFile,
    scrapeUrl: scrapeUrl,
    getClosestTeam: getClosestTeam
};<commit_msg>Complete getClosestTeam function<commit_after>'use strict';

var config = require('./config');
var leagues = require('../data/leagues');

// Sanitizes a string to be a filename
function stringSanitize(str) {
    return str
        .toLowerCase()
        .replace(/[àáâãäå]/g, 'a')
        .replace(/[èéêë]/g, 'e')
        .replace(/[ìíîï]/g, 'i')
        .replace(/[òóôõö]/g, 'o')
        .replace(/[ùúûü]/g, 'u')
        .replace(/æ/g, 'ae')
        .replace(/ç/g, 'c')
        .replace(/ñ/g, 'n')
        .replace(/œ/g, 'oe')
        .replace(/[ýÿ]/g, 'y')
        .replace(/[^a-z0-9]/gi, '_');
}

// Formats a string with arguments
function stringFormat(str) {
    for (var i = 0; i + 1 < arguments.length; i++) {
        str = str.replace('{' + i + '}', arguments[i + 1]);
    }

    return str;
}

// Reads a json file and call the callback with its content
function readJsonFile(path, callback) {
    var jsonfile = require('jsonfile');
    jsonfile.readFile(path, { throws: false }, (err, data) => {
        callback(data || []);
    });
}

// Writes content in a json file
function writeJsonFile(path, content) {
    var fs = require('fs');
    fs.writeFile(path, JSON.stringify(content, null, 4), (err) => {
        if (err) {
            console.log(err);
        } else {
            console.log('File updated: ' + path);
        }
    });
}

// Scrapes an url and call the callback with its content
function scrapeUrl(url, callback) {
    var request = require('request');
    var cheerio = require('cheerio');
    request(url, (err, resp, body) => {
        if (err) {
            console.log(err);
        } else {
            var $ = cheerio.load(body);
            callback($);
        }
    });
}

// Gets the team object with the closest name match
function getClosestTeam(teams, name) {
    var didYouMean = require('didYouMean');
    didYouMean.returnWinningObject = true;
    didYouMean.threshold = null;
    didYouMean.thresholdAbsolute = null;

    return didYouMean(name, teams, 'team');
}

module.exports = {
    stringSanitize: stringSanitize,
    stringFormat: stringFormat,
    readJsonFile: readJsonFile,
    writeJsonFile: writeJsonFile,
    scrapeUrl: scrapeUrl,
    getClosestTeam: getClosestTeam
};<|endoftext|>


<commit_before>(function(){

    var $compare_flow,
        $dropdown_wrapper,
        $btn_comparison_cancel,
        $btn_comparison_submit,
        $comparison_option_left,
        $comparison_option_right,
        $comparison_default_right,
        $comparison_result_wrapper;

    var templates = {};

    var entityOne = null,
        entityTwo = null,
        entityOneXHR,
        entityTwoXHR;

    var getCompareOptionHTML = function(data) {
        data.type_name = data.type.id === 1 ? 'school' : 'preschool';
        return templates['compare-option'](data);
    };

    var fetchData = function(entity) {
        var schoolURL = '/schools/school/' + entity.id;
        var endpoints = [
            schoolURL + '/infrastructure',
            schoolURL + '/finance',
            schoolURL + '/demographics'
        ];
        var endpointsParamsString = _(endpoints).map(function(s) {
            return 'endpoints=' + s;
        }).join('&');
        var $deferred = $.Deferred();
        var apiURL = 'merge?' + endpointsParamsString;
        var apiXHR = klp.api.do(apiURL);
        apiXHR.done(function(data) {
            entity.infrastructure_data = data[endpoints[0]];
            entity.finance_data = data[endpoints[1]];
            entity.demographics_data = data[endpoints[2]];
            $deferred.resolve(entity);
        });
        apiXHR.fail(function(err) {
            alert("failed to load school data");
            $deferred.reject(err);
        });
        return $deferred;
    };

    var getContext = function(entity) {
        console.log("get context called with ", entity);
        var context = $.extend(entity, klp.utils.getBoyGirlPercents(entity.num_boys, entity.num_girls));
        var mt = klp.utils.getMTProfilePercents(entity.demographics_data.mt_profile);
        context.mt_profile_percents = mt.percents || {};
        context.total_mt = mt.total;
        context.hasFinanceData = entity.finance_data.sg_amount || entity.finance_data.smg_amount || entity.finance_data.tlm_amount;
        if (context.hasFinanceData) {
            context.finance_percents = klp.utils.getFinancePercents(entity.finance_data);
        }
        context.studentTeacherRatio = klp.utils.getStudentTeacherRatio(entity.infrastructure_data);
        context.hasStudents = entity.num_boys || entity.num_girls;
        return context;
    };

    var getMTProfiles = function(entity1, entity2) {
        var mt1 = entity1.mt_profile_percents || {};
        var mt2 = entity2.mt_profile_percents || {};
        console.log("mt1", mt1, "mt2", mt2);
        var allLanguages = _(_(mt1).keys().concat(_(mt2).keys())).unique();
        console.log("all languages", allLanguages);
        var mts = {};
        _(allLanguages).each(function(lang) {
            mts[lang] = {};
            mts[lang].school1 = {
                'percent': mt1.hasOwnProperty(lang) ? mt1[lang] : 0,
                'total': entity1.demographics_data.mt_profile.hasOwnProperty(lang) ? entity1.demographics_data.mt_profile[lang] : 0
            };
            mts[lang].school2 = {
                'percent': mt2.hasOwnProperty(lang) ? mt2[lang] : 0,
                'total': entity2.demographics_data.mt_profile.hasOwnProperty(lang) ? entity2.demographics_data.mt_profile[lang] : 0
            };
        });
        console.log("mts", mts);
        return mts;
    };

    var getInfrastructureComparison = function(entity1, entity2) {
        var data1 = entity1.infrastructure_data.dise_facility;
        var data2 = entity2.infrastructure_data.dise_facility;
        var topLevelKeys = _(_(data1).keys().concat(_(data2).keys())).unique();
        var allOptions = {};
        _(topLevelKeys).each(function(key) {
            if (key in data1 && key in data2) {
                var arr = _(_(data1[key]).keys().concat(_(data2[key]).keys())).unique();
            } else if (key in data1) {
                var arr = _(data1[key]).keys();
            } else {
                var arr = _(data2[key]).keys();
            }
            allOptions[key] = arr;
        });
        var data = {};
        _(allOptions).each(function(value, key) {
            data[key] = {};
            _(allOptions[key]).each(function(nestedKey) {
                data[key][nestedKey] = {
                    'entity1': getInfrastructureItem(data1, key, nestedKey),
                    'entity2': getInfrastructureItem(data2, key, nestedKey)
                }
            });
        });
        console.log("infrastructure compare data ", data);
        return data;
    };

    var getInfrastructureItem = function(entity, key, nestedKey) {
        if (!entity.hasOwnProperty(key) || !entity[key].hasOwnProperty(nestedKey)) {
            return 'grey';
        }
        var hasInfra = entity[key][nestedKey];
        if (hasInfra) {
            return 'green';
        } else {
            return 'red';
        }
    };

    var selectOptionRight = function(entity) {
        entityTwo = entity;
        entityTwoXHR = fetchData(entity);
        var html = getCompareOptionHTML(entity);
        //console.log("right html", html);
        $dropdown_wrapper.removeClass("show");
        $comparison_option_right.html(html);
        $comparison_default_right.hide();
        $comparison_option_right.removeClass('hide').show();
        show_submit_button();
    };

    var open = function(entity1){
        //e.preventDefault();
        console.log("compare open called with ", entity1);
        if(klp.map){
            klp.map.closePopup();
        }
        entityOne = entity1;
        entityOneXHR = fetchData(entity1);
        var html = getCompareOptionHTML(entity1);
        $comparison_option_left.html(html);
        //klp.place_info.close_place();
        $comparison_result_wrapper.removeClass('show');

        $compare_flow.removeClass("hide");

        setTimeout(function(){
            $compare_flow.removeClass("hide").addClass("show");
        },0);
    };

    var close = function(e){
        e.preventDefault();
        $compare_flow.removeClass("show");
        entityOne = entityTwo = entityOneXHR = entityTwoXHR = null;
        setTimeout(function(){
            $compare_flow.removeClass("show").addClass("hide");
            clear_option_left();
            clear_option_right();
        },300);
    };

    var reset_submit_button = function(){
        $btn_comparison_cancel.addClass("show");
        $btn_comparison_submit.removeClass("show");
        $comparison_result_wrapper.html("").removeClass('show');
    };
    var show_submit_button = function(){
        $btn_comparison_cancel.removeClass("show");
        $btn_comparison_submit.addClass("show");
    };
    var clear_option_left = function(){
        // to do
        reset_submit_button();
    };
    var clear_option_right = function(e){
        if(e){
            e.preventDefault();
        }

        $comparison_option_right.hide();
        $comparison_default_right.show();

        reset_submit_button();
    };
    var show_options_dropdown_right = function(e){
        e.preventDefault();
        $dropdown_wrapper.toggleClass("show");
    };

    var init = function(){
        $compare_flow = $("#compare_flow");
        $dropdown_wrapper = $('#dropdown_wrapper');

        $btn_comparison_cancel = $compare_flow.find(".js-btn-cancel");
        $btn_comparison_submit = $compare_flow.find(".js-btn-compare");

        $comparison_option_left = $(".js-comparison-option-left");
        $comparison_option_right = $(".js-comparison-option-right");
        $comparison_default_right = $(".js-comparison-default-right");

        $comparison_result_wrapper = $(".js-comparison-result-wrapper");
        $comparison_search = $("#comparison-search-select");
        _(['compare-option', 'comparison-result']).each(function(templateName) {
            var html = $('#tpl-' + templateName).html();
            //console.log("html", templateName, html);
            templates[templateName] = swig.compile(html);
        });

        $comparison_search.select2({
            placeholder: 'Search for schools...',
            minimumInputLength: 3,
            quietMillis: 300,
            allowClear: true,
            ajax: {
                url: "/api/v1/schools/info",
                quietMillis: 300,
                allowClear: true,
                data: function (term, page) {
                    return {
                        search: term,
                        type: entityOne.type.id === 1 ? 'primaryschools' : 'preschools'
                    };
                },
                results: function (data, page) {
                    //console.log("data", data);
                    return {results: data.features};
                }
            },
            formatResult: function(item) {
                return item.name;
                // console.log("item", item);
                // return {
                //     id: item.id,
                //     text: item.name,
                //     data: item
                // }
            },
            formatSelection: function(item) {
                return item.name;
            }
        });

        $comparison_search.on("change", function(e) {
            //console.log("changed event ", e);
            var data = e.added;
            selectOptionRight(data);
        });

        //$(document).on('click', ".js-trigger-compare", open);
        $(document).on('click', ".js-comparison-close", close);
        $(document).on('click', ".js-comparison-clear-left", clear_option_left);
        $(document).on('click', ".js-comparison-clear-right", clear_option_right);
        $(document).on('click', ".js-comparison-show-options-right", show_options_dropdown_right);
        //$(document).on('click', ".js-dropdown-option-right", select_options_right);
        $(document).on('click', ".js-btn-compare", function(e){
            e.preventDefault();
            $.when(entityOneXHR, entityTwoXHR).done(function(data1, data2) {
                //console.log("compare xhrs done ", data1, data2);
                console.log("school1", data1, "school2", data2);
                var school1 = getContext(data1);
                var school2 = getContext(data2);

                var context = {
                    'school1': school1,
                    'school2': school2,
                    'mt_profiles': getMTProfiles(school1, school2),
                    'infrastructure': getInfrastructureComparison(school1, school2)
                };
                var html = templates['comparison-result'](context);
                //console.log('comparison result html', html);
                $comparison_result_wrapper.html(html);
                $comparison_result_wrapper.html(html).addClass('show');
                setTimeout(function(){
                    var $selects = $comparison_result_wrapper.find('select');
                    $selects.easyDropDown();
                },0);

                setTimeout(function(){
                    init_comparison_charts(context);
                },100);
                //var html = templates['comparison-result'](context);
            });
            $btn_comparison_submit.removeClass("show");
            //var html = klp._tpl.comparison_result();
            //$comparison_result_wrapper.html(html).addClass('show');

        });
    };

    klp.comparison = {
        init: init,
        open: open,
        close: close
    };

    function init_comparison_charts(context) {
        var chart_width = 300;
        var s1 = context.school1;
        var s2 = context.school2;
        console.log("charts init called with ", context);
        var chartOptions = {
            innerSize: '85%',
            width: chart_width,
            height: chart_width
        };

        $('#comparison_boygirlchart_1').boyGirlChart(s1, chartOptions);

        $('#comparison_boygirlchart_2').boyGirlChart(s2, chartOptions);

        if (s1.hasFinanceData) {
            $('#comparison_financechart_1').financeChart(s1.finance_percents, chartOptions);
        }

        if (s2.hasFinanceData) {
            $('#comparison_financechart_2').financeChart(s2.finance_percents, chartOptions);
        }

    }
})();<commit_msg>fix compare modal not loading if infrastructure data is missing<commit_after>(function(){

    var $compare_flow,
        $dropdown_wrapper,
        $btn_comparison_cancel,
        $btn_comparison_submit,
        $comparison_option_left,
        $comparison_option_right,
        $comparison_default_right,
        $comparison_result_wrapper;

    var templates = {};

    var entityOne = null,
        entityTwo = null,
        entityOneXHR,
        entityTwoXHR;

    var getCompareOptionHTML = function(data) {
        data.type_name = data.type.id === 1 ? 'school' : 'preschool';
        return templates['compare-option'](data);
    };

    var fetchData = function(entity) {
        var schoolURL = '/schools/school/' + entity.id;
        var endpoints = [
            schoolURL + '/infrastructure',
            schoolURL + '/finance',
            schoolURL + '/demographics'
        ];
        var endpointsParamsString = _(endpoints).map(function(s) {
            return 'endpoints=' + s;
        }).join('&');
        var $deferred = $.Deferred();
        var apiURL = 'merge?' + endpointsParamsString;
        var apiXHR = klp.api.do(apiURL);
        apiXHR.done(function(data) {
            entity.infrastructure_data = data[endpoints[0]];
            entity.finance_data = data[endpoints[1]];
            entity.demographics_data = data[endpoints[2]];
            $deferred.resolve(entity);
        });
        apiXHR.fail(function(err) {
            alert("failed to load school data");
            $deferred.reject(err);
        });
        return $deferred;
    };

    var getContext = function(entity) {
        console.log("get context called with ", entity);
        var context = $.extend(entity, klp.utils.getBoyGirlPercents(entity.num_boys, entity.num_girls));
        var mt = klp.utils.getMTProfilePercents(entity.demographics_data.mt_profile);
        context.mt_profile_percents = mt.percents || {};
        context.total_mt = mt.total;
        context.hasFinanceData = entity.finance_data.sg_amount || entity.finance_data.smg_amount || entity.finance_data.tlm_amount;
        if (context.hasFinanceData) {
            context.finance_percents = klp.utils.getFinancePercents(entity.finance_data);
        }
        context.studentTeacherRatio = klp.utils.getStudentTeacherRatio(entity.infrastructure_data);
        context.hasStudents = entity.num_boys || entity.num_girls;
        return context;
    };

    var getMTProfiles = function(entity1, entity2) {
        var mt1 = entity1.mt_profile_percents || {};
        var mt2 = entity2.mt_profile_percents || {};
        console.log("mt1", mt1, "mt2", mt2);
        var allLanguages = _(_(mt1).keys().concat(_(mt2).keys())).unique();
        console.log("all languages", allLanguages);
        var mts = {};
        _(allLanguages).each(function(lang) {
            mts[lang] = {};
            mts[lang].school1 = {
                'percent': mt1.hasOwnProperty(lang) ? mt1[lang] : 0,
                'total': entity1.demographics_data.mt_profile.hasOwnProperty(lang) ? entity1.demographics_data.mt_profile[lang] : 0
            };
            mts[lang].school2 = {
                'percent': mt2.hasOwnProperty(lang) ? mt2[lang] : 0,
                'total': entity2.demographics_data.mt_profile.hasOwnProperty(lang) ? entity2.demographics_data.mt_profile[lang] : 0
            };
        });
        console.log("mts", mts);
        return mts;
    };

    var getInfrastructureComparison = function(entity1, entity2) {
        var data1 = entity1.infrastructure_data.dise_facility || {};
        var data2 = entity2.infrastructure_data.dise_facility || {};
        var topLevelKeys = _(_(data1).keys().concat(_(data2).keys())).unique();
        var allOptions = {};
        _(topLevelKeys).each(function(key) {
            if (key in data1 && key in data2) {
                var arr = _(_(data1[key]).keys().concat(_(data2[key]).keys())).unique();
            } else if (key in data1) {
                var arr = _(data1[key]).keys();
            } else {
                var arr = _(data2[key]).keys();
            }
            allOptions[key] = arr;
        });
        var data = {};
        _(allOptions).each(function(value, key) {
            data[key] = {};
            _(allOptions[key]).each(function(nestedKey) {
                data[key][nestedKey] = {
                    'entity1': getInfrastructureItem(data1, key, nestedKey),
                    'entity2': getInfrastructureItem(data2, key, nestedKey)
                }
            });
        });
        console.log("infrastructure compare data ", data);
        return data;
    };

    var getInfrastructureItem = function(entity, key, nestedKey) {
        if (!entity.hasOwnProperty(key) || !entity[key].hasOwnProperty(nestedKey)) {
            return 'grey';
        }
        var hasInfra = entity[key][nestedKey];
        if (hasInfra) {
            return 'green';
        } else {
            return 'red';
        }
    };

    var selectOptionRight = function(entity) {
        entityTwo = entity;
        entityTwoXHR = fetchData(entity);
        var html = getCompareOptionHTML(entity);
        //console.log("right html", html);
        $dropdown_wrapper.removeClass("show");
        $comparison_option_right.html(html);
        $comparison_default_right.hide();
        $comparison_option_right.removeClass('hide').show();
        show_submit_button();
    };

    var open = function(entity1){
        //e.preventDefault();
        console.log("compare open called with ", entity1);
        if(klp.map){
            klp.map.closePopup();
        }
        entityOne = entity1;
        entityOneXHR = fetchData(entity1);
        var html = getCompareOptionHTML(entity1);
        $comparison_option_left.html(html);
        //klp.place_info.close_place();
        $comparison_result_wrapper.removeClass('show');

        $compare_flow.removeClass("hide");

        setTimeout(function(){
            $compare_flow.removeClass("hide").addClass("show");
        },0);
    };

    var close = function(e){
        e.preventDefault();
        $compare_flow.removeClass("show");
        entityOne = entityTwo = entityOneXHR = entityTwoXHR = null;
        setTimeout(function(){
            $compare_flow.removeClass("show").addClass("hide");
            clear_option_left();
            clear_option_right();
        },300);
    };

    var reset_submit_button = function(){
        $btn_comparison_cancel.addClass("show");
        $btn_comparison_submit.removeClass("show");
        $comparison_result_wrapper.html("").removeClass('show');
    };
    var show_submit_button = function(){
        $btn_comparison_cancel.removeClass("show");
        $btn_comparison_submit.addClass("show");
    };
    var clear_option_left = function(){
        // to do
        reset_submit_button();
    };
    var clear_option_right = function(e){
        if(e){
            e.preventDefault();
        }

        $comparison_option_right.hide();
        $comparison_default_right.show();

        reset_submit_button();
    };
    var show_options_dropdown_right = function(e){
        e.preventDefault();
        $dropdown_wrapper.toggleClass("show");
    };

    var init = function(){
        $compare_flow = $("#compare_flow");
        $dropdown_wrapper = $('#dropdown_wrapper');

        $btn_comparison_cancel = $compare_flow.find(".js-btn-cancel");
        $btn_comparison_submit = $compare_flow.find(".js-btn-compare");

        $comparison_option_left = $(".js-comparison-option-left");
        $comparison_option_right = $(".js-comparison-option-right");
        $comparison_default_right = $(".js-comparison-default-right");

        $comparison_result_wrapper = $(".js-comparison-result-wrapper");
        $comparison_search = $("#comparison-search-select");
        _(['compare-option', 'comparison-result']).each(function(templateName) {
            var html = $('#tpl-' + templateName).html();
            //console.log("html", templateName, html);
            templates[templateName] = swig.compile(html);
        });

        $comparison_search.select2({
            placeholder: 'Search for schools...',
            minimumInputLength: 3,
            quietMillis: 300,
            allowClear: true,
            ajax: {
                url: "/api/v1/schools/info",
                quietMillis: 300,
                allowClear: true,
                data: function (term, page) {
                    return {
                        search: term,
                        type: entityOne.type.id === 1 ? 'primaryschools' : 'preschools'
                    };
                },
                results: function (data, page) {
                    //console.log("data", data);
                    return {results: data.features};
                }
            },
            formatResult: function(item) {
                return item.name;
                // console.log("item", item);
                // return {
                //     id: item.id,
                //     text: item.name,
                //     data: item
                // }
            },
            formatSelection: function(item) {
                return item.name;
            }
        });

        $comparison_search.on("change", function(e) {
            //console.log("changed event ", e);
            var data = e.added;
            selectOptionRight(data);
        });

        //$(document).on('click', ".js-trigger-compare", open);
        $(document).on('click', ".js-comparison-close", close);
        $(document).on('click', ".js-comparison-clear-left", clear_option_left);
        $(document).on('click', ".js-comparison-clear-right", clear_option_right);
        $(document).on('click', ".js-comparison-show-options-right", show_options_dropdown_right);
        //$(document).on('click', ".js-dropdown-option-right", select_options_right);
        $(document).on('click', ".js-btn-compare", function(e){
            e.preventDefault();
            $.when(entityOneXHR, entityTwoXHR).done(function(data1, data2) {
                //console.log("compare xhrs done ", data1, data2);
                console.log("school1", data1, "school2", data2);
                var school1 = getContext(data1);
                var school2 = getContext(data2);

                var context = {
                    'school1': school1,
                    'school2': school2,
                    'mt_profiles': getMTProfiles(school1, school2),
                    'infrastructure': getInfrastructureComparison(school1, school2)
                };
                var html = templates['comparison-result'](context);
                //console.log('comparison result html', html);
                $comparison_result_wrapper.html(html);
                $comparison_result_wrapper.html(html).addClass('show');
                setTimeout(function(){
                    var $selects = $comparison_result_wrapper.find('select');
                    $selects.easyDropDown();
                },0);

                setTimeout(function(){
                    init_comparison_charts(context);
                },100);
                //var html = templates['comparison-result'](context);
            });
            $btn_comparison_submit.removeClass("show");
            //var html = klp._tpl.comparison_result();
            //$comparison_result_wrapper.html(html).addClass('show');

        });
    };

    klp.comparison = {
        init: init,
        open: open,
        close: close
    };

    function init_comparison_charts(context) {
        var chart_width = 300;
        var s1 = context.school1;
        var s2 = context.school2;
        console.log("charts init called with ", context);
        var chartOptions = {
            innerSize: '85%',
            width: chart_width,
            height: chart_width
        };

        $('#comparison_boygirlchart_1').boyGirlChart(s1, chartOptions);

        $('#comparison_boygirlchart_2').boyGirlChart(s2, chartOptions);

        if (s1.hasFinanceData) {
            $('#comparison_financechart_1').financeChart(s1.finance_percents, chartOptions);
        }

        if (s2.hasFinanceData) {
            $('#comparison_financechart_2').financeChart(s2.finance_percents, chartOptions);
        }

    }
})();<|endoftext|>


<commit_before>// Set mode
var mode = true; // true for manhattan and false for euclid
var param = window.location.search.substring(1);

if (param == "manhattan") {
  mode = true;
} else if (param == "euclid") {
  mode = false;
}

// Setup audio input
navigator.getUserMedia = (navigator.getUserMedia ||
                          navigator.webkitGetUserMedia ||
                          navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia);

// Setup audio context
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();

// Setup audio analyser
var analyser = audioCtx.createAnalyser();
analyser.fftSize = 32;
var bufferLength = analyser.frequencyBinCount;
var frequencyArray = new Uint8Array(bufferLength);

function setup() {
  createCanvas(windowWidth, windowHeight);
  background('#2980b9');
  smooth();
  frameRate(60);

  // Get audio input
  if (navigator.getUserMedia) {
     navigator.getUserMedia (
        // constraints
        {
           audio: true
        },

        // successCallback
        function(localMediaStream) {
          var microphone = audioCtx.createMediaStreamSource(localMediaStream);
          microphone.connect(analyser);
        },

        // errorCallback
        function(err) {
           console.log("The following error occured: " + err);
        }
     );
  } else {
     console.log("getUserMedia not supported");
     alert("Your browser doesn't support getUserMedia!");
  }
}

function draw() {
  // Get frequency data
  analyser.getByteFrequencyData(frequencyArray);

  // Get volume
  var volume = getVolume(frequencyArray);

  // Get Frequency bands
  var bands = getActiveFrequencyBands(frequencyArray);
  bands = 255 / bufferLength * bands;
  bands = 255 - int(bands);

  // Draw Voronoi diagram
  var numSites = volume;
  var sites = new Array(numSites);
  var colors = new Array(numSites);

  for (var i = 0; i < numSites; i++) {
    colors[i] = color(random(bands, 255), random(bands, 255), random(bands, 255));
    sites[i] = createVector(random(width), random(height));
  }

  loadPixels();
  var density = pixelDensity();

  if (colors.length !== 0) {
    for (var x = 0; x < windowWidth; x += 4) {
      for (var y = 0; y < windowHeight; y += 4) {
        var closestIndex = 0;
        var minDistance = windowWidth;

        for (var i = 0; i < numSites; i++) {
          var distance = mode ? abs(sites[i].x - x) + abs(sites[i].y - y) : dist(x, y, sites[i].x, sites[i].y);

          if (distance < minDistance) {
            closestIndex = i;
            minDistance = distance;
          }
        }

        for (var i = 0; i < density; i++) {
          for (var j = 0; j < density; j++) {
            var indx = 4 * ((y * density + j) * width * density + (x * density + i));
            pixels[indx] = red(colors[closestIndex]);
            pixels[indx + 1] = green(colors[closestIndex]);
            pixels[indx + 2] = blue(colors[closestIndex]);
            pixels[indx + 3] = alpha(colors[closestIndex]);
          }
        }
      }
    }
  }

  updatePixels();
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

function getVolume(array) {
  var vol = 0;
  var values = 0;
  var length = array.length;

  for (var i = 0; i < length; i++) {
    values += array[i];
  }
  vol = values / length;

  return int(vol);
}

function getActiveFrequencyBands(array) {
  var bands = 0;
  var threshold = 10;
  length = array.length;

  for (var i = 0; i < length; i++) {
    if (array[i] > threshold) {
      bands++;
    }
  }

  return bands;
}
<commit_msg>Pixel density fix<commit_after>// Set mode
var mode = true; // true for manhattan and false for euclid
var param = window.location.search.substring(1);

if (param == "manhattan") {
  mode = true;
} else if (param == "euclid") {
  mode = false;
}

// Setup audio input
navigator.getUserMedia = (navigator.getUserMedia ||
                          navigator.webkitGetUserMedia ||
                          navigator.mozGetUserMedia ||
                          navigator.msGetUserMedia);

// Setup audio context
var audioCtx = new (window.AudioContext || window.webkitAudioContext)();

// Setup audio analyser
var analyser = audioCtx.createAnalyser();
analyser.fftSize = 32;
var bufferLength = analyser.frequencyBinCount;
var frequencyArray = new Uint8Array(bufferLength);

function setup() {
  createCanvas(windowWidth, windowHeight);
  background('#2980b9');
  smooth();
  frameRate(60);

  // Get audio input
  if (navigator.getUserMedia) {
     navigator.getUserMedia (
        // constraints
        {
           audio: true
        },

        // successCallback
        function(localMediaStream) {
          var microphone = audioCtx.createMediaStreamSource(localMediaStream);
          microphone.connect(analyser);
        },

        // errorCallback
        function(err) {
           console.log("The following error occured: " + err);
        }
     );
  } else {
     console.log("getUserMedia not supported");
     alert("Your browser doesn't support getUserMedia!");
  }
}

function draw() {
  // Get frequency data
  analyser.getByteFrequencyData(frequencyArray);

  // Get volume
  var volume = getVolume(frequencyArray);

  // Get Frequency bands
  var bands = getActiveFrequencyBands(frequencyArray);
  bands = 255 / bufferLength * bands;
  bands = 255 - int(bands);

  // Draw Voronoi diagram
  var numSites = volume;
  var sites = new Array(numSites);
  var colors = new Array(numSites);

  for (var i = 0; i < numSites; i++) {
    colors[i] = color(random(bands, 255), random(bands, 255), random(bands, 255));
    sites[i] = createVector(random(width), random(height));
  }

  loadPixels();
  var density = pixelDensity();

  if (colors.length !== 0) {
    for (var x = 0; x < windowWidth; x++) {
      for (var y = 0; y < windowHeight; y++) {
        var closestIndex = 0;
        var minDistance = windowWidth;

        for (var i = 0; i < numSites; i++) {
          var distance = mode ? abs(sites[i].x - x) + abs(sites[i].y - y) : dist(x, y, sites[i].x, sites[i].y);

          if (distance < minDistance) {
            closestIndex = i;
            minDistance = distance;
          }
        }

        for (var i = 0; i < density; i++) {
          for (var j = 0; j < density; j++) {
            var indx = 4 * ((y * density + j) * width * density + (x * density + i));
            pixels[indx] = red(colors[closestIndex]);
            pixels[indx + 1] = green(colors[closestIndex]);
            pixels[indx + 2] = blue(colors[closestIndex]);
            pixels[indx + 3] = alpha(colors[closestIndex]);
          }
        }
      }
    }
  }

  updatePixels();
}

function windowResized() {
  resizeCanvas(windowWidth, windowHeight);
}

function getVolume(array) {
  var vol = 0;
  var values = 0;
  var length = array.length;

  for (var i = 0; i < length; i++) {
    values += array[i];
  }
  vol = values / length;

  return int(vol);
}

function getActiveFrequencyBands(array) {
  var bands = 0;
  var threshold = 10;
  length = array.length;

  for (var i = 0; i < length; i++) {
    if (array[i] > threshold) {
      bands++;
    }
  }

  return bands;
}
<|endoftext|>