<commit_before>/**
* Copyright 2012-2019, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

/* global PlotlyGeoAssets:false */

var mapboxgl = require('mapbox-gl');
var d3 = require('d3');

var Fx = require('../../components/fx');
var Lib = require('../../lib');
var Registry = require('../../registry');
var Axes = require('../cartesian/axes');
var dragElement = require('../../components/dragelement');
var prepSelect = require('../cartesian/select').prepSelect;
var selectOnClick = require('../cartesian/select').selectOnClick;
var constants = require('./constants');
var layoutAttributes = require('./layout_attributes');
var createMapboxLayer = require('./layers');

function Mapbox(gd, id) {
    this.id = id;
    this.gd = gd;

    var fullLayout = gd._fullLayout;
    var context = gd._context;

    this.container = fullLayout._glcontainer.node();
    this.isStatic = context.staticPlot;

    // unique id for this Mapbox instance
    this.uid = fullLayout._uid + '-' + this.id;

    // create framework on instantiation for a smoother first plot call
    this.div = null;
    this.xaxis = null;
    this.yaxis = null;
    this.createFramework(fullLayout);

    // state variables used to infer how and what to update
    this.map = null;
    this.accessToken = null;
    this.styleObj = null;
    this.traceHash = {};
    this.layerList = [];
}

var proto = Mapbox.prototype;

proto.plot = function(calcData, fullLayout, promises) {
    var self = this;
    var opts = fullLayout[self.id];

    // remove map and create a new map if access token has change
    if(self.map && (opts.accesstoken !== self.accessToken)) {
        self.map.remove();
        self.map = null;
        self.styleObj = null;
        self.traceHash = [];
        self.layerList = {};
    }

    var promise;

    if(!self.map) {
        promise = new Promise(function(resolve, reject) {
            self.createMap(calcData, fullLayout, resolve, reject);
        });
    } else {
        promise = new Promise(function(resolve, reject) {
            self.updateMap(calcData, fullLayout, resolve, reject);
        });
    }

    promises.push(promise);
};

