<commit_before>'use strict';

var _ = require('underscore');
var Backbone = require('backbone');
var $ = require('jquery');

var Notification = require('./notification');
var atomic = require('../model/atomic');
var { Dropbox, Server } = require('../backend');
var ListPicker = require('./list_picker');

// Renders a single Landmark. Should update when constituent landmark
// updates and that's it.
var LandmarkView = Backbone.View.extend({

    tagName: "div",

    events: {
        click: "handleClick"
    },

    initialize: function ({labelIndex}) {
        this.listenTo(this.model, 'change', this.render);
        _.bindAll(this, 'select', 'selectGroup', 'handleClick', 'selectAll');
        this._clickedTimeout = null;
        this.labelIndex = labelIndex;
    },

    render: function () {
        var html = $("

");
        html.addClass("Lm", this.model.isEmpty());

        html.toggleClass("Lm-Empty", this.model.isEmpty());
        html.toggleClass("Lm-Value", !this.model.isEmpty());
        html.toggleClass("Lm-Selected", this.model.isSelected());
        html.toggleClass("Lm-NextAvailable", this.model.isNextAvailable());

        // in case our element is already live replace the content
        this.$el.replaceWith(html);
        // now set the element back so we have a handle. If this is the
        // first time then we have gotten a handle for the future
        this.setElement(html);
        return this;
    },

    handleClick: function (event) {
        if (this._clickedTimeout === null) {
            this._clickedTimeout = setTimeout(() => {
                this._clickedTimeout = null;
                this.select(event);
            }, 200);
        } else {
            clearTimeout(this._clickedTimeout);
            this._clickedTimeout = null;
            this.selectGroup(event);
        }
    },

    select: atomic.atomicOperation(function (event) {
        if (event.shiftKey) {
            this.selectAll(event);
        } else if (this.model.isSelected()) {
            this.model.deselect();
        } else if (event.ctrlKey || event.metaKey) {
            if (!this.model.isSelected()) {
                this.model.select();
                $('#viewportContainer').trigger("groupSelected");
            }
        } else if (this.model.isEmpty()) {
            // user is clicking on an empty landmark - mark it as the next for
            // insertion
            this.model.setNextAvailable();
        } else {
            this.model.selectAndDeselectRest();
        }
    }),

    selectGroup: function () {
        this.model.group().deselectAll();
        this.model.group().labels[this.labelIndex].selectAll();
        $('#viewportContainer').trigger("groupSelected");
    },

    selectAll: function () {
        this.model.group().selectAll();
        $('#viewportContainer').trigger("groupSelected");
    }
});

// Renders the LandmarkList. Don't think this ListView should ever have to
// render (as we build a fresh View each time a group is activated
// and de-activated)
var LandmarkListView = Backbone.View.extend({

    tagName: 'div',

    initialize: function ({labelIndex}) {
        _.bindAll(this, 'render', 'renderOne');
        this.lmViews = [];
        this.labelIndex = labelIndex;
        this.render();
    },

    render: function() {
        this.cleanup();
        this.$el.empty();
        this.$el.addClass('LmGroup-Flex');
        this.collection.map(this.renderOne);
        return this;
    },

    renderOne: function(model) {
        var lm = new LandmarkView({model: model, labelIndex: this.labelIndex});
        // reset the view's element to it's template
        this.$el.append(lm.render().$el);
        this.lmViews.push(lm);
        return this;
    },

    cleanup: function () {
        this.lmViews.forEach(function (lm) { lm.remove(); });
        this.lmViews = [];
    }

});

var LandmarkGroupLabelView = Backbone.View.extend({

    className: "LmGroup-Label",

    initialize: function () {
        var label = this.model.label;
        this.$el.html(label);
    }
});

// Renders a single LandmarkGroup. Either the view is closed and we just
// render the header (LandmarkGroupButtonView) or this group is active and
// we render all the landmarks (LandmarkListView) as well as the header.
var LandmarkGroupView = Backbone.View.extend({

    tagName: 'div',

    initialize: function({labelIndex}) {
        _.bindAll(this, 'render');
        this.landmarkList = null;
        this.label = null;
        this.labelIndex = labelIndex;
        this.render();
    },

    render: function () {
        this.cleanup();
        this.$el.empty();
        this.$el.addClass('LmGroup');
        this.landmarkList = new LandmarkListView(
            {collection: this.model.landmarks, labelIndex: this.labelIndex});
        this.label = new LandmarkGroupLabelView({model: this.model});
        this.$el.append(this.label.render().$el);
        this.$el.append(this.landmarkList.render().$el);
        return this;
    },

    cleanup: function () {
        if (this.landmarkList) {
            // already have a list view! clean it up + remove
            this.landmarkList.cleanup();
            this.landmarkList.remove();
            this.landmarkList = null;
        }
        if (this.label) {
            // already have a label view! remove
            this.label.remove();
            this.label = null;
        }
    }
});

// Renders a collection of LandmarkGroups. At any one time one of these
// will be expanded - the rest closed. This View is indifferent - it just
// builds LandmarkGroupView's and asks them to render in turn.
var LandmarkGroupListView = Backbone.View.extend({

    initialize: function () {
        _.bindAll(this, 'render', 'renderOne');
        this.groups = [];
        this.render();
    },

    render: function() {
        this.cleanup();
        this.$el.empty();
        this.collection.map(this.renderOne);
        return this;
    },

    renderOne: function(label, labelIndex) {
        var group = new LandmarkGroupView({model: label, labelIndex});
        // reset the view's element to it's template
        this.$el.append(group.render().$el);
        this.groups.push(group);
        return this;
    },

    cleanup: function () {
        this.groups.forEach(function (group) {
            group.cleanup();
            group.remove();
        });
        this.groups = [];
    }

});

var ActionsView = Backbone.View.extend({

    el: '#lmActionsPanel',

    initialize: function({app}) {
        _.bindAll(this, 'save', 'help', 'render');
        this.listenTo(this.model.tracker, "change", this.render);
        this.app = app;
        this.render();
    },

    events: {
        'click #save': "save",
        'click #help': "help",
        'click #download': "download"
    },

    render: function () {
        this.$el.find('#save')
                .toggleClass('Active', !this.model.tracker.isUpToDate());
    },

    save: function (evt) {
        evt.stopPropagation();
        this.$el.find('#save').addClass('Button--Disabled');
        this.model.save().then(() => {
            Notification.notify({type: 'success', msg: 'Save Completed'});
            this.$el.find('#save').removeClass('Button--Disabled');
        }, () => {
            Notification.notify({type: 'error', msg: 'Save Failed'});
            this.$el.find('#save').removeClass('Button--Disabled');
        });
    },

    help: function (e) {
        e.stopPropagation();  // prevent the event from trigging the help immediately
        this.app.toggleHelpOverlay();
    },

    download: function (evt) {
        evt.stopPropagation();
        if (this.model) {
            const spinner = Notification.loading.start();
            this.$el.find('#download').addClass('Button--Disabled');
            const data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(this.model.toJSON())),
                filename = `${this.app.asset().id}_${this.app.activeTemplate()}.ljson`;

            // Remove previous element from dom
            const previous = document.getElementById('downloadLMLink');
            if (previous) {
                previous.remove();
            }

            const link = document.createElement('a');
            link.setAttribute('style', 'display:none;');
            link.setAttribute('download', filename);
            link.setAttribute('href', `data:${data}`);
            link.setAttribute('id', `downloadLMLink`);
            link.setAttribute('hidden', `true`);

            // target="_blank" for Safari who still does not understand
            // the download attribute
            link.setAttribute('target', '_blank');

            // Add to DOM and click
            document.body.appendChild(link);
            document.getElementById('downloadLMLink').click();

            Notification.loading.stop(spinner);
            this.$el.find('#download').removeClass('Button--Disabled');

        }
    }
});

var UndoRedoView = Backbone.View.extend({

    el: "#undoRedo",

    events: {
        'click .Undo': 'undo',
        'click .Redo': 'redo'
    },

    initialize: function ({app}) {
        this.tracker = this.model.tracker;
        this.app = app;
        this.listenTo(this.tracker, "change", this.render);
        _.bindAll(this, 'render', 'cleanup', 'undo', 'redo');
        this.render();
    },

    cleanup: function () {
        this.stopListening(this.tracker);
        this.$el.find('.Undo').addClass('Disabled');
        this.$el.find('.Redo').addClass('Disabled');
    },

    render: function () {
        this.$el.find('.Undo').toggleClass('Disabled', !this.tracker.canUndo());
        this.$el.find('.Redo').toggleClass('Disabled', !this.tracker.canRedo());
    },

    undo: function () {
        if (!this.tracker.canUndo()) {
            return;
        } else {
            this.model.undo();
        }
    },

    redo: function () {
        if (!this.tracker.canRedo()) {
            return;
        } else {
            this.model.redo();
        }
    }
});

var TemplatePanel = Backbone.View.extend({
    el: '#templatePanel',

    events: {
        'click': 'click'
    },

    initialize: function () {
        _.bindAll(this, "update");
        this.listenTo(this.model, "change:activeTemplate", this.update);
    },

    update: function () {
        this.$el.toggleClass(
            'Disabled', this.model &&
                        this.model.templates().length <= 1 &&
                        this.model.server() instanceof Server
        );
        this.$el.text(this.model.activeTemplate() || '-');
    },

    click: function () {
        const backend = this.model.server();
        if (backend instanceof Dropbox) {
            backend.pickTemplate(() => {
                this.model._initTemplates(true);
            }, function (err) {
                Notification.notify({
                    type: 'error',
                    msg: 'Error switching template ' + err
                });
            }, true);
        } else if (backend instanceof Server) {

            const tmpls = this.model.templates();

            if (tmpls.length <= 1) {
                return;
            }

            const picker = new ListPicker({
                list: tmpls.map(t => [t, t]),
                title: 'Select a template',
                closable: true,
                disposeOnClose: true,
                useFilter: tmpls.length > 5,
                submit: tmpl => this.model.set('activeTemplate', tmpl)
            });
            picker.open();
        }
    }
});

var LmLoadView = Backbone.View.extend({
    el: '#lmLoadPanel',

    events: {
        'click #loadPrevious': 'loadPrevious'
    },

    initialize: function ({app}) {
        _.bindAll(this, 'render', 'loadPrevious');
        this.app = app;
        this.render();
    },

    render: function () {
        const show = this.app.assetSource().hasPredecessor();
        this.$el.toggleClass('Hide', !show);
        this.$el.find('button').toggleClass('Button-Danger',
                                            !this.model.isEmpty());
    },

    loadPrevious: function () {
        this.app.reloadLandmarksFromPrevious();
        this.render();
    }
});

