"use strict";
/* Copyright 2015 Bloomberg Finance L.P.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiSelector = exports.BrushIntervalSelector = exports.BrushSelector = void 0;
const d3 = __importStar(require("d3"));
const utils_1 = require("./utils");
const _ = __importStar(require("underscore"));
const selector = __importStar(require("./Selector"));
const utils = __importStar(require("./utils"));
const sel_utils = __importStar(require("./selector_utils"));
// TODO: examine refactoring the two abstract base classes belowing using an
// up-to-date mixin pattern.
// Because we use abstract base classes, the mixins cannot work with current TypeScript:
// https://github.com/microsoft/TypeScript/issues/29653
// Instead of using mixins we use free functions for now
function point_selector(x, y) {
    return function (xar, yar) {
        if (typeof yar === 'undefined') {
            // the 'old' method for backwards compatibility
            return sel_utils.point_in_rectangle(xar, x, y);
        }
        const len = Math.min(xar.length, yar.length);
        const mask = new Uint8Array(len);
        // for performance we keep the if statement out of the loop
        if (x.length && y.length) {
            for (let i = 0; i < len; i++) {
                mask[i] =
                    x[0] <= xar[i] && xar[i] <= x[1] && y[0] <= yar[i] && yar[i] <= y[1]
                        ? 1
                        : 0;
            }
            return mask;
        }
        else if (x.length) {
            for (let i = 0; i < len; i++) {
                mask[i] = x[0] <= xar[i] && xar[i] <= x[1] ? 1 : 0;
            }
        }
        else {
            // (y.length)
            for (let i = 0; i < len; i++) {
                mask[i] = y[0] <= yar[i] && yar[i] <= y[1] ? 1 : 0;
            }
        }
        return mask;
    };
}
function rect_selector(x, y) {
    return function (xy) {
        return sel_utils.rect_inter_rect(xy[0], xy[1], x, y);
    };
}
function sort(a, b) {
    return a - b;
}
function update_mark_selected(brush, extent_x, extent_y) {
    if (extent_x === undefined || extent_x.length === 0) {
        // Reset all the selected in marks
        _.each(brush.mark_views, (mark_view) => {
            return mark_view.selector_changed();
        });
        return;
    }
    let x, y;
    if (extent_y === undefined) {
        // 1d brush
        const orient = brush.model.get('orientation');
        (x = orient == 'vertical' ? [] : extent_x),
            (y = orient == 'vertical' ? extent_x : []);
    }
    else {
        // 2d brush
        (x = extent_x), (y = extent_y);
    }
    if (x.length) {
        x.sort(sort);
    }
    if (y.length) {
        y.sort(sort);
    }
    _.each(brush.mark_views, (mark_view) => {
        mark_view.selector_changed(point_selector(x, y), rect_selector(x, y));
    });
}
function adjust_rectangle(brush) {
    if (brush.model.get('orientation') == 'vertical') {
        brush.d3el.selectAll('rect').attr('x', 0).attr('width', brush.width);
    }
    else {
        brush.d3el.selectAll('rect').attr('y', 0).attr('height', brush.height);
    }
}
class BrushMixinXYSelector extends selector.BaseXYSelector {
    constructor() {
        super(...arguments);
        this.ignoreBrushEvents = false;
    }
    brush_render() {
        this.brushing = false;
    }
    color_change() {
        if (this.model.get('color') !== null) {
            this.d3el.selectAll('.selection').style('fill', this.model.get('color'));
        }
    }
    brush_start() {
        if (this.ignoreBrushEvents) {
            return;
        }
        this.brushing = true;
        this.model.set('brushing', true);
        this.touch();
    }
    brush_move() {
        if (this.ignoreBrushEvents) {
            return;
        }
        this.convert_and_save();
    }
    brush_end() {
        if (this.ignoreBrushEvents) {
            return;
        }
        this.model.set('brushing', false);
        this.convert_and_save();
        this.brushing = false;
    }
    adjust_rectangle() {
        adjust_rectangle(this);
    }
    update_mark_selected(extent_x, extent_y) {
        return update_mark_selected(this, extent_x, extent_y);
    }
}
class BrushMixinXSelector extends selector.BaseXSelector {
    constructor() {
        super(...arguments);
        this.ignoreBrushEvents = false;
    }
    brush_render() {
        this.brushing = false;
    }
    color_change() {
        if (this.model.get('color') !== null) {
            this.d3el.selectAll('.selection').style('fill', this.model.get('color'));
        }
    }
    brush_start() {
        if (this.ignoreBrushEvents) {
            return;
        }
        this.brushing = true;
        this.model.set('brushing', true);
        this.touch();
    }
    brush_move() {
        if (this.ignoreBrushEvents) {
            return;
        }
        this.convert_and_save();
    }
    brush_end() {
        if (this.ignoreBrushEvents) {
            return;
        }
        this.model.set('brushing', false);
        this.convert_and_save();
        this.brushing = false;
    }
    adjust_rectangle() {
        adjust_rectangle(this);
    }
    update_mark_selected(extent_x, extent_y) {
        return update_mark_selected(this, extent_x, extent_y);
    }
}
class BrushSelector extends BrushMixinXYSelector {
    render() {
        const _super = Object.create(null, {
            render: { get: () => super.render }
        });
        return __awaiter(this, void 0, void 0, function* () {
            yield _super.render.call(this);
            this.brush_render();
            yield this.create_scales();
            yield this.mark_views_promise;
            this.brush = d3
                .brush()
                .on('start', _.bind(this.brush_start, this))
                .on('brush', _.bind(this.brush_move, this))
                .on('end', _.bind(this.brush_end, this));
            this.brush.extent([
                [0, 0],
                [this.width, this.height],
            ]);
            this.d3el.attr('class', 'selector brushintsel');
            this.d3el.call(this.brush);
            this.adjust_rectangle();
            this.color_change();
            this.create_listeners();
            this.selected_changed();
        });
    }
    create_listeners() {
        super.create_listeners();
        this.listenTo(this.model, 'change:color', this.color_change);
        // Move these to BaseXYSelector
        this.listenTo(this.model, 'change:selected_x', this.selected_changed);
        this.listenTo(this.model, 'change:selected_y', this.selected_changed);
    }
    empty_selection() {
        this.update_mark_selected();
        this.model.set('selected_x', null);
        this.model.set('selected_y', null);
        this.touch();
    }
    convert_and_save() {
        const e = utils_1.d3GetEvent();
        if (!e.sourceEvent) {
            return;
        }
        if (!e.selection) {
            this.empty_selection();
        }
        else {
            const d0 = e.selection;
            const pixel_extent_x = [d0[0][0], d0[1][0]];
            const pixel_extent_y = [d0[1][1], d0[0][1]];
            const extent_x = pixel_extent_x
                .map(this.x_scale.invert.bind(this.x_scale))
                .sort(sort);
            const extent_y = pixel_extent_y
                .map(this.y_scale.invert.bind(this.y_scale))
                .sort(sort);
            this.update_mark_selected(pixel_extent_x, pixel_extent_y);
            this.set_selected('selected_x', this.x_scale.model.typedRange(extent_x));
            this.set_selected('selected_y', this.y_scale.model.typedRange(extent_y));
            this.touch();
        }
    }
    selected_changed() {
        if (this.brushing) {
            return;
        }
        //reposition the interval selector and set the selected attribute.
        const selected_x = this.model.get('selected_x') || [], selected_y = this.model.get('selected_y') || [];
        if (selected_x.length === 0 || selected_y.length === 0) {
            this.update_mark_selected();
        }
        else if (selected_x.length != 2 || selected_y.length != 2) {
            // invalid value for selected. Ignoring the value
            return;
        }
        else {
            const pixel_extent_x = selected_x
                .map((v) => this.x_scale.offset + this.x_scale.scale(v))
                .sort(sort);
            const pixel_extent_y = selected_y
                .map((v) => this.y_scale.offset + this.y_scale.scale(v))
                .sort(sort);
            this.update_mark_selected(pixel_extent_x, pixel_extent_y);
        }
        this.syncModelToBrush();
    }
    relayout() {
        super.relayout();
        this.d3el
            .select('.background')
            .attr('width', this.width)
            .attr('height', this.height);
        this.set_x_range([this.x_scale]);
        this.set_y_range([this.y_scale]);
        this.brush.extent([
            [0, 0],
            [this.width, this.height],
        ]);
        this.syncModelToBrush();
    }
    syncModelToBrush() {
        // Move and redraw the brush selector, preventing move events to be triggered
        this.ignoreBrushEvents = true;
        try {
            if (this.model.get('selected_x') && this.model.get('selected_y')) {
                const range_x = this.model
                    .get('selected_x')
                    .map((v) => this.x_scale.offset + this.x_scale.scale(v))
                    .sort(sort);
                const range_y = this.model
                    .get('selected_y')
                    .map((v) => this.y_scale.offset + this.y_scale.scale(v))
                    .sort(sort);
                this.brush.move(this.d3el, [
                    [range_x[0], range_y[0]],
                    [range_x[1], range_y[1]],
                ]);
            }
            else {
                this.brush.move(this.d3el, null);
            }
            this.d3el.call(this.brush);
        }
        finally {
            this.ignoreBrushEvents = false;
        }
        this.d3el.call(this.brush);
    }
    // TODO: check that we've properly overridden the mixin.
    adjust_rectangle() { }
    reset() { }
}
exports.BrushSelector = BrushSelector;
class BrushIntervalSelector extends BrushMixinXSelector {
    render() {
        const _super = Object.create(null, {
            render: { get: () => super.render }
        });
        return __awaiter(this, void 0, void 0, function* () {
            yield _super.render.call(this);
            this.brush_render();
            yield this.mark_views_promise;
            yield this.create_scales();
            this.brush = (this.model.get('orientation') == 'vertical'
                ? d3.brushY()
                : d3.brushX())
                .on('start', _.bind(this.brush_start, this))
                .on('brush', _.bind(this.brush_move, this))
                .on('end', _.bind(this.brush_end, this));
            this.brush.extent([
                [0, 0],
                [this.width, this.height],
            ]);
            this.d3el.attr('class', 'selector brushintsel');
            this.d3el.call(this.brush);
            this.adjust_rectangle();
            this.color_change();
            this.create_listeners();
            this.selected_changed();
        });
    }
    create_listeners() {
        super.create_listeners();
        this.listenTo(this.model, 'change:color', this.color_change);
        this.listenTo(this.model, 'change:selected', this.selected_changed);
    }
    empty_selection() {
        this.update_mark_selected();
        this.model.set('selected', null);
        this.touch();
    }
    convert_and_save() {
        const e = utils_1.d3GetEvent();
        if (!e.sourceEvent) {
            return;
        }
        if (!e.selection) {
            this.empty_selection();
        }
        else {
            const pixel_extent = e.selection;
            const extent = pixel_extent
                .map(this.scale.invert.bind(this.scale))
                .sort(sort);
            this.update_mark_selected(pixel_extent);
            this.set_selected('selected', this.scale.model.typedRange(extent));
            this.touch();
        }
    }
    update_scale_domain(ignore_gui_update) {
        // Call the base class function to update the scale.
        super.update_scale_domain();
        if (ignore_gui_update !== true) {
            this.selected_changed();
        }
    }
    selected_changed() {
        if (this.brushing) {
            return;
        }
        //reposition the interval selector and set the selected attribute.
        const selected = this.model.get('selected') || [];
        if (selected.length === 0) {
            this.update_mark_selected();
        }
        else if (selected.length != 2) {
            // invalid value for selected. Ignoring the value
            return;
        }
        else {
            const extent = [selected[0], selected[1]];
            const pixel_extent = extent.map((v) => this.scale.scale(v)).sort(sort);
            this.update_mark_selected(pixel_extent);
        }
        this.syncModelToBrush();
    }
    relayout() {
        super.relayout();
        this.adjust_rectangle();
        this.d3el
            .select('.background')
            .attr('width', this.width)
            .attr('height', this.height);
        this.set_range([this.scale]);
        this.brush.extent([
            [0, 0],
            [this.width, this.height],
        ]);
        this.syncModelToBrush();
    }
    syncModelToBrush() {
        // Move and redraw the brush selector, preventing move events to be triggered
        this.ignoreBrushEvents = true;
        try {
            if (this.model.get('selected')) {
                const range = this.model
                    .get('selected')
                    .map((v) => this.scale.scale(v))
                    .sort(sort);
                this.brush.move(this.d3el, range);
            }
            else {
                this.brush.move(this.d3el, null);
            }
            this.d3el.call(this.brush);
        }
        finally {
            this.ignoreBrushEvents = false;
        }
    }
    reset() { }
}
exports.BrushIntervalSelector = BrushIntervalSelector;
function add_remove_classes(selection, add_classes, remove_classes) {
    //adds the classes present in add_classes and removes the classes in
    //the list remove_classes
    //selection attribute should be a d3-selection
    if (remove_classes) {
        remove_classes.forEach((r_class) => {
            selection.classed(r_class, false);
        });
    }
    if (add_classes) {
        add_classes.forEach((a_class) => {
            selection.classed(a_class, true);
        });
    }
}
class MultiSelector extends BrushMixinXSelector {
    constructor() {
        super(...arguments);
        this.brushes = [];
        this.brush_g = [];
    }
    render() {
        super.render.apply(this);
        this.brush_render();
        this.names = this.model.get('names');
        this.curr_index = 0;
        const scale_creation_promise = this.create_scales();
        Promise.all([this.mark_views_promise, scale_creation_promise]).then(() => {
            this.d3el.attr('class', 'multiselector');
            this.d3el.attr('width', this.width);
            this.d3el.attr('height', this.height);
            this.create_brush();
            this.selecting_brush = false;
            this.create_listeners();
        });
    }
    create_listeners() {
        super.create_listeners();
        this.listenTo(this.model, 'change:names', this.labels_change);
        this.listenTo(this.model, 'change:color', this.color_change);
    }
    labels_change(model, value) {
        const prev_names = model.previous('names');
        this.names = value;
        const data = _.range(this.curr_index + 1);
        const selected = utils.deepCopy(this.model.get('selected'));
        // TODO: Use do diff?
        data.forEach((elem) => {
            const label = this.get_label(elem);
            const prev_label = this.get_label(elem, prev_names);
            if (prev_label !== label) {
                this.d3el.select('.brush_text_' + elem).text(label);
                selected[label] = selected[prev_label];
                delete selected[prev_label];
            }
        });
        this.set_selected('_selected', selected);
        this.touch();
    }
    create_brush() {
        // Function to add new brushes.
        const index = this.curr_index;
        const vertical = this.model.get('orientation') == 'vertical';
        const brush = (vertical ? d3.brushY() : d3.brushX())
            .on('start', () => {
            this.brush_start();
        })
            .on('brush', () => {
            this.multi_brush_move(index);
        })
            .on('end', () => {
            this.multi_brush_end(index);
        });
        brush.extent([
            [0, 0],
            [this.width, this.height],
        ]);
        const new_brush_g = this.d3el.append('g').attr('class', 'selector brushintsel active');
        new_brush_g
            .append('text')
            .text(this.get_label(this.curr_index))
            .attr('class', 'brush_text_' + this.curr_index)
            .style('display', 'none');
        if (this.model.get('orientation') == 'vertical') {
            new_brush_g.select('text').attr('x', 30);
        }
        else {
            new_brush_g.select('text').attr('y', 30);
        }
        new_brush_g.call(brush);
        this.color_change();
        this.adjust_rectangle();
        this.reset_handler(new_brush_g);
        this.brushes[this.curr_index] = brush;
        this.brush_g[this.curr_index] = new_brush_g;
        this.curr_index = this.curr_index + 1;
    }
    reset_handler(brush_g) {
        const that = this;
        const old_handler = brush_g.on('mousedown.brush');
        brush_g.on('mousedown.brush', function () {
            const accelKey = utils_1.d3GetEvent().ctrlKey || utils_1.d3GetEvent().metaKey;
            if (utils_1.d3GetEvent().shiftKey && accelKey) {
                that.reset();
            }
            else if (accelKey) {
                add_remove_classes(d3.select(this), ['inactive'], ['active']);
                that.create_brush();
            }
            else if (utils_1.d3GetEvent().shiftKey && that.selecting_brush === false) {
                add_remove_classes(that.d3el.selectAll('.selector'), ['visible'], ['active', 'inactive']);
                that.selecting_brush = true;
            }
            else {
                add_remove_classes(that.d3el.selectAll('.selector'), ['inactive'], ['visible']);
                add_remove_classes(d3.select(this), ['active'], ['inactive']);
                old_handler.call(this);
                that.selecting_brush = false;
            }
        });
    }
    get_label(index, arr) {
        //arr is optional. If you do not pass anything, this.names is
        //considered arr.
        if (arr === undefined || arr === null) {
            arr = this.names;
        }
        return arr.length > index ? arr[index] : index;
    }
    multi_brush_move(item) {
        if (this.ignoreBrushEvents) {
            return;
        }
        const extent = utils_1.d3GetEvent().selection;
        this.update_text(item, extent);
        this.convert_and_save(extent, item);
    }
    update_text(item, extent) {
        if (extent === null) {
            this.d3el.select('.brush_text_' + item).style('display', 'none');
            return;
        }
        const orient = this.model.get('orientation') == 'vertical' ? 'y' : 'x';
        const hide_names = !this.model.get('show_names');
        const mid = (extent[0] + extent[1]) / 2;
        this.d3el
            .select('.brush_text_' + item)
            .style('display', hide_names ? 'none' : 'inline')
            .attr(orient, mid);
    }
    multi_brush_end(item) {
        if (this.ignoreBrushEvents) {
            return;
        }
        const sel = utils_1.d3GetEvent().selection;
        this.model.set('brushing', false);
        this.convert_and_save(sel, item);
        this.brushing = false;
    }
    reset() {
        this.d3el.selectAll('.selector').remove();
        this.model.set('_selected', {});
        this.curr_index = 0;
        this.brushes = [];
        this.brush_g = [];
        this.touch();
        this.create_brush();
    }
    convert_and_save(extent, item) {
        if (!extent) {
            this.update_mark_selected();
            this.model.set('_selected', {});
        }
        else {
            const selected = utils.deepCopy(this.model.get('_selected'));
            selected[this.get_label(item)] = extent.map(this.scale.invert.bind(this.scale));
            this.update_mark_selected(extent);
            this.model.set('_selected', selected);
        }
        this.touch();
    }
    // TODO: make a proper implementation
    selected_changed() { }
    relayout() {
        super.relayout();
        this.adjust_rectangle();
        this.d3el
            .select('.background')
            .attr('width', this.width)
            .attr('height', this.height);
        this.set_range([this.scale]);
        this.brushes.forEach((brush) => {
            brush.extent([
                [0, 0],
                [this.width, this.height],
            ]);
        });
        this.syncModelToBrush();
    }
    syncModelToBrush() {
        // Move and redraw the brush selectors, preventing move events to be triggered
        this.ignoreBrushEvents = true;
        try {
            const selected = this.model.get('_selected');
            this.brushes.forEach((brush, index) => {
                const brushSelected = selected[this.get_label(index)];
                if (brushSelected) {
                    const range = brushSelected
                        .map((v) => this.scale.scale(v))
                        .sort(sort);
                    this.update_text(index, range);
                    brush.move(this.brush_g[index], range);
                }
                else {
                    this.update_text(index, null);
                    brush.move(this.brush_g[index], null);
                }
                this.brush_g[index].call(brush);
                this.reset_handler(this.brush_g[index]);
            });
        }
        finally {
            this.ignoreBrushEvents = false;
        }
    }
    remove() {
        this.model.off('change:names', null, this);
        this.model.off('change:color', null, this);
        super.remove();
    }
}
exports.MultiSelector = MultiSelector;
//# sourceMappingURL=BrushSelector.js.map