proto.createMap = function(calcData, fullLayout, resolve, reject) {
    var self = this;
    var gd = self.gd;
    var opts = fullLayout[self.id];

    // store style id and URL or object
    var styleObj = self.styleObj = getStyleObj(opts.style);

    // store access token associated with this map
    self.accessToken = opts.accesstoken;

    // create the map!
    var map = self.map = new mapboxgl.Map({
        container: self.div,

        style: styleObj.style,
        center: convertCenter(opts.center),
        zoom: opts.zoom,
        bearing: opts.bearing,
        pitch: opts.pitch,

        interactive: !self.isStatic,
        preserveDrawingBuffer: self.isStatic,

        doubleClickZoom: false,
        boxZoom: false
    });

    // make sure canvas does not inherit left and top css
    map._canvas.style.left = '0px';
    map._canvas.style.top = '0px';

    self.rejectOnError(reject);

    var promises = [];

    promises.push(new Promise(function(resolve) {
        map.once('load', resolve);
    }));

    promises = promises.concat(self.fetchMapData(calcData, fullLayout));

    Promise.all(promises).then(function() {
        self.updateData(calcData);
        self.updateLayout(fullLayout);
        self.resolveOnRender(resolve);
    });

    if(self.isStatic) return;

    var wheeling = false;

    // keep track of pan / zoom in user layout and emit relayout event
    map.on('moveend', function(eventData) {
        if(!self.map) return;

        // 'moveend' gets triggered by map.setCenter, map.setZoom,
        // map.setBearing and map.setPitch.
        //
        // Here, we make sure that state updates amd 'plotly_relayout'
        // are triggered only when the 'moveend' originates from a
        // mouse target (filtering out API calls) to not
        // duplicate 'plotly_relayout' events.

        if(eventData.originalEvent || wheeling) {
            var optsNow = gd._fullLayout[self.id];
            Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));

            var viewNow = self.getView();
            optsNow._input.center = optsNow.center = viewNow.center;
            optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
            optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
            optsNow._input.pitch = optsNow.pitch = viewNow.pitch;

            gd.emit('plotly_relayout', self.getViewEdits(viewNow));
        }
        wheeling = false;
    });

    map.on('wheel', function() {
        wheeling = true;
    });

    map.on('mousemove', function(evt) {
        var bb = self.div.getBoundingClientRect();

        // some hackery to get Fx.hover to work
        evt.clientX = evt.point.x + bb.left;
        evt.clientY = evt.point.y + bb.top;

        evt.target.getBoundingClientRect = function() { return bb; };

        self.xaxis.p2c = function() { return evt.lngLat.lng; };
        self.yaxis.p2c = function() { return evt.lngLat.lat; };

        Fx.hover(gd, evt, self.id);
    });

    function unhover() {
        Fx.loneUnhover(fullLayout._toppaper);
    }

    map.on('dragstart', unhover);
    map.on('zoomstart', unhover);

    function emitUpdate() {
        var viewNow = self.getView();
        gd.emit('plotly_relayouting', self.getViewEdits(viewNow));
    }

    map.on('drag', emitUpdate);
    map.on('zoom', emitUpdate);

    map.on('dblclick', function() {
        var optsNow = gd._fullLayout[self.id];
        Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));

        var viewInitial = self.viewInitial;
        map.setCenter(convertCenter(viewInitial.center));
        map.setZoom(viewInitial.zoom);
        map.setBearing(viewInitial.bearing);
        map.setPitch(viewInitial.pitch);

        var viewNow = self.getView();
        optsNow._input.center = optsNow.center = viewNow.center;
        optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
        optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
        optsNow._input.pitch = optsNow.pitch = viewNow.pitch;

        gd.emit('plotly_doubleclick', null);
        gd.emit('plotly_relayout', self.getViewEdits(viewNow));
    });

    // define event handlers on map creation, to keep one ref per map,
    // so that map.on / map.off in updateFx works as expected
    self.clearSelect = function() {
        gd._fullLayout._zoomlayer.selectAll('.select-outline').remove();
    };

    /**
     * Returns a click handler function that is supposed
     * to handle clicks in pan mode.
     */
    self.onClickInPanFn = function(dragOptions) {
        return function(evt) {
            var clickMode = gd._fullLayout.clickmode;

            if(clickMode.indexOf('select') > -1) {
                selectOnClick(evt.originalEvent, gd, [self.xaxis], [self.yaxis], self.id, dragOptions);
            }

            if(clickMode.indexOf('event') > -1) {
                // TODO: this does not support right-click. If we want to support it, we
                // would likely need to change mapbox to use dragElement instead of straight
                // mapbox event binding. Or perhaps better, make a simple wrapper with the
                // right mousedown, mousemove, and mouseup handlers just for a left/right click
                // pie would use this too.
                Fx.click(gd, evt.originalEvent);
            }
        };
    };
};

proto.fetchMapData = function(calcData) {
    var promises = [];

    function fetch(url) {
        return new Promise(function(resolve, reject) {
            d3.json(url, function(err, d) {
                if(err) {
                    var msg = err.status === 404 ?
                        ('GeoJSON at URL ' + url + ' does not exist.') :
                        ('Unexpected error while fetching from ' + url);
                    return reject(new Error(msg));
                }

                PlotlyGeoAssets[url] = d;
                resolve(d);
            });
        });
    }

    for(var i = 0; i < calcData.length; i++) {
        var trace = calcData[i][0].trace;
        var url = trace.geojson;

        if(typeof url === 'string' && !PlotlyGeoAssets[url]) {
            PlotlyGeoAssets[url] = 'pending';
            promises.push(fetch(url));
        }
    }

    return promises;
};