var Sidebar = Backbone.View.extend({

    initialize: function () {
        _.bindAll(this, "landmarksChange");
        this.listenTo(this.model, "change:landmarks", this.landmarksChange);
        this.actionsView = null;
        this.lmLoadView = null;
        this.lmView = null;
        this.undoRedoView = null;
        this.templatePanel = new TemplatePanel({model: this.model});
    },

    landmarksChange: function () {
        console.log('Sidebar - rewiring after landmark change');
        if (this.actionsView) {
            this.actionsView.undelegateEvents();
        }

        if (this.lmLoadView) {
            this.lmLoadView.undelegateEvents();
        }

        if (this.undoRedoView) {
            this.undoRedoView.undelegateEvents();
        }

        if (this.lmView) {
            this.lmView.cleanup();
        }

        var lms = this.model.landmarks();

        if (lms === null) {
            return;
        }

        this.actionsView = new ActionsView({model: lms, app: this.model});
        this.lmLoadView = new LmLoadView({model: lms, app: this.model});
        this.undoRedoView = new UndoRedoView({model: lms});
        this.lmView = new LandmarkGroupListView({collection: lms.labels});
        $('#landmarksPanel').html(this.lmView.render().$el);
    }

});

module.exports.Sidebar = Sidebar;
<commit_msg>Make TemplatePanel more generic > use pickTemplate as a detector<commit_after>'use strict';

var _ = require('underscore');
var Backbone = require('backbone');
var $ = require('jquery');

var Notification = require('./notification');
var atomic = require('../model/atomic');
var { Server } = require('../backend');
var ListPicker = require('./list_picker');