proto.updateMap = function(calcData, fullLayout, resolve, reject) {
    var self = this;
    var map = self.map;
    var opts = fullLayout[this.id];

    self.rejectOnError(reject);

    var promises = [];
    var styleObj = getStyleObj(opts.style);

    if(self.styleObj.id !== styleObj.id) {
        self.styleObj = styleObj;
        map.setStyle(styleObj.style);

        // need to rebuild trace layers on reload
        // to avoid 'lost event' errors
        self.traceHash = {};

        promises.push(new Promise(function(resolve) {
            map.once('styledata', resolve);
        }));
    }

    promises = promises.concat(self.fetchMapData(calcData, fullLayout));

    Promise.all(promises).then(function() {
        self.updateData(calcData);
        self.updateLayout(fullLayout);
        self.resolveOnRender(resolve);
    });
};

proto.updateData = function(calcData) {
    var traceHash = this.traceHash;
    var traceObj, trace, i, j;

    // update or create trace objects
    for(i = 0; i < calcData.length; i++) {
        var calcTrace = calcData[i];

        trace = calcTrace[0].trace;
        traceObj = traceHash[trace.uid];

        if(traceObj) {
            traceObj.update(calcTrace);
        } else if(trace._module) {
            traceHash[trace.uid] = trace._module.plot(this, calcTrace);
        }
    }

    // remove empty trace objects
    var ids = Object.keys(traceHash);
    idLoop:
    for(i = 0; i < ids.length; i++) {
        var id = ids[i];

        for(j = 0; j < calcData.length; j++) {
            trace = calcData[j][0].trace;
            if(id === trace.uid) continue idLoop;
        }

        traceObj = traceHash[id];
        traceObj.dispose();
        delete traceHash[id];
    }
};
<commit_msg>sort all choroplethmapbox traces below scattermapbox traces<commit_after>/**
* Copyright 2012-2019, Plotly, Inc.
* All rights reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

'use strict';

/* global PlotlyGeoAssets:false */

var mapboxgl = require('mapbox-gl');
var d3 = require('d3');

var Fx = require('../../components/fx');
var Lib = require('../../lib');
var Registry = require('../../registry');
var Axes = require('../cartesian/axes');
var dragElement = require('../../components/dragelement');
var prepSelect = require('../cartesian/select').prepSelect;
var selectOnClick = require('../cartesian/select').selectOnClick;
var constants = require('./constants');
var layoutAttributes = require('./layout_attributes');
var createMapboxLayer = require('./layers');

function Mapbox(gd, id) {
    this.id = id;
    this.gd = gd;

    var fullLayout = gd._fullLayout;
    var context = gd._context;

    this.container = fullLayout._glcontainer.node();
    this.isStatic = context.staticPlot;

    // unique id for this Mapbox instance
    this.uid = fullLayout._uid + '-' + this.id;

    // create framework on instantiation for a smoother first plot call
    this.div = null;
    this.xaxis = null;
    this.yaxis = null;
    this.createFramework(fullLayout);

    // state variables used to infer how and what to update
    this.map = null;
    this.accessToken = null;
    this.styleObj = null;
    this.traceHash = {};
    this.layerList = [];
}

var proto = Mapbox.prototype;

proto.plot = function(calcData, fullLayout, promises) {
    var self = this;
    var opts = fullLayout[self.id];

    // remove map and create a new map if access token has change
    if(self.map && (opts.accesstoken !== self.accessToken)) {
        self.map.remove();
        self.map = null;
        self.styleObj = null;
        self.traceHash = [];
        self.layerList = {};
    }

    var promise;

    if(!self.map) {
        promise = new Promise(function(resolve, reject) {
            self.createMap(calcData, fullLayout, resolve, reject);
        });
    } else {
        promise = new Promise(function(resolve, reject) {
            self.updateMap(calcData, fullLayout, resolve, reject);
        });
    }

    promises.push(promise);
};

proto.createMap = function(calcData, fullLayout, resolve, reject) {
    var self = this;
    var gd = self.gd;
    var opts = fullLayout[self.id];

    // store style id and URL or object
    var styleObj = self.styleObj = getStyleObj(opts.style);

    // store access token associated with this map
    self.accessToken = opts.accesstoken;

    // create the map!
    var map = self.map = new mapboxgl.Map({
        container: self.div,

        style: styleObj.style,
        center: convertCenter(opts.center),
        zoom: opts.zoom,
        bearing: opts.bearing,
        pitch: opts.pitch,

        interactive: !self.isStatic,
        preserveDrawingBuffer: self.isStatic,

        doubleClickZoom: false,
        boxZoom: false
    });

    // make sure canvas does not inherit left and top css
    map._canvas.style.left = '0px';
    map._canvas.style.top = '0px';

    self.rejectOnError(reject);

    var promises = [];

    promises.push(new Promise(function(resolve) {
        map.once('load', resolve);
    }));

    promises = promises.concat(self.fetchMapData(calcData, fullLayout));

    Promise.all(promises).then(function() {
        self.updateData(calcData);
        self.updateLayout(fullLayout);
        self.resolveOnRender(resolve);
    });

    if(self.isStatic) return;

    var wheeling = false;

    // keep track of pan / zoom in user layout and emit relayout event
    map.on('moveend', function(eventData) {
        if(!self.map) return;

        // 'moveend' gets triggered by map.setCenter, map.setZoom,
        // map.setBearing and map.setPitch.
        //
        // Here, we make sure that state updates amd 'plotly_relayout'
        // are triggered only when the 'moveend' originates from a
        // mouse target (filtering out API calls) to not
        // duplicate 'plotly_relayout' events.

        if(eventData.originalEvent || wheeling) {
            var optsNow = gd._fullLayout[self.id];
            Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));

            var viewNow = self.getView();
            optsNow._input.center = optsNow.center = viewNow.center;
            optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
            optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
            optsNow._input.pitch = optsNow.pitch = viewNow.pitch;

            gd.emit('plotly_relayout', self.getViewEdits(viewNow));
        }
        wheeling = false;
    });

    map.on('wheel', function() {
        wheeling = true;
    });

    map.on('mousemove', function(evt) {
        var bb = self.div.getBoundingClientRect();

        // some hackery to get Fx.hover to work
        evt.clientX = evt.point.x + bb.left;
        evt.clientY = evt.point.y + bb.top;

        evt.target.getBoundingClientRect = function() { return bb; };

        self.xaxis.p2c = function() { return evt.lngLat.lng; };
        self.yaxis.p2c = function() { return evt.lngLat.lat; };

        Fx.hover(gd, evt, self.id);
    });

    function unhover() {
        Fx.loneUnhover(fullLayout._toppaper);
    }

    map.on('dragstart', unhover);
    map.on('zoomstart', unhover);

    function emitUpdate() {
        var viewNow = self.getView();
        gd.emit('plotly_relayouting', self.getViewEdits(viewNow));
    }

    map.on('drag', emitUpdate);
    map.on('zoom', emitUpdate);

    map.on('dblclick', function() {
        var optsNow = gd._fullLayout[self.id];
        Registry.call('_storeDirectGUIEdit', gd.layout, gd._fullLayout._preGUI, self.getViewEdits(optsNow));

        var viewInitial = self.viewInitial;
        map.setCenter(convertCenter(viewInitial.center));
        map.setZoom(viewInitial.zoom);
        map.setBearing(viewInitial.bearing);
        map.setPitch(viewInitial.pitch);

        var viewNow = self.getView();
        optsNow._input.center = optsNow.center = viewNow.center;
        optsNow._input.zoom = optsNow.zoom = viewNow.zoom;
        optsNow._input.bearing = optsNow.bearing = viewNow.bearing;
        optsNow._input.pitch = optsNow.pitch = viewNow.pitch;

        gd.emit('plotly_doubleclick', null);
        gd.emit('plotly_relayout', self.getViewEdits(viewNow));
    });

    // define event handlers on map creation, to keep one ref per map,
    // so that map.on / map.off in updateFx works as expected
    self.clearSelect = function() {
        gd._fullLayout._zoomlayer.selectAll('.select-outline').remove();
    };

    /**
     * Returns a click handler function that is supposed
     * to handle clicks in pan mode.
     */
    self.onClickInPanFn = function(dragOptions) {
        return function(evt) {
            var clickMode = gd._fullLayout.clickmode;

            if(clickMode.indexOf('select') > -1) {
                selectOnClick(evt.originalEvent, gd, [self.xaxis], [self.yaxis], self.id, dragOptions);
            }

            if(clickMode.indexOf('event') > -1) {
                // TODO: this does not support right-click. If we want to support it, we
                // would likely need to change mapbox to use dragElement instead of straight
                // mapbox event binding. Or perhaps better, make a simple wrapper with the
                // right mousedown, mousemove, and mouseup handlers just for a left/right click
                // pie would use this too.
                Fx.click(gd, evt.originalEvent);
            }
        };
    };
};