// Renders a single Landmark. Should update when constituent landmark
// updates and that's it.
var LandmarkView = Backbone.View.extend({

    tagName: "div",

    events: {
        click: "handleClick"
    },

    initialize: function ({labelIndex}) {
        this.listenTo(this.model, 'change', this.render);
        _.bindAll(this, 'select', 'selectGroup', 'handleClick', 'selectAll');
        this._clickedTimeout = null;
        this.labelIndex = labelIndex;
    },

    render: function () {
        var html = $("

");
        html.addClass("Lm", this.model.isEmpty());

        html.toggleClass("Lm-Empty", this.model.isEmpty());
        html.toggleClass("Lm-Value", !this.model.isEmpty());
        html.toggleClass("Lm-Selected", this.model.isSelected());
        html.toggleClass("Lm-NextAvailable", this.model.isNextAvailable());

        // in case our element is already live replace the content
        this.$el.replaceWith(html);
        // now set the element back so we have a handle. If this is the
        // first time then we have gotten a handle for the future
        this.setElement(html);
        return this;
    },

    handleClick: function (event) {
        if (this._clickedTimeout === null) {
            this._clickedTimeout = setTimeout(() => {
                this._clickedTimeout = null;
                this.select(event);
            }, 200);
        } else {
            clearTimeout(this._clickedTimeout);
            this._clickedTimeout = null;
            this.selectGroup(event);
        }
    },

    select: atomic.atomicOperation(function (event) {
        if (event.shiftKey) {
            this.selectAll(event);
        } else if (this.model.isSelected()) {
            this.model.deselect();
        } else if (event.ctrlKey || event.metaKey) {
            if (!this.model.isSelected()) {
                this.model.select();
                $('#viewportContainer').trigger("groupSelected");
            }
        } else if (this.model.isEmpty()) {
            // user is clicking on an empty landmark - mark it as the next for
            // insertion
            this.model.setNextAvailable();
        } else {
            this.model.selectAndDeselectRest();
        }
    }),

    selectGroup: function () {
        this.model.group().deselectAll();
        this.model.group().labels[this.labelIndex].selectAll();
        $('#viewportContainer').trigger("groupSelected");
    },

    selectAll: function () {
        this.model.group().selectAll();
        $('#viewportContainer').trigger("groupSelected");
    }
});

// Renders the LandmarkList. Don't think this ListView should ever have to
// render (as we build a fresh View each time a group is activated
// and de-activated)
var LandmarkListView = Backbone.View.extend({

    tagName: 'div',

    initialize: function ({labelIndex}) {
        _.bindAll(this, 'render', 'renderOne');
        this.lmViews = [];
        this.labelIndex = labelIndex;
        this.render();
    },

    render: function() {
        this.cleanup();
        this.$el.empty();
        this.$el.addClass('LmGroup-Flex');
        this.collection.map(this.renderOne);
        return this;
    },

    renderOne: function(model) {
        var lm = new LandmarkView({model: model, labelIndex: this.labelIndex});
        // reset the view's element to it's template
        this.$el.append(lm.render().$el);
        this.lmViews.push(lm);
        return this;
    },

    cleanup: function () {
        this.lmViews.forEach(function (lm) { lm.remove(); });
        this.lmViews = [];
    }

});

var LandmarkGroupLabelView = Backbone.View.extend({

    className: "LmGroup-Label",

    initialize: function () {
        var label = this.model.label;
        this.$el.html(label);
    }
});

// Renders a single LandmarkGroup. Either the view is closed and we just
// render the header (LandmarkGroupButtonView) or this group is active and
// we render all the landmarks (LandmarkListView) as well as the header.
var LandmarkGroupView = Backbone.View.extend({

    tagName: 'div',

    initialize: function({labelIndex}) {
        _.bindAll(this, 'render');
        this.landmarkList = null;
        this.label = null;
        this.labelIndex = labelIndex;
        this.render();
    },

    render: function () {
        this.cleanup();
        this.$el.empty();
        this.$el.addClass('LmGroup');
        this.landmarkList = new LandmarkListView(
            {collection: this.model.landmarks, labelIndex: this.labelIndex});
        this.label = new LandmarkGroupLabelView({model: this.model});
        this.$el.append(this.label.render().$el);
        this.$el.append(this.landmarkList.render().$el);
        return this;
    },

    cleanup: function () {
        if (this.landmarkList) {
            // already have a list view! clean it up + remove
            this.landmarkList.cleanup();
            this.landmarkList.remove();
            this.landmarkList = null;
        }
        if (this.label) {
            // already have a label view! remove
            this.label.remove();
            this.label = null;
        }
    }
});

// Renders a collection of LandmarkGroups. At any one time one of these
// will be expanded - the rest closed. This View is indifferent - it just
// builds LandmarkGroupView's and asks them to render in turn.
var LandmarkGroupListView = Backbone.View.extend({

    initialize: function () {
        _.bindAll(this, 'render', 'renderOne');
        this.groups = [];
        this.render();
    },

    render: function() {
        this.cleanup();
        this.$el.empty();
        this.collection.map(this.renderOne);
        return this;
    },

    renderOne: function(label, labelIndex) {
        var group = new LandmarkGroupView({model: label, labelIndex});
        // reset the view's element to it's template
        this.$el.append(group.render().$el);
        this.groups.push(group);
        return this;
    },

    cleanup: function () {
        this.groups.forEach(function (group) {
            group.cleanup();
            group.remove();
        });
        this.groups = [];
    }

});

var ActionsView = Backbone.View.extend({

    el: '#lmActionsPanel',

    initialize: function({app}) {
        _.bindAll(this, 'save', 'help', 'render');
        this.listenTo(this.model.tracker, "change", this.render);
        this.app = app;
        this.render();
    },

    events: {
        'click #save': "save",
        'click #help': "help",
        'click #download': "download"
    },

    render: function () {
        this.$el.find('#save')
                .toggleClass('Active', !this.model.tracker.isUpToDate());
    },

    save: function (evt) {
        evt.stopPropagation();
        this.$el.find('#save').addClass('Button--Disabled');
        this.model.save().then(() => {
            Notification.notify({type: 'success', msg: 'Save Completed'});
            this.$el.find('#save').removeClass('Button--Disabled');
        }, () => {
            Notification.notify({type: 'error', msg: 'Save Failed'});
            this.$el.find('#save').removeClass('Button--Disabled');
        });
    },

    help: function (e) {
        e.stopPropagation();  // prevent the event from trigging the help immediately
        this.app.toggleHelpOverlay();
    },

    download: function (evt) {
        evt.stopPropagation();
        if (this.model) {
            const spinner = Notification.loading.start();
            this.$el.find('#download').addClass('Button--Disabled');
            const data = "text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(this.model.toJSON())),
                filename = `${this.app.asset().id}_${this.app.activeTemplate()}.ljson`;

            // Remove previous element from dom
            const previous = document.getElementById('downloadLMLink');
            if (previous) {
                previous.remove();
            }

            const link = document.createElement('a');
            link.setAttribute('style', 'display:none;');
            link.setAttribute('download', filename);
            link.setAttribute('href', `data:${data}`);
            link.setAttribute('id', `downloadLMLink`);
            link.setAttribute('hidden', `true`);

            // target="_blank" for Safari who still does not understand
            // the download attribute
            link.setAttribute('target', '_blank');

            // Add to DOM and click
            document.body.appendChild(link);
            document.getElementById('downloadLMLink').click();

            Notification.loading.stop(spinner);
            this.$el.find('#download').removeClass('Button--Disabled');

        }
    }
});

var UndoRedoView = Backbone.View.extend({

    el: "#undoRedo",

    events: {
        'click .Undo': 'undo',
        'click .Redo': 'redo'
    },

    initialize: function ({app}) {
        this.tracker = this.model.tracker;
        this.app = app;
        this.listenTo(this.tracker, "change", this.render);
        _.bindAll(this, 'render', 'cleanup', 'undo', 'redo');
        this.render();
    },

    cleanup: function () {
        this.stopListening(this.tracker);
        this.$el.find('.Undo').addClass('Disabled');
        this.$el.find('.Redo').addClass('Disabled');
    },

    render: function () {
        this.$el.find('.Undo').toggleClass('Disabled', !this.tracker.canUndo());
        this.$el.find('.Redo').toggleClass('Disabled', !this.tracker.canRedo());
    },

    undo: function () {
        if (!this.tracker.canUndo()) {
            return;
        } else {
            this.model.undo();
        }
    },

    redo: function () {
        if (!this.tracker.canRedo()) {
            return;
        } else {
            this.model.redo();
        }
    }
});

var TemplatePanel = Backbone.View.extend({
    el: '#templatePanel',

    events: {
        'click': 'click'
    },

    initialize: function () {
        _.bindAll(this, "update");
        this.listenTo(this.model, "change:activeTemplate", this.update);
    },

    update: function () {
        this.$el.toggleClass(
            'Disabled', this.model &&
                        ((this.model.templates().length <= 1 &&
                        this.model.server() instanceof Server) ||
                        typeof this.model.server().pickTemplate !== 'function')
        );
        this.$el.text(this.model.activeTemplate() || '-');
    },

    click: function () {
        const backend = this.model.server();

        if (backend instanceof Server) {

            const tmpls = this.model.templates();

            if (tmpls.length <= 1) {
                return;
            }

            const picker = new ListPicker({
                list: tmpls.map(t => [t, t]),
                title: 'Select a template',
                closable: true,
                disposeOnClose: true,
                useFilter: tmpls.length > 5,
                submit: tmpl => this.model.set('activeTemplate', tmpl)
            });
            picker.open();
        } else if (typeof this.model.server().pickTemplate === 'function') {
            backend.pickTemplate(() => {
                this.model._initTemplates(true);
            }, function (err) {
                Notification.notify({
                    type: 'error',
                    msg: 'Error switching template ' + err
                });
            }, true);
        }
    }
});

var LmLoadView = Backbone.View.extend({
    el: '#lmLoadPanel',

    events: {
        'click #loadPrevious': 'loadPrevious'
    },

    initialize: function ({app}) {
        _.bindAll(this, 'render', 'loadPrevious');
        this.app = app;
        this.render();
    },

    render: function () {
        const show = this.app.assetSource().hasPredecessor();
        this.$el.toggleClass('Hide', !show);
        this.$el.find('button').toggleClass('Button-Danger',
                                            !this.model.isEmpty());
    },

    loadPrevious: function () {
        this.app.reloadLandmarksFromPrevious();
        this.render();
    }
});

var Sidebar = Backbone.View.extend({

    initialize: function () {
        _.bindAll(this, "landmarksChange");
        this.listenTo(this.model, "change:landmarks", this.landmarksChange);
        this.actionsView = null;
        this.lmLoadView = null;
        this.lmView = null;
        this.undoRedoView = null;
        this.templatePanel = new TemplatePanel({model: this.model});
    },

    landmarksChange: function () {
        console.log('Sidebar - rewiring after landmark change');
        if (this.actionsView) {
            this.actionsView.undelegateEvents();
        }

        if (this.lmLoadView) {
            this.lmLoadView.undelegateEvents();
        }

        if (this.undoRedoView) {
            this.undoRedoView.undelegateEvents();
        }

        if (this.lmView) {
            this.lmView.cleanup();
        }

        var lms = this.model.landmarks();

        if (lms === null) {
            return;
        }

        this.actionsView = new ActionsView({model: lms, app: this.model});
        this.lmLoadView = new LmLoadView({model: lms, app: this.model});
        this.undoRedoView = new UndoRedoView({model: lms});
        this.lmView = new LandmarkGroupListView({collection: lms.labels});
        $('#landmarksPanel').html(this.lmView.render().$el);
    }

});

module.exports.Sidebar = Sidebar;
<|endoftext|>


<commit_before>import L from 'leaflet';
import featureLayerService from '../../Services/FeatureLayerService';
import { cleanUrl, warn } from '../../Util';
import VirtualGrid from 'leaflet-virtual-grid';

export var FeatureManager = VirtualGrid.extend({
  /**
   * Options
   */

  options: {
    attribution: null,
    where: '1=1',
    fields: ['*'],
    from: false,
    to: false,
    timeField: false,
    timeFilterMode: 'server',
    simplifyFactor: 0,
    precision: 6
  },

  /**
   * Constructor
   */

  initialize: function (options) {
    VirtualGrid.prototype.initialize.call(this, options);

    options.url = cleanUrl(options.url);
    options = L.setOptions(this, options);

    this.service = featureLayerService(options);
    this.service.addEventParent(this);

    // use case insensitive regex to look for common fieldnames used for indexing
    if (this.options.fields[0] !== '*') {
      var oidCheck = false;
      for (var i = 0; i < this.options.fields.length; i++) {
        if (this.options.fields[i].match(/^(OBJECTID|FID|OID|ID)$/i)) {
          oidCheck = true;
        }
      }
      if (oidCheck === false) {
        warn('no known esriFieldTypeOID field detected in fields Array.  Please add an attribute field containing unique IDs to ensure the layer can be drawn correctly.');
      }
    }

    if (this.options.timeField.start && this.options.timeField.end) {
      this._startTimeIndex = new BinarySearchIndex();
      this._endTimeIndex = new BinarySearchIndex();
    } else if (this.options.timeField) {
      this._timeIndex = new BinarySearchIndex();
    }

    this._cache = {};
    this._currentSnapshot = []; // cache of what layers should be active
    this._activeRequests = 0;
  },

  /**
   * Layer Interface
   */

  onAdd: function (map) {
    return VirtualGrid.prototype.onAdd.call(this, map);
  },

  onRemove: function (map) {
    return VirtualGrid.prototype.onRemove.call(this, map);
  },

  getAttribution: function () {
    return this.options.attribution;
  },

  /**
   * Feature Managment
   */

  createCell: function (bounds, coords) {
    this._requestFeatures(bounds, coords);
  },

  _requestFeatures: function (bounds, coords, callback) {
    this._activeRequests++;

    // our first active request fires loading
    if (this._activeRequests === 1) {
      this.fire('loading', {
        bounds: bounds
      }, true);
    }

    return this._buildQuery(bounds).run(function (error, featureCollection, response) {
      if (response && response.exceededTransferLimit) {
        this.fire('drawlimitexceeded');
      }

      // no error, features
      if (!error && featureCollection && featureCollection.features.length) {
        // schedule adding features until the next animation frame
        L.Util.requestAnimFrame(L.Util.bind(function () {
          this._addFeatures(featureCollection.features, coords);
          this._postProcessFeatures(bounds);
        }, this));
      }

      // no error, no features
      if (!error && featureCollection && !featureCollection.features.length) {
        this._postProcessFeatures(bounds);
      }

      if (callback) {
        callback.call(this, error, featureCollection);
      }
    }, this);
  },

  _postProcessFeatures: function (bounds) {
    // deincriment the request counter now that we have processed features
    this._activeRequests--;

    // if there are no more active requests fire a load event for this view
    if (this._activeRequests <= 0) {
      this.fire('load', {
        bounds: bounds
      });
    }
  },

  _cacheKey: function (coords) {
    return coords.z + ':' + coords.x + ':' + coords.y;
  },

  _addFeatures: function (features, coords) {
    var key = this._cacheKey(coords);
    this._cache[key] = this._cache[key] || [];

    for (var i = features.length - 1; i >= 0; i--) {
      var id = features[i].id;
      this._currentSnapshot.push(id);
      this._cache[key].push(id);
    }

    if (this.options.timeField) {
      this._buildTimeIndexes(features);
    }

    var zoom = this._map.getZoom();

    if (zoom > this.options.maxZoom ||
        zoom < this.options.minZoom) { return; }

    this.createLayers(features);
  },

  _buildQuery: function (bounds) {
    var query = this.service.query()
      .intersects(bounds)
      .where(this.options.where)
      .fields(this.options.fields)
      .precision(this.options.precision);

    if (this.options.simplifyFactor) {
      query.simplify(this._map, this.options.simplifyFactor);
    }

    if (this.options.timeFilterMode === 'server' && this.options.from && this.options.to) {
      query.between(this.options.from, this.options.to);
    }

    return query;
  },

  /**
   * Where Methods
   */

  setWhere: function (where, callback, context) {
    this.options.where = (where && where.length) ? where : '1=1';

    var oldSnapshot = [];
    var newSnapshot = [];
    var pendingRequests = 0;
    var requestError = null;
    var requestCallback = L.Util.bind(function (error, featureCollection) {
      if (error) {
        requestError = error;
      }

      if (featureCollection) {
        for (var i = featureCollection.features.length - 1; i >= 0; i--) {
          newSnapshot.push(featureCollection.features[i].id);
        }
      }

      pendingRequests--;

      if (pendingRequests <= 0) {
        this._currentSnapshot = newSnapshot;
        // schedule adding features until the next animation frame
        L.Util.requestAnimFrame(L.Util.bind(function () {
          this.removeLayers(oldSnapshot);
          this.addLayers(newSnapshot);
          if (callback) {
            callback.call(context, requestError);
          }
        }, this));
      }
    }, this);

    for (var i = this._currentSnapshot.length - 1; i >= 0; i--) {
      oldSnapshot.push(this._currentSnapshot[i]);
    }

    for (var key in this._activeCells) {
      pendingRequests++;
      var coords = this._keyToCellCoords(key);
      var bounds = this._cellCoordsToBounds(coords);
      this._requestFeatures(bounds, key, requestCallback);
    }

    return this;
  },

  getWhere: function () {
    return this.options.where;
  },

  /**
   * Time Range Methods
   */

  getTimeRange: function () {
    return [this.options.from, this.options.to];
  },

  setTimeRange: function (from, to, callback, context) {
    var oldFrom = this.options.from;
    var oldTo = this.options.to;
    var pendingRequests = 0;
    var requestError = null;
    var requestCallback = L.Util.bind(function (error) {
      if (error) {
        requestError = error;
      }
      this._filterExistingFeatures(oldFrom, oldTo, from, to);

      pendingRequests--;

      if (callback && pendingRequests <= 0) {
        callback.call(context, requestError);
      }
    }, this);

    this.options.from = from;
    this.options.to = to;

    this._filterExistingFeatures(oldFrom, oldTo, from, to);

    if (this.options.timeFilterMode === 'server') {
      for (var key in this._activeCells) {
        pendingRequests++;
        var coords = this._keyToCellCoords(key);
        var bounds = this._cellCoordsToBounds(coords);
        this._requestFeatures(bounds, key, requestCallback);
      }
    }

    return this;
  },

  refresh: function () {
    for (var key in this._activeCells) {
      var coords = this._keyToCellCoords(key);
      var bounds = this._cellCoordsToBounds(coords);
      this._requestFeatures(bounds, key);
    }

    if (this.redraw) {
      this.once('load', function () {
        this.eachFeature(function (layer) {
          this._redraw(layer.feature.id);
        }, this);
      }, this);
    }
  },

  _filterExistingFeatures: function (oldFrom, oldTo, newFrom, newTo) {
    var layersToRemove = (oldFrom && oldTo) ? this._getFeaturesInTimeRange(oldFrom, oldTo) : this._currentSnapshot;
    var layersToAdd = this._getFeaturesInTimeRange(newFrom, newTo);

    if (layersToAdd.indexOf) {
      for (var i = 0; i < layersToAdd.length; i++) {
        var shouldRemoveLayer = layersToRemove.indexOf(layersToAdd[i]);
        if (shouldRemoveLayer >= 0) {
          layersToRemove.splice(shouldRemoveLayer, 1);
        }
      }
    }

    // schedule adding features until the next animation frame
    L.Util.requestAnimFrame(L.Util.bind(function () {
      this.removeLayers(layersToRemove);
      this.addLayers(layersToAdd);
    }, this));
  },

  _getFeaturesInTimeRange: function (start, end) {
    var ids = [];
    var search;

    if (this.options.timeField.start && this.options.timeField.end) {
      var startTimes = this._startTimeIndex.between(start, end);
      var endTimes = this._endTimeIndex.between(start, end);
      search = startTimes.concat(endTimes);
    } else {
      search = this._timeIndex.between(start, end);
    }

    for (var i = search.length - 1; i >= 0; i--) {
      ids.push(search[i].id);
    }

    return ids;
  },

  _buildTimeIndexes: function (geojson) {
    var i;
    var feature;
    if (this.options.timeField.start && this.options.timeField.end) {
      var startTimeEntries = [];
      var endTimeEntries = [];
      for (i = geojson.length - 1; i >= 0; i--) {
        feature = geojson[i];
        startTimeEntries.push({
          id: feature.id,
          value: new Date(feature.properties[this.options.timeField.start])
        });
        endTimeEntries.push({
          id: feature.id,
          value: new Date(feature.properties[this.options.timeField.end])
        });
      }
      this._startTimeIndex.bulkAdd(startTimeEntries);
      this._endTimeIndex.bulkAdd(endTimeEntries);
    } else {
      var timeEntries = [];
      for (i = geojson.length - 1; i >= 0; i--) {
        feature = geojson[i];
        timeEntries.push({
          id: feature.id,
          value: new Date(feature.properties[this.options.timeField])
        });
      }

      this._timeIndex.bulkAdd(timeEntries);
    }
  },

  _featureWithinTimeRange: function (feature) {
    if (!this.options.from || !this.options.to) {
      return true;
    }

    var from = +this.options.from.valueOf();
    var to = +this.options.to.valueOf();

    if (typeof this.options.timeField === 'string') {
      var date = +feature.properties[this.options.timeField];
      return (date >= from) && (date <= to);
    }

    if (this.options.timeField.start && this.options.timeField.end) {
      var startDate = +feature.properties[this.options.timeField.start];
      var endDate = +feature.properties[this.options.timeField.end];
      return ((startDate >= from) && (startDate <= to)) || ((endDate >= from) && (endDate <= to));
    }
  },

  /**
   * Service Methods
   */

  authenticate: function (token) {
    this.service.authenticate(token);
    return this;
  },

  metadata: function (callback, context) {
    this.service.metadata(callback, context);
    return this;
  },

  query: function () {
    return this.service.query();
  },

  _getMetadata: function (callback) {
    if (this._metadata) {
      var error;
      callback(error, this._metadata);
    } else {
      this.metadata(L.Util.bind(function (error, response) {
        this._metadata = response;
        callback(error, this._metadata);
      }, this));
    }
  },

  addFeature: function (feature, callback, context) {
    this._getMetadata(L.Util.bind(function (error, metadata) {
      if (error) {
        if (callback) { callback.call(this, error, null); }
        return;
      }

      this.service.addFeature(feature, L.Util.bind(function (error, response) {
        if (!error) {
          // assign ID from result to appropriate objectid field from service metadata
          feature.properties[metadata.objectIdField] = response.objectId;

          // we also need to update the geojson id for createLayers() to function
          feature.id = response.objectId;
          this.createLayers([feature]);
        }

        if (callback) {
          callback.call(context, error, response);
        }
      }, this));
    }, this));
  },

  updateFeature: function (feature, callback, context) {
    this.service.updateFeature(feature, function (error, response) {
      if (!error) {
        this.removeLayers([feature.id], true);
        this.createLayers([feature]);
      }

      if (callback) {
        callback.call(context, error, response);
      }
    }, this);
  },

  deleteFeature: function (id, callback, context) {
    this.service.deleteFeature(id, function (error, response) {
      if (!error && response.objectId) {
        this.removeLayers([response.objectId], true);
      }
      if (callback) {
        callback.call(context, error, response);
      }
    }, this);
  },

  deleteFeatures: function (ids, callback, context) {
    return this.service.deleteFeatures(ids, function (error, response) {
      if (!error && response.length > 0) {
        for (var i = 0; i < response.length; i++) {
          this.removeLayers([response[i].objectId], true);
        }
      }
      if (callback) {
        callback.call(context, error, response);
      }
    }, this);
  }
});

/**
 * Temporal Binary Search Index
 */

function BinarySearchIndex (values) {
  this.values = values || [];
}

BinarySearchIndex.prototype._query = function (query) {
  var minIndex = 0;
  var maxIndex = this.values.length - 1;
  var currentIndex;
  var currentElement;

  while (minIndex <= maxIndex) {
    currentIndex = (minIndex + maxIndex) / 2 | 0;
    currentElement = this.values[Math.round(currentIndex)];
    if (+currentElement.value < +query) {
      minIndex = currentIndex + 1;
    } else if (+currentElement.value > +query) {
      maxIndex = currentIndex - 1;
    } else {
      return currentIndex;
    }
  }

  return ~maxIndex;
};

BinarySearchIndex.prototype.sort = function () {
  this.values.sort(function (a, b) {
    return +b.value - +a.value;
  }).reverse();
  this.dirty = false;
};

BinarySearchIndex.prototype.between = function (start, end) {
  if (this.dirty) {
    this.sort();
  }

  var startIndex = this._query(start);
  var endIndex = this._query(end);

  if (startIndex === 0 && endIndex === 0) {
    return [];
  }

  startIndex = Math.abs(startIndex);
  endIndex = (endIndex < 0) ? Math.abs(endIndex) : endIndex + 1;

  return this.values.slice(startIndex, endIndex);
};

BinarySearchIndex.prototype.bulkAdd = function (items) {
  this.dirty = true;
  this.values = this.values.concat(items);
};
<commit_msg>scaffolding for new zoom logic<commit_after>import L from 'leaflet';
import featureLayerService from '../../Services/FeatureLayerService';
import { cleanUrl, warn } from '../../Util';
import VirtualGrid from 'leaflet-virtual-grid';

export var FeatureManager = VirtualGrid.extend({
  /**
   * Options
   */

  options: {
    attribution: null,
    where: '1=1',
    fields: ['*'],
    from: false,
    to: false,
    timeField: false,
    timeFilterMode: 'server',
    simplifyFactor: 0,
    precision: 6
  },

  /**
   * Constructor
   */

  initialize: function (options) {
    VirtualGrid.prototype.initialize.call(this, options);

    options.url = cleanUrl(options.url);
    options = L.setOptions(this, options);

    this.service = featureLayerService(options);
    this.service.addEventParent(this);

    // use case insensitive regex to look for common fieldnames used for indexing
    if (this.options.fields[0] !== '*') {
      var oidCheck = false;
      for (var i = 0; i < this.options.fields.length; i++) {
        if (this.options.fields[i].match(/^(OBJECTID|FID|OID|ID)$/i)) {
          oidCheck = true;
        }
      }
      if (oidCheck === false) {
        warn('no known esriFieldTypeOID field detected in fields Array.  Please add an attribute field containing unique IDs to ensure the layer can be drawn correctly.');
      }
    }

    if (this.options.timeField.start && this.options.timeField.end) {
      this._startTimeIndex = new BinarySearchIndex();
      this._endTimeIndex = new BinarySearchIndex();
    } else if (this.options.timeField) {
      this._timeIndex = new BinarySearchIndex();
    }

    this._cache = {};
    this._currentSnapshot = []; // cache of what layers should be active
    this._activeRequests = 0;
  },

  /**
   * Layer Interface
   */

  onAdd: function (map) {
    /*
    not sure if this the best way to maintain a reference to FeatureManager
    within the event listener?
    */
    var self = this;

    map.on('zoomend', function(e) {
      var zoom = map.getZoom();
      if (zoom > self.service.options.maxZoom || zoom < self.service.options.minZoom) {
        console.log('dont draw!');
        // self.removeLayers(self._currentSnapshot);
        // self._currentSnapshot = [];
      }
      else {
        console.log('draw!');
        /* For every cell in this._activeCells:

        1. Get the cache key for the coords of the cell ` var key = this._cacheKey(coords);
        2. If this._cache[key] exists it will be an array of feature IDs.
        3. Call this.addLayers(this._cache[key]) to instruct the feature layer to add the layers back.

        */
      }
    });

    return VirtualGrid.prototype.onAdd.call(this, map);
  },

  onRemove: function (map) {
    return VirtualGrid.prototype.onRemove.call(this, map);
  },

  getAttribution: function () {
    return this.options.attribution;
  },

  /**
   * Feature Managment
   */

  createCell: function (bounds, coords) {
    this._requestFeatures(bounds, coords);
  },

  _requestFeatures: function (bounds, coords, callback) {
    this._activeRequests++;

    // our first active request fires loading
    if (this._activeRequests === 1) {
      this.fire('loading', {
        bounds: bounds
      }, true);
    }

    return this._buildQuery(bounds).run(function (error, featureCollection, response) {
      if (response && response.exceededTransferLimit) {
        this.fire('drawlimitexceeded');
      }

      // no error, features
      if (!error && featureCollection && featureCollection.features.length) {
        // schedule adding features until the next animation frame
        L.Util.requestAnimFrame(L.Util.bind(function () {
          this._addFeatures(featureCollection.features, coords);
          this._postProcessFeatures(bounds);
        }, this));
      }

      // no error, no features
      if (!error && featureCollection && !featureCollection.features.length) {
        this._postProcessFeatures(bounds);
      }

      if (callback) {
        callback.call(this, error, featureCollection);
      }
    }, this);
  },

  _postProcessFeatures: function (bounds) {
    // deincriment the request counter now that we have processed features
    this._activeRequests--;

    // if there are no more active requests fire a load event for this view
    if (this._activeRequests <= 0) {
      this.fire('load', {
        bounds: bounds
      });
    }
  },

  _cacheKey: function (coords) {
    return coords.z + ':' + coords.x + ':' + coords.y;
  },

  _addFeatures: function (features, coords) {
    var key = this._cacheKey(coords);
    this._cache[key] = this._cache[key] || [];

    for (var i = features.length - 1; i >= 0; i--) {
      var id = features[i].id;
      this._currentSnapshot.push(id);
      this._cache[key].push(id);
    }

    if (this.options.timeField) {
      this._buildTimeIndexes(features);
    }

    // remove this logic entirely once the new stuff is hooked up
    // var zoom = this._map.getZoom();

    // if (zoom > this.options.maxZoom ||
    //     zoom < this.options.minZoom) { return; }

    // will have to rip out the stuff below too once the new stuff is working
    // https://github.com/patrickarlt/leaflet-virtual-grid/blob/master/src/virtual-grid.js#L100-L102
    this.createLayers(features);
  },

  _buildQuery: function (bounds) {
    var query = this.service.query()
      .intersects(bounds)
      .where(this.options.where)
      .fields(this.options.fields)
      .precision(this.options.precision);

    if (this.options.simplifyFactor) {
      query.simplify(this._map, this.options.simplifyFactor);
    }

    if (this.options.timeFilterMode === 'server' && this.options.from && this.options.to) {
      query.between(this.options.from, this.options.to);
    }

    return query;
  },

  /**
   * Where Methods
   */

  setWhere: function (where, callback, context) {
    this.options.where = (where && where.length) ? where : '1=1';

    var oldSnapshot = [];
    var newSnapshot = [];
    var pendingRequests = 0;
    var requestError = null;
    var requestCallback = L.Util.bind(function (error, featureCollection) {
      if (error) {
        requestError = error;
      }

      if (featureCollection) {
        for (var i = featureCollection.features.length - 1; i >= 0; i--) {
          newSnapshot.push(featureCollection.features[i].id);
        }
      }

      pendingRequests--;

      if (pendingRequests <= 0) {
        this._currentSnapshot = newSnapshot;
        // schedule adding features until the next animation frame
        L.Util.requestAnimFrame(L.Util.bind(function () {
          this.removeLayers(oldSnapshot);
          this.addLayers(newSnapshot);
          if (callback) {
            callback.call(context, requestError);
          }
        }, this));
      }
    }, this);

    for (var i = this._currentSnapshot.length - 1; i >= 0; i--) {
      oldSnapshot.push(this._currentSnapshot[i]);
    }

    for (var key in this._activeCells) {
      pendingRequests++;
      var coords = this._keyToCellCoords(key);
      var bounds = this._cellCoordsToBounds(coords);
      this._requestFeatures(bounds, key, requestCallback);
    }

    return this;
  },

  getWhere: function () {
    return this.options.where;
  },

  /**
   * Time Range Methods
   */

  getTimeRange: function () {
    return [this.options.from, this.options.to];
  },

  setTimeRange: function (from, to, callback, context) {
    var oldFrom = this.options.from;
    var oldTo = this.options.to;
    var pendingRequests = 0;
    var requestError = null;
    var requestCallback = L.Util.bind(function (error) {
      if (error) {
        requestError = error;
      }
      this._filterExistingFeatures(oldFrom, oldTo, from, to);

      pendingRequests--;

      if (callback && pendingRequests <= 0) {
        callback.call(context, requestError);
      }
    }, this);

    this.options.from = from;
    this.options.to = to;

    this._filterExistingFeatures(oldFrom, oldTo, from, to);

    if (this.options.timeFilterMode === 'server') {
      for (var key in this._activeCells) {
        pendingRequests++;
        var coords = this._keyToCellCoords(key);
        var bounds = this._cellCoordsToBounds(coords);
        this._requestFeatures(bounds, key, requestCallback);
      }
    }

    return this;
  },

  refresh: function () {
    for (var key in this._activeCells) {
      var coords = this._keyToCellCoords(key);
      var bounds = this._cellCoordsToBounds(coords);
      this._requestFeatures(bounds, key);
    }

    if (this.redraw) {
      this.once('load', function () {
        this.eachFeature(function (layer) {
          this._redraw(layer.feature.id);
        }, this);
      }, this);
    }
  },

  _filterExistingFeatures: function (oldFrom, oldTo, newFrom, newTo) {
    var layersToRemove = (oldFrom && oldTo) ? this._getFeaturesInTimeRange(oldFrom, oldTo) : this._currentSnapshot;
    var layersToAdd = this._getFeaturesInTimeRange(newFrom, newTo);

    if (layersToAdd.indexOf) {
      for (var i = 0; i < layersToAdd.length; i++) {
        var shouldRemoveLayer = layersToRemove.indexOf(layersToAdd[i]);
        if (shouldRemoveLayer >= 0) {
          layersToRemove.splice(shouldRemoveLayer, 1);
        }
      }
    }

    // schedule adding features until the next animation frame
    L.Util.requestAnimFrame(L.Util.bind(function () {
      this.removeLayers(layersToRemove);
      this.addLayers(layersToAdd);
    }, this));
  },

  _getFeaturesInTimeRange: function (start, end) {
    var ids = [];
    var search;

    if (this.options.timeField.start && this.options.timeField.end) {
      var startTimes = this._startTimeIndex.between(start, end);
      var endTimes = this._endTimeIndex.between(start, end);
      search = startTimes.concat(endTimes);
    } else {
      search = this._timeIndex.between(start, end);
    }

    for (var i = search.length - 1; i >= 0; i--) {
      ids.push(search[i].id);
    }

    return ids;
  },

  _buildTimeIndexes: function (geojson) {
    var i;
    var feature;
    if (this.options.timeField.start && this.options.timeField.end) {
      var startTimeEntries = [];
      var endTimeEntries = [];
      for (i = geojson.length - 1; i >= 0; i--) {
        feature = geojson[i];
        startTimeEntries.push({
          id: feature.id,
          value: new Date(feature.properties[this.options.timeField.start])
        });
        endTimeEntries.push({
          id: feature.id,
          value: new Date(feature.properties[this.options.timeField.end])
        });
      }
      this._startTimeIndex.bulkAdd(startTimeEntries);
      this._endTimeIndex.bulkAdd(endTimeEntries);
    } else {
      var timeEntries = [];
      for (i = geojson.length - 1; i >= 0; i--) {
        feature = geojson[i];
        timeEntries.push({
          id: feature.id,
          value: new Date(feature.properties[this.options.timeField])
        });
      }

      this._timeIndex.bulkAdd(timeEntries);
    }
  },

  _featureWithinTimeRange: function (feature) {
    if (!this.options.from || !this.options.to) {
      return true;
    }

    var from = +this.options.from.valueOf();
    var to = +this.options.to.valueOf();

    if (typeof this.options.timeField === 'string') {
      var date = +feature.properties[this.options.timeField];
      return (date >= from) && (date <= to);
    }

    if (this.options.timeField.start && this.options.timeField.end) {
      var startDate = +feature.properties[this.options.timeField.start];
      var endDate = +feature.properties[this.options.timeField.end];
      return ((startDate >= from) && (startDate <= to)) || ((endDate >= from) && (endDate <= to));
    }
  },

  /**
   * Service Methods
   */

  authenticate: function (token) {
    this.service.authenticate(token);
    return this;
  },

  metadata: function (callback, context) {
    this.service.metadata(callback, context);
    return this;
  },

  query: function () {
    return this.service.query();
  },

  _getMetadata: function (callback) {
    if (this._metadata) {
      var error;
      callback(error, this._metadata);
    } else {
      this.metadata(L.Util.bind(function (error, response) {
        this._metadata = response;
        callback(error, this._metadata);
      }, this));
    }
  },

  addFeature: function (feature, callback, context) {
    this._getMetadata(L.Util.bind(function (error, metadata) {
      if (error) {
        if (callback) { callback.call(this, error, null); }
        return;
      }

      this.service.addFeature(feature, L.Util.bind(function (error, response) {
        if (!error) {
          // assign ID from result to appropriate objectid field from service metadata
          feature.properties[metadata.objectIdField] = response.objectId;

          // we also need to update the geojson id for createLayers() to function
          feature.id = response.objectId;
          this.createLayers([feature]);
        }

        if (callback) {
          callback.call(context, error, response);
        }
      }, this));
    }, this));
  },

  updateFeature: function (feature, callback, context) {
    this.service.updateFeature(feature, function (error, response) {
      if (!error) {
        this.removeLayers([feature.id], true);
        this.createLayers([feature]);
      }

      if (callback) {
        callback.call(context, error, response);
      }
    }, this);
  },

  deleteFeature: function (id, callback, context) {
    this.service.deleteFeature(id, function (error, response) {
      if (!error && response.objectId) {
        this.removeLayers([response.objectId], true);
      }
      if (callback) {
        callback.call(context, error, response);
      }
    }, this);
  },

  deleteFeatures: function (ids, callback, context) {
    return this.service.deleteFeatures(ids, function (error, response) {
      if (!error && response.length > 0) {
        for (var i = 0; i < response.length; i++) {
          this.removeLayers([response[i].objectId], true);
        }
      }
      if (callback) {
        callback.call(context, error, response);
      }
    }, this);
  }
});

/**
 * Temporal Binary Search Index
 */

function BinarySearchIndex (values) {
  this.values = values || [];
}

BinarySearchIndex.prototype._query = function (query) {
  var minIndex = 0;
  var maxIndex = this.values.length - 1;
  var currentIndex;
  var currentElement;

  while (minIndex <= maxIndex) {
    currentIndex = (minIndex + maxIndex) / 2 | 0;
    currentElement = this.values[Math.round(currentIndex)];
    if (+currentElement.value < +query) {
      minIndex = currentIndex + 1;
    } else if (+currentElement.value > +query) {
      maxIndex = currentIndex - 1;
    } else {
      return currentIndex;
    }
  }

  return ~maxIndex;
};

BinarySearchIndex.prototype.sort = function () {
  this.values.sort(function (a, b) {
    return +b.value - +a.value;
  }).reverse();
  this.dirty = false;
};

BinarySearchIndex.prototype.between = function (start, end) {
  if (this.dirty) {
    this.sort();
  }

  var startIndex = this._query(start);
  var endIndex = this._query(end);

  if (startIndex === 0 && endIndex === 0) {
    return [];
  }

  startIndex = Math.abs(startIndex);
  endIndex = (endIndex < 0) ? Math.abs(endIndex) : endIndex + 1;

  return this.values.slice(startIndex, endIndex);
};

BinarySearchIndex.prototype.bulkAdd = function (items) {
  this.dirty = true;
  this.values = this.values.concat(items);
};
<|endoftext|>


<commit_before>/* global bop */
this.ckan.module('mapsearch-result-panel', function ($, _) {
    /*
     *author: ""
     author_email: ""
     extras: Array[1]
     groups: Array[0]
     id: "9e34aa70-af7e-437c-ac2e-1d38a9489b15"
     isopen: true
     license_id: "cc-by-sa"
     license_title: "Creative Commons Attribution Share-Alike"
     license_url: "http://www.opendefinition.org/licenses/cc-by-sa"
     maintainer: ""
     maintainer_email: ""
     metadata_created: "2014-01-13T12:04:57.261053"
     metadata_modified: "2014-01-14T13:39:07.130159"
     name: "heiderhof"
     notes: "in stuttgart auf der hasenheide"
     num_resources: 1
     num_tags: 3
     organization: Object
     owner_org: "b7cf2b95-94d4-43f0-bf6a-67cbbf451504"
     private: false
     relationships_as_object: Array[0]
     relationships_as_subject: Array[0]
     resources: Array[1]
     revision_id: "0a9fceb2-e7d4-4deb-826f-9b8daaa7f155"
     revision_timestamp: "2014-01-14T11:50:53.688686"
     state: "active"
     tags: Array[3]
     title: "heiderhof"
     tracking_summary: Object
     type: "dataset"
     url: null
     version: null
     */
    if (typeof bop === 'undefined') {bop = {}}

    bop.make_result_panel = function (result, options) {
        var list_item_proto;
        panel = $('#result_panel_prototype').clone();
        panel.attr('id', result.id);
        var title_link = panel.find('.title_link');
        title_link.text(result.title);
        var dataset_url = "/dataset/" + result.id;
        title_link.attr('href', dataset_url);
        title_link.attr('title', "go to detail page");
        panel.find('.notes').html(truncate_notes(result.notes, dataset_url));
        panel.find('.license').text(result.license_title);
        if (result.tags && result.tags.length > 0) {
            var tag_list_container = panel.find('.result_tag_list');
            list_item_proto = tag_list_container.find('.list_item').clone();
            tag_list_container.empty();
            $.each(result.tags, function(idx, tag) {
                var list_item = list_item_proto.clone(),
                    filter_string = "tags:" + tag.display_name;
                list_item.find('a').data('filter', filter_string).text(tag.display_name);
                tag_list_container.append(list_item);
                if (bop.filters.indexOf(filter_string) !== -1) {
                    list_item.find('a').addClass('on');
                }
            });
        } else {
            panel.find('.result_tag_list').remove();
        }
        if (result.groups && result.groups.length > 0) {
            //TODO: implement groups
            panel.find('.groups_container').remove();
            console.log("NotImplementedError: groups present but not shown");
        } else {
            panel.find('.groups_container').remove();
        }
        if (result.resources && result.resources.length > 0) {
            var resources_list_container = panel.find('.result_resources_list');
            list_item_proto = resources_list_container.find('.list_item').clone();
            resources_list_container.empty();
            $.each(result.resources, function(idx, resource) {
                var list_item = list_item_proto.clone();
                list_item.find('a').attr('href', resource.url).text(resource.name || "unnamed file");
                resources_list_container.append(list_item);
            });
        } else {
            panel.find('.result_resources_list').remove();
        }
        if (result.organization) {
            panel.find('.organization').text(result.organization.name);
        } else {
            panel.find('.organization_container').remove();
        }
        panel.find('.created_at').text(new Date(result.metadata_created).toDateString());
        panel.find('.modified_at').text(new Date(result.metadata_modified).toDateString());
        panel.show();
        return panel;
    };

    var truncate_notes = function (notes, url) {
        var limit = 80,
            split = notes.split(" ");
        if (split.length <= limit) {
            return split.join(" ");
        } else {
            var base = split.slice(0, limit).join(" ");
            return base + " ... (see full notes here)";
        }
    };

    bop.insert_result_panel = function (panel) {
        container = $('#result_panel_container');
        container.append(panel);
    };

    bop.select_result = function (elem) {
        var id;
        if (typeof elem == 'string') {
            id = elem;
            elem = $('#' + elem);
        } else {
            id = elem.attr('id');
        }
        $('.dataset_result_panel').removeClass("selected");
        bop.result_layer.eachLayer(function(layer){
            try {
                var props = layer.feature.properties;
                if (!props.mapsearch || props.mapsearch != 'small') {
                    layer.setStyle(bop.result_style);
                }
            } catch(err) {}
        });
        elem.addClass("selected");
        elem.get(0).scrollIntoView();
        bop.result_layer.eachLayer(
            function (lay) {
              if (lay.feature.properties.id == id) {
                 lay.setStyle(bop.highlightStyle);
              }
            }
        );
        bop.update_result_list_presentation_mode();
    };

    $('#result_panel_container').on('click', ".dataset_result_panel", function (e) {
        bop.select_result($(this));
        e.preventDefault();
    });

    bop.update_result_list_presentation_mode = function () {
        $('.dataset_result_panel').find(".hide_when_small").hide();
        $('.dataset_result_panel.selected').find(".hide_when_small").show();
    };
});
<commit_msg>click on result panel should toggle selection<commit_after>/* global bop */
this.ckan.module('mapsearch-result-panel', function ($, _) {
    /*
     *author: ""
     author_email: ""
     extras: Array[1]
     groups: Array[0]
     id: "9e34aa70-af7e-437c-ac2e-1d38a9489b15"
     isopen: true
     license_id: "cc-by-sa"
     license_title: "Creative Commons Attribution Share-Alike"
     license_url: "http://www.opendefinition.org/licenses/cc-by-sa"
     maintainer: ""
     maintainer_email: ""
     metadata_created: "2014-01-13T12:04:57.261053"
     metadata_modified: "2014-01-14T13:39:07.130159"
     name: "heiderhof"
     notes: "in stuttgart auf der hasenheide"
     num_resources: 1
     num_tags: 3
     organization: Object
     owner_org: "b7cf2b95-94d4-43f0-bf6a-67cbbf451504"
     private: false
     relationships_as_object: Array[0]
     relationships_as_subject: Array[0]
     resources: Array[1]
     revision_id: "0a9fceb2-e7d4-4deb-826f-9b8daaa7f155"
     revision_timestamp: "2014-01-14T11:50:53.688686"
     state: "active"
     tags: Array[3]
     title: "heiderhof"
     tracking_summary: Object
     type: "dataset"
     url: null
     version: null
     */
    if (typeof bop === 'undefined') {bop = {}}

    bop.make_result_panel = function (result, options) {
        var list_item_proto;
        panel = $('#result_panel_prototype').clone();
        panel.attr('id', result.id);
        var title_link = panel.find('.title_link');
        title_link.text(result.title);
        var dataset_url = "/dataset/" + result.id;
        title_link.attr('href', dataset_url);
        title_link.attr('title', "go to detail page");
        panel.find('.notes').html(truncate_notes(result.notes, dataset_url));
        panel.find('.license').text(result.license_title);
        if (result.tags && result.tags.length > 0) {
            var tag_list_container = panel.find('.result_tag_list');
            list_item_proto = tag_list_container.find('.list_item').clone();
            tag_list_container.empty();
            $.each(result.tags, function(idx, tag) {
                var list_item = list_item_proto.clone(),
                    filter_string = "tags:" + tag.display_name;
                list_item.find('a').data('filter', filter_string).text(tag.display_name);
                tag_list_container.append(list_item);
                if (bop.filters.indexOf(filter_string) !== -1) {
                    list_item.find('a').addClass('on');
                }
            });
        } else {
            panel.find('.result_tag_list').remove();
        }
        if (result.groups && result.groups.length > 0) {
            //TODO: implement groups
            panel.find('.groups_container').remove();
            console.log("NotImplementedError: groups present but not shown");
        } else {
            panel.find('.groups_container').remove();
        }
        if (result.resources && result.resources.length > 0) {
            var resources_list_container = panel.find('.result_resources_list');
            list_item_proto = resources_list_container.find('.list_item').clone();
            resources_list_container.empty();
            $.each(result.resources, function(idx, resource) {
                var list_item = list_item_proto.clone();
                list_item.find('a').attr('href', resource.url).text(resource.name || "unnamed file");
                resources_list_container.append(list_item);
            });
        } else {
            panel.find('.result_resources_list').remove();
        }
        if (result.organization) {
            panel.find('.organization').text(result.organization.name);
        } else {
            panel.find('.organization_container').remove();
        }
        panel.find('.created_at').text(new Date(result.metadata_created).toDateString());
        panel.find('.modified_at').text(new Date(result.metadata_modified).toDateString());
        panel.show();
        return panel;
    };

    var truncate_notes = function (notes, url) {
        var limit = 80,
            split = notes.split(" ");
        if (split.length <= limit) {
            return split.join(" ");
        } else {
            var base = split.slice(0, limit).join(" ");
            return base + " ... (see full notes here)";
        }
    };

    bop.insert_result_panel = function (panel) {
        container = $('#result_panel_container');
        container.append(panel);
    };

    bop.unselect_all_results = function () {
        $('.dataset_result_panel').removeClass("selected");
        bop.result_layer.eachLayer(function(layer){
            try {
                var props = layer.feature.properties;
                if (!props.mapsearch || props.mapsearch != 'small') {
                    layer.setStyle(bop.result_style);
                }
            } catch(err) {}
        });
        bop.update_result_list_presentation_mode();
    }
    bop.select_result = function (elem) {
        var id;
        if (typeof elem == 'string') {
            id = elem;
            elem = $('#' + elem);
        } else {
            id = elem.attr('id');
        }
        bop.unselect_all_results()
        elem.addClass("selected");
        elem.get(0).scrollIntoView();
        bop.result_layer.eachLayer(
            function (lay) {
              if (lay.feature.properties.id == id) {
                 lay.setStyle(bop.highlightStyle);
              }
            }
        );
        bop.update_result_list_presentation_mode();
    };

    $('#result_panel_container').on('click', ".dataset_result_panel", function (e) {
        if (e.target.tagName != 'A') {
            if ($(e.target).parents('.dataset_result_panel.selected').length == 0) {
                bop.select_result($(this));
                e.preventDefault();
            } else {
                bop.unselect_all_results();
            }
        }
        ev = e;
    });

    bop.update_result_list_presentation_mode = function () {
        $('.dataset_result_panel').find(".hide_when_small").hide();
        $('.dataset_result_panel.selected').find(".hide_when_small").show();
    };
});
<|endoftext|>


<commit_before>// (C) Copyright 2014-2015 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { login } from '../actions';
import Split from 'grommet/components/Split';
import Section from 'grommet/components/Section';
import Sidebar from 'grommet/components/Sidebar';
import LoginForm from 'grommet/components/LoginForm';
import Logo from './Logo';

class IndexerLogin extends Component {

  constructor() {
    super();
    this._onSubmit = this._onSubmit.bind(this);
  }

  _onSubmit(fields) {
    this.props.dispatch(login(fields.username, fields.password));
  }

  render() {
    const { session } = this.props;
    var loginError = session.error;
    var errors = [];
    if (loginError) {
      var message;
      var resolution;
      message = loginError.message;
      if (loginError.resolution) {
        resolution = loginError.resolution;
      }
      errors.push(message);
      errors.push(resolution);
    }
    return (
      <Split flex="left" separator={true}>

        <Sidebar justify="center" align="center" pad="medium" size="large">
          <LoginForm
            logo={<Logo size="large" />}
            title="Ferret"
            onSubmit={this._onSubmit}
            errors={errors} />
        </Sidebar>
      </Split>
    );
  }
}

IndexerLogin.propTypes = {
  session: PropTypes.shape({
    email: PropTypes.string,
    error: PropTypes.shape({
      message: PropTypes.string,
      resolution: PropTypes.string
    }),
    token: PropTypes.string
  })
};

let select = (state) => ({session: state.session});

export default connect(select)(IndexerLogin);
<commit_msg>Make Login responsive.<commit_after>// (C) Copyright 2014-2015 Hewlett Packard Enterprise Development LP

import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { login } from '../actions';
import Split from 'grommet/components/Split';
import Section from 'grommet/components/Section';
import Sidebar from 'grommet/components/Sidebar';
import LoginForm from 'grommet/components/LoginForm';
import Logo from './Logo';

class IndexerLogin extends Component {

  constructor() {
    super();
    this._onSubmit = this._onSubmit.bind(this);
    this._onResponsive = this._onResponsive.bind(this);
    this.state = {responsive: 'multiple'};
  }

  _onSubmit(fields) {
    this.props.dispatch(login(fields.username, fields.password));
  }

  _onResponsive(responsive) {
    this.setState({responsive: responsive});
  }

  render() {
    const { session } = this.props;

    var image;
    if ('multiple' === this.state.responsive) {
      image =
;
    }

    var loginError = session.error;
    var errors = [];
    if (loginError) {
      var message;
      var resolution;
      message = loginError.message;
      if (loginError.resolution) {
        resolution = loginError.resolution;
      }
      errors.push(message);
      errors.push(resolution);
    }

    return (
      <Split flex="left" separator={true} onResponsive={this._onResponsive}>
        {image}
        <Sidebar justify="center" align="center" pad="medium" size="large">
          <LoginForm
            logo={<Logo size="large" />}
            title="Ferret"
            onSubmit={this._onSubmit}
            errors={errors} />
        </Sidebar>
      </Split>
    );
  }
}

IndexerLogin.propTypes = {
  session: PropTypes.shape({
    email: PropTypes.string,
    error: PropTypes.shape({
      message: PropTypes.string,
      resolution: PropTypes.string
    }),
    token: PropTypes.string
  })
};

let select = (state) => ({session: state.session});

export default connect(select)(IndexerLogin);
<|endoftext|>


<commit_before>//WARNING: no log() function usage before init.js

exports.core.os = navigator.platform
exports.core.device = 0
exports.core.vendor = ""

exports.trace = { key: false, focus: false }

exports.core.keyCodes = {
	13: 'Select',
	27: 'Back',
	37: 'Left',
	32: 'Space',
	33: 'PageUp',
	34: 'PageDown',
	38: 'Up',
	39: 'Right',
	40: 'Down',
	48: '0',
	49: '1',
	50: '2',
	51: '3',
	52: '4',
	53: '5',
	54: '6',
	55: '7',
	56: '8',
	57: '9',
	112: 'Red',
	113: 'Green',
	114: 'Yellow',
	115: 'Blue'
}

var copyArguments = function(args, src, prefix) {
	var copy = Array.prototype.slice.call(args, src)
	if (prefix !== undefined)
		copy.unshift(prefix)
	return copy
}

exports.core.copyArguments = copyArguments

/* ${init.js} */

if (log === null)
	log = console.log.bind(console)

var _checkDevice = function(target, info) {
	if (navigator.userAgent.indexOf(target) < 0)
		return

	log("[QML] " + target)
	exports.core.vendor = info.vendor
	exports.core.device = info.device
	exports.core.os = info.os
	log("loaded")
}

if (!exports.core.vendor) {
	_checkDevice('Blackberry', { 'vendor': 'blackberry', 'device': 2, 'os': 'blackberry' })
	_checkDevice('Android', { 'vendor': 'google', 'device': 2, 'os': 'android' })
	_checkDevice('iPhone', { 'vendor': 'apple', 'device': 2, 'os': 'iOS' })
	_checkDevice('iPad', { 'vendor': 'apple', 'device': 2, 'os': 'iOS' })
	_checkDevice('iPod', { 'vendor': 'apple', 'device': 2, 'os': 'iOS' })
}

var colorTable = {
	'maroon':	'800000',
	'red':		'ff0000',
	'orange':	'ffA500',
	'yellow':	'ffff00',
	'olive':	'808000',
	'purple':	'800080',
	'fuchsia':	'ff00ff',
	'white':	'ffffff',
	'lime':		'00ff00',
	'green':	'008000',
	'navy':		'000080',
	'blue':		'0000ff',
	'aqua':		'00ffff',
	'teal':		'008080',
	'black':	'000000',
	'silver':	'c0c0c0',
	'gray':		'080808',
	'transparent': '0000'
}

var safeCallImpl = function(callback, args, onError) {
	try { return callback.apply(null, args) } catch(ex) { onError(ex) }
}

exports.core.safeCall = function(args, onError) {
	return function(callback) { return safeCallImpl(callback, args, onError) }
}

/**
 * @constructor
 */
exports.core.CoreObject = function() { }
exports.core.CoreObject.prototype.constructor = exports.core.CoreObject

/** @constructor */
exports.core.Color = function(value) {
	if (typeof value !== 'string')
	{
		this.r = 255
		this.g = 0
		this.b = 255
		log("invalid color specification: " + value)
		return
	}
	var triplet
	if (value.substring(0, 4) == "rgba") {
		var b = value.indexOf('('), e = value.lastIndexOf(')')
		value = value.substring(b + 1, e).split(',')
		this.r = parseInt(value[0], 10)
		this.g = parseInt(value[1], 10)
		this.b = parseInt(value[2], 10)
		this.a = parseInt(value[3], 10)
		return
	}
	else {
		var h = value.charAt(0);
		if (h != '#')
			triplet = colorTable[value];
		else
			triplet = value.substring(1)
	}
	if (!triplet) {
		this.r = 255
		this.g = 0
		this.b = 255
		log("invalid color specification: " + value)
		return
	}

	var len = triplet.length;
	if (len == 3 || len == 4) {
		var r = parseInt(triplet.charAt(0), 16)
		var g = parseInt(triplet.charAt(1), 16)
		var b = parseInt(triplet.charAt(2), 16)
		var a = (len == 4)? parseInt(triplet.charAt(3), 16): 15
		this.r = (r << 4) | r;
		this.g = (g << 4) | g;
		this.b = (b << 4) | b;
		this.a = (a << 4) | a;
	} else if (len == 6 || len == 8) {
		this.r = parseInt(triplet.substring(0, 2), 16)
		this.g = parseInt(triplet.substring(2, 4), 16)
		this.b = parseInt(triplet.substring(4, 6), 16)
		this.a = (len == 8)? parseInt(triplet.substring(6, 8), 16): 255
	} else
		throw new Error("invalid color specification: " + value)
}
exports.core.Color.prototype.constructor = exports.core.Color
/** @const */
var Color = exports.core.Color

exports.core.Color.prototype.get = function() {
	return "rgba(" + this.r + "," + this.g + "," + this.b + "," + (this.a / 255) + ")";
}

exports.core.normalizeColor = function(spec) {
	return (new Color(spec)).get()
}

/** @constructor */
exports.core.DelayedAction = function(context, action) {
	this.context = context
	this.action = function() {
		this._scheduled = false
		action()
	}.bind(this)
}

exports.core.DelayedAction.prototype.schedule = function() {
	if (!this._scheduled) {
		this._scheduled = true
		this.context.scheduleAction(this.action)
	}
}

var Modernizr = window.Modernizr
var requestAnimationFrame = Modernizr.prefixed('requestAnimationFrame', window)	|| function(callback) { return setTimeout(callback, 0) }
var cancelAnimationFrame = Modernizr.prefixed('cancelAnimationFrame', window)	|| function(id) { return clearTimeout(id) }

exports.addProperty = function(proto, type, name, defaultValue) {
	var convert
	switch(type) {
		case 'enum':
		case 'int':		convert = function(value) { return parseInt(value, 0) }; break
		case 'bool':	convert = function(value) { return value? true: false }; break
		case 'real':	convert = function(value) { return parseFloat(value) }; break
		case 'string':	convert = function(value) { return String(value) }; break
		default:		convert = function(value) { return value }; break
	}

	if (defaultValue !== undefined) {
		defaultValue = convert(defaultValue)
	} else {
		switch(type) {
			case 'enum': //fixme: add default value here
			case 'int':		defaultValue = 0; break
			case 'bool':	defaultValue = false; break
			case 'real':	defaultValue = 0.0; break
			case 'string':	defaultValue = ""; break
			case 'array':	defaultValue = []; break
			default:
				defaultValue = (type[0].toUpperCase() == type[0])? null: undefined
		}
	}

	var storageName = '__property_' + name
	var forwardName = '__forward_' + name

	Object.defineProperty(proto, name, {
		get: function() {
			var p = this[storageName]
			return p !== undefined?
				p.interpolatedValue !== undefined? p.interpolatedValue: p.value:
				defaultValue
		},

		set: function(newValue) {
			newValue = convert(newValue)
			var p = this[storageName]
			if (p === undefined) { //no storage
				if (newValue === defaultValue) //value == defaultValue, no storage allocation
					return

				p = this[storageName] = { value : defaultValue }
			}
			var animation = this.getAnimation(name)
			if (animation && p.value !== newValue) {
				if (p.frameRequest)
					cancelAnimationFrame(p.frameRequest)

				var now = new Date()
				p.started = now.getTime() + now.getMilliseconds() / 1000.0

				var src = p.interpolatedValue !== undefined? p.interpolatedValue: p.value
				var dst = newValue

				var self = this

				var complete = function() {
					cancelAnimationFrame(p.frameRequest)
					p.frameRequest = undefined
					animation.complete = function() { }
					animation.running = false
					p.interpolatedValue = undefined
					p.started = undefined
					self._update(name, dst, src)
				}

				var duration = animation.duration

				var nextFrame = function() {
					var date = new Date()
					var now = date.getTime() + date.getMilliseconds() / 1000.0
					var t = 1.0 * (now - p.started) / duration
					if (t >= 1) {
						complete()
					} else {
						p.interpolatedValue = convert(animation.interpolate(dst, src, t))
						self._update(name, p.interpolatedValue, src)
						p.frameRequest = requestAnimationFrame(nextFrame)
					}
				}

				p.frameRequest = requestAnimationFrame(nextFrame)

				animation.running = true
				animation.complete = complete
			}
			var oldValue = p.value
			if (oldValue !== newValue) {
				var forwardTarget = this[forwardName]
				if (forwardTarget !== undefined) {
					if (oldValue !== null && (oldValue instanceof Object)) {
						//forward property update for mixins
						var forwardedOldValue = oldValue[forwardTarget]
						if (newValue !== forwardedOldValue) {
							oldValue[forwardTarget] = newValue
							this._update(name, newValue, forwardedOldValue)
						}
						return
					} else if (newValue instanceof Object) {
						//first assignment of mixin
						this.connectOnChanged(newValue, forwardTarget, function(v, ov) { this._update(name, v, ov) }.bind(this))
					}
				}
				p.value = newValue
				if ((!animation || !animation.running) && newValue == defaultValue)
					delete this[storageName]
				if (!animation)
					this._update(name, newValue, oldValue)
			}
		},
		enumerable: true
	})
}

exports.addAliasProperty = function(self, name, getObject, srcProperty) {
	var target = getObject()
	self.connectOnChanged(target, srcProperty, function(value) { self._update(name, value) })

	Object.defineProperty(self, name, {
		get: function() { return target[srcProperty] },
		set: function(value) { target[srcProperty] = value },
		enumerable: true
	})
}

exports.core.createSignalForwarder = function(object, name) {
	return (function() { object.emit.apply(object, copyArguments(arguments, 0, name)) })
}

/** @constructor */
exports.core.EventBinder = function(target) {
	this.target = target
	this.callbacks = {}
	this.enabled = false
}

exports.core.EventBinder.prototype.on = function(event, callback) {
	if (event in this.callbacks)
		throw new Error('double adding of event (' + event + ')')
	this.callbacks[event] = callback
	if (this.enabled)
		this.target.on(event, callback)
}

exports.core.EventBinder.prototype.constructor = exports.core.EventBinder

exports.core.EventBinder.prototype.enable = function(value) {
	if (value != this.enabled) {
		var target = this.target
		this.enabled = value
		if (value) {
			for(var event in this.callbacks)
				target.on(event, this.callbacks[event])
		} else {
			for(var event in this.callbacks)
				target.removeListener(event, this.callbacks[event])
		}
	}
}
<commit_msg>added Color([r, g, b, [, a]]) ctor, added mixColor function<commit_after>//WARNING: no log() function usage before init.js

exports.core.os = navigator.platform
exports.core.device = 0
exports.core.vendor = ""

exports.trace = { key: false, focus: false }

exports.core.keyCodes = {
	13: 'Select',
	27: 'Back',
	37: 'Left',
	32: 'Space',
	33: 'PageUp',
	34: 'PageDown',
	38: 'Up',
	39: 'Right',
	40: 'Down',
	48: '0',
	49: '1',
	50: '2',
	51: '3',
	52: '4',
	53: '5',
	54: '6',
	55: '7',
	56: '8',
	57: '9',
	112: 'Red',
	113: 'Green',
	114: 'Yellow',
	115: 'Blue'
}

var copyArguments = function(args, src, prefix) {
	var copy = Array.prototype.slice.call(args, src)
	if (prefix !== undefined)
		copy.unshift(prefix)
	return copy
}

exports.core.copyArguments = copyArguments

/* ${init.js} */

if (log === null)
	log = console.log.bind(console)

var _checkDevice = function(target, info) {
	if (navigator.userAgent.indexOf(target) < 0)
		return

	log("[QML] " + target)
	exports.core.vendor = info.vendor
	exports.core.device = info.device
	exports.core.os = info.os
	log("loaded")
}

if (!exports.core.vendor) {
	_checkDevice('Blackberry', { 'vendor': 'blackberry', 'device': 2, 'os': 'blackberry' })
	_checkDevice('Android', { 'vendor': 'google', 'device': 2, 'os': 'android' })
	_checkDevice('iPhone', { 'vendor': 'apple', 'device': 2, 'os': 'iOS' })
	_checkDevice('iPad', { 'vendor': 'apple', 'device': 2, 'os': 'iOS' })
	_checkDevice('iPod', { 'vendor': 'apple', 'device': 2, 'os': 'iOS' })
}

var colorTable = {
	'maroon':	'800000',
	'red':		'ff0000',
	'orange':	'ffA500',
	'yellow':	'ffff00',
	'olive':	'808000',
	'purple':	'800080',
	'fuchsia':	'ff00ff',
	'white':	'ffffff',
	'lime':		'00ff00',
	'green':	'008000',
	'navy':		'000080',
	'blue':		'0000ff',
	'aqua':		'00ffff',
	'teal':		'008080',
	'black':	'000000',
	'silver':	'c0c0c0',
	'gray':		'080808',
	'transparent': '0000'
}

var safeCallImpl = function(callback, args, onError) {
	try { return callback.apply(null, args) } catch(ex) { onError(ex) }
}

exports.core.safeCall = function(args, onError) {
	return function(callback) { return safeCallImpl(callback, args, onError) }
}

/**
 * @constructor
 */
exports.core.CoreObject = function() { }
exports.core.CoreObject.prototype.constructor = exports.core.CoreObject

/** @constructor */
exports.core.Color = function(value) {
	if (Array.isArray(value)) {
		this.r = value[0]
		this.g = value[1]
		this.b = value[2]
		this.a = value[3] !== undefined? value[3]: 255
		return
	}
	if (typeof value !== 'string')
	{
		this.r = 255
		this.g = 0
		this.b = 255
		log("invalid color specification: " + value)
		return
	}
	var triplet
	if (value.substring(0, 4) == "rgba") {
		var b = value.indexOf('('), e = value.lastIndexOf(')')
		value = value.substring(b + 1, e).split(',')
		this.r = parseInt(value[0], 10)
		this.g = parseInt(value[1], 10)
		this.b = parseInt(value[2], 10)
		this.a = parseInt(value[3], 10)
		return
	}
	else {
		var h = value.charAt(0);
		if (h != '#')
			triplet = colorTable[value];
		else
			triplet = value.substring(1)
	}
	if (!triplet) {
		this.r = 255
		this.g = 0
		this.b = 255
		log("invalid color specification: " + value)
		return
	}

	var len = triplet.length;
	if (len == 3 || len == 4) {
		var r = parseInt(triplet.charAt(0), 16)
		var g = parseInt(triplet.charAt(1), 16)
		var b = parseInt(triplet.charAt(2), 16)
		var a = (len == 4)? parseInt(triplet.charAt(3), 16): 15
		this.r = (r << 4) | r;
		this.g = (g << 4) | g;
		this.b = (b << 4) | b;
		this.a = (a << 4) | a;
	} else if (len == 6 || len == 8) {
		this.r = parseInt(triplet.substring(0, 2), 16)
		this.g = parseInt(triplet.substring(2, 4), 16)
		this.b = parseInt(triplet.substring(4, 6), 16)
		this.a = (len == 8)? parseInt(triplet.substring(6, 8), 16): 255
	} else
		throw new Error("invalid color specification: " + value)
}
exports.core.Color.prototype.constructor = exports.core.Color
/** @const */
var Color = exports.core.Color

exports.core.Color.prototype.get = function() {
	return "rgba(" + this.r + "," + this.g + "," + this.b + "," + (this.a / 255) + ")";
}

exports.core.normalizeColor = function(spec) {
	return (new Color(spec)).get()
}

exports.core.mixColor = function(specA, specB, r) {
	var a = new Color(specA)
	var b = new Color(specB)
	var mix = function(a, b, r) { return Math.floor((a - b) * r + b) }
	return new Color([mix(a.r, b.r, r), mix(a.g, b.g, r), mix(a.b, b.b, r), mix(a.a, b.a, r)]).get()
}

/** @constructor */
exports.core.DelayedAction = function(context, action) {
	this.context = context
	this.action = function() {
		this._scheduled = false
		action()
	}.bind(this)
}

exports.core.DelayedAction.prototype.schedule = function() {
	if (!this._scheduled) {
		this._scheduled = true
		this.context.scheduleAction(this.action)
	}
}

var Modernizr = window.Modernizr
var requestAnimationFrame = Modernizr.prefixed('requestAnimationFrame', window)	|| function(callback) { return setTimeout(callback, 0) }
var cancelAnimationFrame = Modernizr.prefixed('cancelAnimationFrame', window)	|| function(id) { return clearTimeout(id) }

exports.addProperty = function(proto, type, name, defaultValue) {
	var convert
	switch(type) {
		case 'enum':
		case 'int':		convert = function(value) { return parseInt(value, 0) }; break
		case 'bool':	convert = function(value) { return value? true: false }; break
		case 'real':	convert = function(value) { return parseFloat(value) }; break
		case 'string':	convert = function(value) { return String(value) }; break
		default:		convert = function(value) { return value }; break
	}

	if (defaultValue !== undefined) {
		defaultValue = convert(defaultValue)
	} else {
		switch(type) {
			case 'enum': //fixme: add default value here
			case 'int':		defaultValue = 0; break
			case 'bool':	defaultValue = false; break
			case 'real':	defaultValue = 0.0; break
			case 'string':	defaultValue = ""; break
			case 'array':	defaultValue = []; break
			default:
				defaultValue = (type[0].toUpperCase() == type[0])? null: undefined
		}
	}

	var storageName = '__property_' + name
	var forwardName = '__forward_' + name

	Object.defineProperty(proto, name, {
		get: function() {
			var p = this[storageName]
			return p !== undefined?
				p.interpolatedValue !== undefined? p.interpolatedValue: p.value:
				defaultValue
		},

		set: function(newValue) {
			newValue = convert(newValue)
			var p = this[storageName]
			if (p === undefined) { //no storage
				if (newValue === defaultValue) //value == defaultValue, no storage allocation
					return

				p = this[storageName] = { value : defaultValue }
			}
			var animation = this.getAnimation(name)
			if (animation && p.value !== newValue) {
				if (p.frameRequest)
					cancelAnimationFrame(p.frameRequest)

				var now = new Date()
				p.started = now.getTime() + now.getMilliseconds() / 1000.0

				var src = p.interpolatedValue !== undefined? p.interpolatedValue: p.value
				var dst = newValue

				var self = this

				var complete = function() {
					cancelAnimationFrame(p.frameRequest)
					p.frameRequest = undefined
					animation.complete = function() { }
					animation.running = false
					p.interpolatedValue = undefined
					p.started = undefined
					self._update(name, dst, src)
				}

				var duration = animation.duration

				var nextFrame = function() {
					var date = new Date()
					var now = date.getTime() + date.getMilliseconds() / 1000.0
					var t = 1.0 * (now - p.started) / duration
					if (t >= 1) {
						complete()
					} else {
						p.interpolatedValue = convert(animation.interpolate(dst, src, t))
						self._update(name, p.interpolatedValue, src)
						p.frameRequest = requestAnimationFrame(nextFrame)
					}
				}

				p.frameRequest = requestAnimationFrame(nextFrame)

				animation.running = true
				animation.complete = complete
			}
			var oldValue = p.value
			if (oldValue !== newValue) {
				var forwardTarget = this[forwardName]
				if (forwardTarget !== undefined) {
					if (oldValue !== null && (oldValue instanceof Object)) {
						//forward property update for mixins
						var forwardedOldValue = oldValue[forwardTarget]
						if (newValue !== forwardedOldValue) {
							oldValue[forwardTarget] = newValue
							this._update(name, newValue, forwardedOldValue)
						}
						return
					} else if (newValue instanceof Object) {
						//first assignment of mixin
						this.connectOnChanged(newValue, forwardTarget, function(v, ov) { this._update(name, v, ov) }.bind(this))
					}
				}
				p.value = newValue
				if ((!animation || !animation.running) && newValue == defaultValue)
					delete this[storageName]
				if (!animation)
					this._update(name, newValue, oldValue)
			}
		},
		enumerable: true
	})
}

exports.addAliasProperty = function(self, name, getObject, srcProperty) {
	var target = getObject()
	self.connectOnChanged(target, srcProperty, function(value) { self._update(name, value) })

	Object.defineProperty(self, name, {
		get: function() { return target[srcProperty] },
		set: function(value) { target[srcProperty] = value },
		enumerable: true
	})
}

exports.core.createSignalForwarder = function(object, name) {
	return (function() { object.emit.apply(object, copyArguments(arguments, 0, name)) })
}

/** @constructor */
exports.core.EventBinder = function(target) {
	this.target = target
	this.callbacks = {}
	this.enabled = false
}

exports.core.EventBinder.prototype.on = function(event, callback) {
	if (event in this.callbacks)
		throw new Error('double adding of event (' + event + ')')
	this.callbacks[event] = callback
	if (this.enabled)
		this.target.on(event, callback)
}

exports.core.EventBinder.prototype.constructor = exports.core.EventBinder

exports.core.EventBinder.prototype.enable = function(value) {
	if (value != this.enabled) {
		var target = this.target
		this.enabled = value
		if (value) {
			for(var event in this.callbacks)
				target.on(event, this.callbacks[event])
		} else {
			for(var event in this.callbacks)
				target.removeListener(event, this.callbacks[event])
		}
	}
}
<|endoftext|>