proto.fetchMapData = function(calcData) {
    var promises = [];

    function fetch(url) {
        return new Promise(function(resolve, reject) {
            d3.json(url, function(err, d) {
                if(err) {
                    var msg = err.status === 404 ?
                        ('GeoJSON at URL ' + url + ' does not exist.') :
                        ('Unexpected error while fetching from ' + url);
                    return reject(new Error(msg));
                }

                PlotlyGeoAssets[url] = d;
                resolve(d);
            });
        });
    }

    for(var i = 0; i < calcData.length; i++) {
        var trace = calcData[i][0].trace;
        var url = trace.geojson;

        if(typeof url === 'string' && !PlotlyGeoAssets[url]) {
            PlotlyGeoAssets[url] = 'pending';
            promises.push(fetch(url));
        }
    }

    return promises;
};

proto.updateMap = function(calcData, fullLayout, resolve, reject) {
    var self = this;
    var map = self.map;
    var opts = fullLayout[this.id];

    self.rejectOnError(reject);

    var promises = [];
    var styleObj = getStyleObj(opts.style);

    if(self.styleObj.id !== styleObj.id) {
        self.styleObj = styleObj;
        map.setStyle(styleObj.style);

        // need to rebuild trace layers on reload
        // to avoid 'lost event' errors
        self.traceHash = {};

        promises.push(new Promise(function(resolve) {
            map.once('styledata', resolve);
        }));
    }

    promises = promises.concat(self.fetchMapData(calcData, fullLayout));

    Promise.all(promises).then(function() {
        self.updateData(calcData);
        self.updateLayout(fullLayout);
        self.resolveOnRender(resolve);
    });
};

var traceType2orderIndex = {
    'choroplethmapbox': 0,
    'scattermapbox': 1
};

proto.updateData = function(calcData) {
    var traceHash = this.traceHash;
    var traceObj, trace, i, j;

    var calcDataSorted = calcData.slice().sort(function(a, b) {
        return (
            traceType2orderIndex[a[0].trace.type] -
            traceType2orderIndex[b[0].trace.type]
        );
    });

    // update or create trace objects
    for(i = 0; i < calcDataSorted.length; i++) {
        var calcTrace = calcDataSorted[i];

        trace = calcTrace[0].trace;
        traceObj = traceHash[trace.uid];

        if(traceObj) {
            traceObj.update(calcTrace);
        } else if(trace._module) {
            traceHash[trace.uid] = trace._module.plot(this, calcTrace);
        }
    }

    // remove empty trace objects
    var ids = Object.keys(traceHash);
    idLoop:
    for(i = 0; i < ids.length; i++) {
        var id = ids[i];

        for(j = 0; j < calcData.length; j++) {
            trace = calcData[j][0].trace;
            if(id === trace.uid) continue idLoop;
        }

        traceObj = traceHash[id];
        traceObj.dispose();
        delete traceHash[id];
    }
};

{'fraction_non_alphanumeric': 0.12765147912108818, 'fraction_numerical': 0.0029011699800247314, 'mean_word_length': 2.281367041198502, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 7, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7190732', 'n_tokens_mistral': 6442, 'n_tokens_neox': 6219, 'n_words': 1493}