<commit_before>var React = require('react');
var axios = require('axios');

export default class ControlPanel extends React.Component {
    //TODO (#31): add listeners for Keyboard controls
    constructor(props) {
        super(props);
        this.state = {
            power: 50,
            discoveredBots: [],
            keyboard: false,
            xbox: false,
            trackedBots: [],
        };

        this.handleInputChange = this.handleInputChange.bind(this);
        this.updateDiscoveredBots = this.updateDiscoveredBots.bind(this);
        this.sendKV = this.sendKV.bind(this);
        this.startLogging = this.startLogging.bind(this);
        this.sendMotors = this.sendMotors.bind(this);
        this.removeBot = this.removeBot.bind(this);
        this.xboxToggle = this.xboxToggle.bind(this);
        //this.keyboardToggle = this.keyboardToggle.bind(this);
        this.getTrackedBots = this.getTrackedBots.bind(this);
        this.selectBot = this.selectBot.bind(this);
    }

    /* handler for input changes to modify the state */
    handleInputChange(event) {
        const target = event.target;
        const value = target.value;
        const name = target.name;
        if (name=="xbox"){
            this.setState({
                [name]: target.checked
            });
            this.xboxToggle(target.checked);
        } else {
            this.setState({
                [name]: value
            });
        }
    }

    /**
     * Updates tracked bots before page load.
     */
    componentWillMount() {
        this.getTrackedBots()
    }

    /* sends a key-value command to bot */
    sendKV(event){
        const pow = this.state.power;
        const target = event.target;
        if(target.id=="fwd") {
            this.sendMotors(pow, pow, pow, pow);
        }
        else if(target.id=="bck") {
            this.sendMotors(-pow, -pow, -pow, -pow);
        }
        else if(target.id=="lft") {
            this.sendMotors(-pow, pow, -pow, pow);
        }
        else if(target.id=="rt") {
            this.sendMotors(pow, -pow, pow, -pow);
        }
        else if(target.id=="cw"){
            this.sendMotors(pow, -pow, pow, -pow);
        }
        else if(target.id=="ccw"){
            this.sendMotors(-pow, pow, -pow, pow);
        }
        else if(target.id=="stop"){
            this.sendMotors(0,0,0,0);
        }
        else if(target.id=="log") {
            this.startLogging();
        }
        else {
            axios({
                method:'POST',
                url:'/sendKV',
                data: JSON.stringify({
                    key: document.getElementById('kv_key').value,
                    value: document.getElementById('kv_value').value,
                    name: this.props.currentBot
                }),
            })
            .then(function(response) {
                console.log('sent kv');
            })
            .catch(function (error) {
                console.warn(error);
            });
        }
    }

    /* sends a motor command to bot
    * input: front left, front right, back left, back right (all ints)
    * */
    sendMotors(fl,fr,bl,br){
        const _this = this;
        axios({
            method:'POST',
            url:'/commandBot',
            data: JSON.stringify({
                name: this.props.currentBot,
                fl: fl,
                fr: fr,
                bl: bl,
                br: br
            }),
        })
        .then(function(response) {
            console.log('Sent motor command: ' + response)
        })
        .catch(function (error) {
            console.warn(error);
        });
    }

    /**
     * Gets bots currently tracked
     */
    getTrackedBots() {
        const _this = this;
        axios({
            method:'POST',
            url:'/getTrackedBots',
            })
                .then(function(response) {
                    _this.setState({trackedBots: response.data});
            })
                .catch(function (error) {
                    console.log(error);
        });
    }

    /**
     * Handles onChange for bot dropdown. Changes currently selected bot.
     */
    selectBot(event) {
        this.props.setCurrentBot(event.target.value);
    }

    /* starts data logging */
    startLogging(){
        console.log("logging data listener");
        axios({
            method:'POST',
            url:'/logdata',
            data: JSON.stringify({name: this.props.currentBot}),
            processData: false,
        })
        .then(function(response) {
            console.log('started logging data');
        })
        .catch(function (error) {
            console.warn(error);
        });
    }

    /* removes selected bot from list */
    removeBot(){
        console.log("remove bot listener");
        axios({
            method:'POST',
            url:'/removeBot',
            data: JSON.stringify({name: this.props.currentBot}),
        })
        .then(function(response) {
            console.log('removed bot successfully');
        })
        .catch(function (error) {
            console.warn(error);
        });
    }

    /* toggles xbox controls on or off */
    xboxToggle(checked){
        console.log('xboxToggle '+checked);
        if (checked){
            axios({
                method:'POST',
                url:'/runXbox',
                data: JSON.stringify({name: this.props.currentBot}),
            })
            .then(function(response) {
                console.log('successfully toggled Xbox ON');
            })
            .catch(function (error) {
                console.warn(error);
            });
        } else {
            axios({
                method:'POST',
                url:'/stopXbox',
                data: JSON.stringify({name: this.props.currentBot}),
            })
            .then(function(response) {
                console.log('successfully toggled Xbox OFF');
            })
            .catch(function (error) {
                console.warn(error);
            });
        }
    }

    updateDiscoveredBots(){
        //TODO (#32) - change below request to axios
        //        $.ajax({
        //            method: "POST",
        //            url: '/discoverBots',
        //            dataType: 'json',
        //            data: '',
        //            contentType: 'application/json',
        //            success: function (data) {
        //                 //Check if discovered_bots and data are the same (check length and then contents)
        //                if(data.length != discovered_bots.length){
        //                    //If not then clear list and re-make displayed elements
        //                    redoDiscoverList(data);
        //                }
        //                else{
        //                    //Check value to ensure both structures contain the same data
        //                    for(let x=0;x<data.length;x++){
        //                        if(data[x]!=discovered_bots[x]){
        //                            redoDiscoverList(data);
        //                            //Prevent the list from being remade constantly
        //                            break;
        //                        }
        //                    }
        //                }
        //                setTimeout(updateDiscoveredBots,3000); // Try again in 3 sec
        //            }
        //        });
    }

    redoDiscoverList(data){
        let new_bots = [];

        for (let i = 0; i < data.length; i++) {
            //Trim the forward-slash
            var ip_address = data[i].substring(1);
            new_bots.push(ip_address)
        }
        this.setState({discoveredBots: new_bots})
    }

    render(){
        console.log(this.props)
        return (

                Control Panel

Movement controls:

                                 Choose bot:
                                <select value={this.props.currentBot} onChange={this.selectBot}> id="botlist" name="bots">
                                    <option value="(DEBUG) Sim Bot">(DEBUG) Sim Bot</option>
                                    {
                                        this.state.trackedBots.map(function(botname, idx){
                                            return <option
                                                        key={idx}
                                                        value={botname}>
                                                   {botname}
                                                   </option>
                                        })
                                    }
                                </select>

                        <button className="btn btn-danger" id="removeBot" onClick={this.removeBot}>Remove Bot</button>

                        Power ({this.state.power}):
                        <input id="power" type="range" name="power" min="0" max="100" value={this.state.power} defaultValue="50" onChange={this.handleInputChange}/>

                        <button className="btn" id="ccw" onClick={this.sendKV}>turn CCW</button>
                        <button className="btn" id="fwd" onClick={this.sendKV}>forward</button>
                        <button className="btn" id="cw" onClick={this.sendKV}>turn CW</button>

                        <button className="btn" id="lft" onClick={this.sendKV}>left</button>
                        <button className="btn btn-danger" id="stop" onClick={this.sendKV}>STOP</button>
                        <button className="btn" id="rt" onClick={this.sendKV}>right</button>

                        <button className="btn btn-success" id="log" onClick={this.sendKV}>log data</button>
                        <button className="btn" id="bck" onClick={this.sendKV}>backward</button>

                            Keyboard Controls

                                <input name="keyboard" type="checkbox" id="keyboard-controls"/>

                            Xbox Controls

                                <input name="xbox" type="checkbox" id="xbox-controls" onChange={this.handleInputChange}/>

                        <input type="text" id="kv_key" placeholder="Key (e.g. WHEELS)"/>
                        <input type="text" id="kv_value" placeholder="Value (e.g. 10,10)"/>
                        <button id="sendkv" onClick={this.sendKV} className="btn btn-success">Send KV</button>

        )
    }
}

// $(document).ready(function() {
//     /*
//      * Event listener for key inputs. Sends to selected bot.
//      */
//     var lastKeyPressed;
//     window.onkeydown = function (e) {
//         let keyboardEnable = document.getElementById('keyboard-controls').checked;
//         if (!keyboardEnable) return;
//
//         let pow = getPower();
//         let code = e.keyCode ? e.keyCode : e.which;
//
//         if (code === lastKeyPressed) return;
//
//         if (code === 87) {
//             // w=forward
//             sendMotors(pow, pow, pow, pow);
//
//         } else if (code === 83) {
//             // s=backward
//             sendMotors(-pow, -pow, -pow, -pow);
//
//         } else if (code == 65) {
//             // a=ccw
//             sendMotors(-pow, pow, -pow, pow);
//
//         } else if (code == 68) {
//             // d=cw
//             sendMotors(pow, -pow, pow, -pow);
//
//         } else if (code == 81) {
//             // q=left
//             sendMotors(-pow, pow, pow, -pow);
//
//         } else if (code == 69) {
//             // e=right
//             sendMotors(pow, -pow, -pow, pow);
//         } else {
//             return;
//         }
//         lastKeyPressed = code;
//     };
//
//     window.onkeyup = function (e) {
//         let keyboardEnable = document.getElementById('keyboard-controls').checked;
//         if (!keyboardEnable) return;
//
//         let code = e.keyCode ? e.keyCode : e.which;
//
//         if (code === lastKeyPressed) {
//             // Stop
//             sendMotors(0,0,0,0);
//             lastKeyPressed = -1;
//         }
//     };
// });
//
// /*
// *   Send KV -- allows users to manually send key and value to bot (for debugging/testing
//     purposes)
// */
// function sendKV(){
//     $.ajax({
//         method:'POST',
//         url:'/sendKV',
//         dataType: 'json',
//         data: JSON.stringify({
//             key:$("#kv_key").val(),
//             value:$("#kv_value").val(),
//             name:getBotID()
//         }),
//         contentType: 'application/json'
//     });
// }
<commit_msg>Added keyboard functions<commit_after>var React = require('react');
var axios = require('axios');

export default class ControlPanel extends React.Component {
    //TODO (#31): add listeners for Keyboard controls
    constructor(props) {
        super(props);
        this.state = {
            power: 50,
            discoveredBots: [],
            keyboard: false,
            xbox: false,
            trackedBots: [],
        };

        this.handleInputChange = this.handleInputChange.bind(this);
        this.updateDiscoveredBots = this.updateDiscoveredBots.bind(this);
        this.sendKV = this.sendKV.bind(this);
        this.startLogging = this.startLogging.bind(this);
        this.sendMotors = this.sendMotors.bind(this);
        this.removeBot = this.removeBot.bind(this);
        this.xboxToggle = this.xboxToggle.bind(this);
        this.getTrackedBots = this.getTrackedBots.bind(this);
        this.selectBot = this.selectBot.bind(this);
        this.onKeyDown = this.onKeyDown.bind(this);
        window.addEventListener('keydown', this.onKeyDown);

    }

    onKeyDown(event){
        if (this.state.keyboard){
            if (event.key == 'w'){
                this.sendMotors(100, 100, 100, 100);
            }
            else if (event.key == 'a'){
                this.sendMotors(-100, 100, 0,0 );
            }
            else if (event.key == 'd'){
                this.sendMotors(100, -100,0 ,0);
            }
            else{
                this.sendMotors(-100, -100, 0, 0);
            }

        }
    }

    /* handler for input changes to modify the state */
    handleInputChange(event) {
        const target = event.target;
        const value = target.value;
        const name = target.name;
        if (name=="xbox"){
            this.setState({
                [name]: target.checked
            });
            this.xboxToggle(target.checked);
        } else if (name=="keyboard"){
            this.setState({
                [name]: target.checked
            });
            console.log(this.state.keyboard)
        } else {
            this.setState({
                [name]: value
            });
        }
    }

    /**
     * Updates tracked bots before page load.
     */
    componentWillMount() {
        this.getTrackedBots()
    }

    /* sends a key-value command to bot */
    sendKV(event){
        const pow = this.state.power;
        const target = event.target;
        if(target.id=="fwd") {
            this.sendMotors(pow, pow, pow, pow);
        }
        else if(target.id=="bck") {
            this.sendMotors(-pow, -pow, -pow, -pow);
        }
        else if(target.id=="lft") {
            this.sendMotors(-pow, pow, -pow, pow);
        }
        else if(target.id=="rt") {
            this.sendMotors(pow, -pow, pow, -pow);
        }
        else if(target.id=="cw"){
            this.sendMotors(pow, -pow, pow, -pow);
        }
        else if(target.id=="ccw"){
            this.sendMotors(-pow, pow, -pow, pow);
        }
        else if(target.id=="stop"){
            this.sendMotors(0,0,0,0);
        }
        else if(target.id=="log") {
            this.startLogging();
        }
        else {
            axios({
                method:'POST',
                url:'/sendKV',
                data: JSON.stringify({
                    key: document.getElementById('kv_key').value,
                    value: document.getElementById('kv_value').value,
                    name: this.props.currentBot
                }),
            })
            .then(function(response) {
                console.log('sent kv');
            })
            .catch(function (error) {
                console.warn(error);
            });
        }
    }

    /* sends a motor command to bot
    * input: front left, front right, back left, back right (all ints)
    * */
    sendMotors(fl,fr,bl,br){
        const _this = this;
        axios({
            method:'POST',
            url:'/commandBot',
            data: JSON.stringify({
                name: this.props.currentBot,
                fl: fl,
                fr: fr,
                bl: bl,
                br: br
            }),
        })
        .then(function(response) {
            console.log('Sent motor command: ' + response)
        })
        .catch(function (error) {
            console.warn(error);
        });
    }

    /**
     * Gets bots currently tracked
     */
    getTrackedBots() {
        const _this = this;
        axios({
            method:'POST',
            url:'/getTrackedBots',
            })
                .then(function(response) {
                    _this.setState({trackedBots: response.data});
            })
                .catch(function (error) {
                    console.log(error);
        });
    }

    /**
     * Handles onChange for bot dropdown. Changes currently selected bot.
     */
    selectBot(event) {
        this.props.setCurrentBot(event.target.value);
    }

    /* starts data logging */
    startLogging(){
        console.log("logging data listener");
        axios({
            method:'POST',
            url:'/logdata',
            data: JSON.stringify({name: this.props.currentBot}),
            processData: false,
        })
        .then(function(response) {
            console.log('started logging data');
        })
        .catch(function (error) {
            console.warn(error);
        });
    }

    /* removes selected bot from list */
    removeBot(){
        console.log("remove bot listener");
        axios({
            method:'POST',
            url:'/removeBot',
            data: JSON.stringify({name: this.props.currentBot}),
        })
        .then(function(response) {
            console.log('removed bot successfully');
        })
        .catch(function (error) {
            console.warn(error);
        });
    }

    /* toggles xbox controls on or off */
    xboxToggle(checked){
        console.log('xboxToggle '+checked);
        if (checked){
            axios({
                method:'POST',
                url:'/runXbox',
                data: JSON.stringify({name: this.props.currentBot}),
            })
            .then(function(response) {
                console.log('successfully toggled Xbox ON');
            })
            .catch(function (error) {
                console.warn(error);
            });
        } else {
            axios({
                method:'POST',
                url:'/stopXbox',
                data: JSON.stringify({name: this.props.currentBot}),
            })
            .then(function(response) {
                console.log('successfully toggled Xbox OFF');
            })
            .catch(function (error) {
                console.warn(error);
            });
        }
    }

    updateDiscoveredBots(){
        //TODO (#32) - change below request to axios
        //        $.ajax({
        //            method: "POST",
        //            url: '/discoverBots',
        //            dataType: 'json',
        //            data: '',
        //            contentType: 'application/json',
        //            success: function (data) {
        //                 //Check if discovered_bots and data are the same (check length and then contents)
        //                if(data.length != discovered_bots.length){
        //                    //If not then clear list and re-make displayed elements
        //                    redoDiscoverList(data);
        //                }
        //                else{
        //                    //Check value to ensure both structures contain the same data
        //                    for(let x=0;x<data.length;x++){
        //                        if(data[x]!=discovered_bots[x]){
        //                            redoDiscoverList(data);
        //                            //Prevent the list from being remade constantly
        //                            break;
        //                        }
        //                    }
        //                }
        //                setTimeout(updateDiscoveredBots,3000); // Try again in 3 sec
        //            }
        //        });
    }

    redoDiscoverList(data){
        let new_bots = [];

        for (let i = 0; i < data.length; i++) {
            //Trim the forward-slash
            var ip_address = data[i].substring(1);
            new_bots.push(ip_address)
        }
        this.setState({discoveredBots: new_bots})
    }

    render(){
        return (

                Control Panel

Movement controls:

                                 Choose bot:
                                <select value={this.props.currentBot} onChange={this.selectBot}> id="botlist" name="bots">
                                    <option value="(DEBUG) Sim Bot">(DEBUG) Sim Bot</option>
                                    {
                                        this.state.trackedBots.map(function(botname, idx){
                                            return <option
                                                        key={idx}
                                                        value={botname}>
                                                   {botname}
                                                   </option>
                                        })
                                    }
                                </select>

                        <button className="btn btn-danger" id="removeBot" onClick={this.removeBot}>Remove Bot</button>

                        Power ({this.state.power}):
                        <input id="power" type="range" name="power" min="0" max="100" value={this.state.power} defaultValue="50" onChange={this.handleInputChange}/>

                        <button className="btn" id="ccw" onClick={this.sendKV}>turn CCW</button>
                        <button className="btn" id="fwd" onClick={this.sendKV}>forward</button>
                        <button className="btn" id="cw" onClick={this.sendKV}>turn CW</button>

                        <button className="btn" id="lft" onClick={this.sendKV}>left</button>
                        <button className="btn btn-danger" id="stop" onClick={this.sendKV}>STOP</button>
                        <button className="btn" id="rt" onClick={this.sendKV}>right</button>

                        <button className="btn btn-success" id="log" onClick={this.sendKV}>log data</button>
                        <button className="btn" id="bck" onClick={this.sendKV}>backward</button>

                            Keyboard Controls

                                <input name="keyboard" type="checkbox" id="keyboard-controls" onChange={this.handleInputChange} />

                            Xbox Controls

                                <input name="xbox" type="checkbox" id="xbox-controls" onChange={this.handleInputChange}/>

                        <input type="text" id="kv_key" placeholder="Key (e.g. WHEELS)"/>
                        <input type="text" id="kv_value" placeholder="Value (e.g. 10,10)"/>
                        <button id="sendkv" onClick={this.sendKV} className="btn btn-success">Send KV</button>

        )
    }
}

// $(document).ready(function() {
//     /*
//      * Event listener for key inputs. Sends to selected bot.
//      */
//     var lastKeyPressed;
//     window.onkeydown = function (e) {
//         let keyboardEnable = document.getElementById('keyboard-controls').checked;
//         if (!keyboardEnable) return;
//
//         let pow = getPower();
//         let code = e.keyCode ? e.keyCode : e.which;
//
//         if (code === lastKeyPressed) return;
//
//         if (code === 87) {
//             // w=forward
//             sendMotors(pow, pow, pow, pow);
//
//         } else if (code === 83) {
//             // s=backward
//             sendMotors(-pow, -pow, -pow, -pow);
//
//         } else if (code == 65) {
//             // a=ccw
//             sendMotors(-pow, pow, -pow, pow);
//
//         } else if (code == 68) {
//             // d=cw
//             sendMotors(pow, -pow, pow, -pow);
//
//         } else if (code == 81) {
//             // q=left
//             sendMotors(-pow, pow, pow, -pow);
//
//         } else if (code == 69) {
//             // e=right
//             sendMotors(pow, -pow, -pow, pow);
//         } else {
//             return;
//         }
//         lastKeyPressed = code;
//     };
//
//     window.onkeyup = function (e) {
//         let keyboardEnable = document.getElementById('keyboard-controls').checked;
//         if (!keyboardEnable) return;
//
//         let code = e.keyCode ? e.keyCode : e.which;
//
//         if (code === lastKeyPressed) {
//             // Stop
//             sendMotors(0,0,0,0);
//             lastKeyPressed = -1;
//         }
//     };
// });
//
// /*
// *   Send KV -- allows users to manually send key and value to bot (for debugging/testing
//     purposes)
// */
// function sendKV(){
//     $.ajax({
//         method:'POST',
//         url:'/sendKV',
//         dataType: 'json',
//         data: JSON.stringify({
//             key:$("#kv_key").val(),
//             value:$("#kv_value").val(),
//             name:getBotID()
//         }),
//         contentType: 'application/json'
//     });
// }
<|endoftext|>


<commit_before>// Global layer variables
var collisionsLayer, nearmissesLayer, hazardsLayer, theftsLayer;

// Create the a simple leaflet map centered around Victoria
function initializeCfaxMap(){
  // Map init and default layers
  map = L.map('map', {
      layers: [L.tileLayer('https://tiles1-b586b1453a9d82677351c34485e59108.skobblermaps.com/TileService/tiles/2.0/011011321101/10/{z}/{x}/{y}.png@2x', {
        minZoom: 2,
        attribution: '© Tiles: skobbler, Map data: OpenStreetMap contributors, CC-BY-SA'})
      ],
      worldCopyJump: true,
  });
};

// add data defined in data variable to data panel and to map as L.circleMarker
function addData(data){
  // Add geojson features to map
  collisionsLayer = geojsonCircleMarker( data.collisions , "collision").addTo(map);
  nearmissesLayer = geojsonCircleMarker( data.nearmisses , "nearmiss").addTo(map);
  hazardsLayer = geojsonCircleMarker( data.hazards , "hazard").addTo(map);
  theftsLayer = geojsonCircleMarker( data.thefts , "theft").addTo(map);
  rois = geojsonPolygonMarker(data.rois).addTo(map);

  // Fit map extent to alert areas boundary
  map.fitBounds(rois);

  // Add text to data column
  pprint("Collisions", data.collisions);
  pprint("Nearmisses", data.nearmisses);
  pprint("Hazards", data.hazards);
  pprint("Thefts", data.thefts);
}

// Highlight list item and highlight corresponding pnt on map
function highlightPoint(type, pk){
  getPointLayer(type).eachLayer(function(layer){
    if(layer.feature.id == pk){
      map.panTo(layer.getBounds().getCenter());
      layer.bringToFront();
      layer.setStyle({
          fillColor: '#0ff',
          fillOpacity: 1
      });
    }
  });
}

// Return a highlighted point to it's original color
function unhighlightPoint(type){
  getPointLayer(type).eachLayer(function(layer){
    layer.setStyle({
      fillColor: getColor(type),
      fillOpacity: 0.8
    });
  });
}

// Given a incident type, return the layer where points of that type are located
function getPointLayer(type){
  switch(type){
    case "collision": return collisionsLayer;
    case "nearmiss": return nearmissesLayer;
    case "hazard": return hazardsLayer;
    case "theft": return theftsLayer;
  }
}

// Print different point types in a nice formatted list TODO move to new js file
function pprint(title, jsonData){
  $('#data').append('
'+ title +'
');
  var str = "
";
  if(jsonData.features.length === 0){
    str += "No reports this week";
  }
  else{
    jsonData.features.forEach(function(obj) {
      if(title === "Collisions"){
        str += "
"
          + "Date: " + moment(obj.properties.incident_date).calendar() + "
"
          + "Type: " + obj.properties.incident + " (" + obj.properties.incident_with + ")
";
          if(obj.properties.incident_detail != ''){
            str+= "Description: " + obj.properties.incident_detail + "
";
          }
      }
      else if(title === "Nearmisses") {
        str += "
"
          + "Date: " + moment(obj.properties.incident_date).calendar() + "
"
          + "Type: " + obj.properties.incident + " (" + obj.properties.incident_with + ")
";
          if(obj.properties.incident_detail != ''){
            str+= "Description: " + obj.properties.incident_detail + "
";
          }
      }
      else if(title === "Hazards"){
        str += "
"
          + "Date: " + moment(obj.properties.hazard_date).calendar() + "
"
          + "Type: " + obj.properties.hazard + "
";
          if(obj.properties.hazard_detail != ''){
            str += "Description: " + obj.properties.hazard_detail + "
";
          };
      }
      else if(title === "Thefts"){
        str += "
"
          + "Date: " + moment(obj.properties.theft_date).calendar() + "
"
          + "Type: " + obj.properties.theft + "
";
        if(obj.properties.theft_detail != ''){
          str += "Description: " + obj.properties.theft_detail + "
";
        };
      }
      else{
        console.log("Unknown type error in pprint");
      }
    });
  }
  $('#data').append(str + "
");
};

// Listen for and handle hovering over list item in data pane
function listenForHover(){
  // Toggle data and point highlighting
  $('#data li').hover(function(){
    // Highlight list item
    $(this).toggleClass('highlight');
    // highlight corresponding point (call to cfax.js)
    highlightPoint($(this).attr('layer'), $(this).attr('pk'));
  });
  $('#data li').mouseleave(function(){
    // unhighlight all points (call to cfax.js)
    unhighlightPoint($(this).attr('layer'));
  });
}
<commit_msg>changed map tiles to mapquest<commit_after>// Global layer variables
var collisionsLayer, nearmissesLayer, hazardsLayer, theftsLayer;

// Create the a simple leaflet map centered around Victoria
function initializeCfaxMap(){
  // Map init and default layers
  map = L.map('map', {
      layers: [L.tileLayer('http://otile{s}.mqcdn.com/tiles/1.0.0/map/{z}/{x}/{y}.jpeg', {
        attribution: 'Tiles Courtesy of MapQuest — Map data © OpenStreetMap',
        subdomains: '1234'
      })],
      worldCopyJump: true,
  });
};

// add data defined in data variable to data panel and to map as L.circleMarker
function addData(data){
  // Add geojson features to map
  collisionsLayer = geojsonCircleMarker( data.collisions , "collision").addTo(map);
  nearmissesLayer = geojsonCircleMarker( data.nearmisses , "nearmiss").addTo(map);
  hazardsLayer = geojsonCircleMarker( data.hazards , "hazard").addTo(map);
  theftsLayer = geojsonCircleMarker( data.thefts , "theft").addTo(map);
  rois = geojsonPolygonMarker(data.rois).addTo(map);

  // Fit map extent to alert areas boundary
  map.fitBounds(rois);

  // Add text to data column
  pprint("Collisions", data.collisions);
  pprint("Nearmisses", data.nearmisses);
  pprint("Hazards", data.hazards);
  pprint("Thefts", data.thefts);
}

// Highlight list item and highlight corresponding pnt on map
function highlightPoint(type, pk){
  getPointLayer(type).eachLayer(function(layer){
    if(layer.feature.id == pk){
      map.panTo(layer.getBounds().getCenter());
      layer.bringToFront();
      layer.setStyle({
          fillColor: '#0ff',
          fillOpacity: 1
      });
    }
  });
}

// Return a highlighted point to it's original color
function unhighlightPoint(type){
  getPointLayer(type).eachLayer(function(layer){
    layer.setStyle({
      fillColor: getColor(type),
      fillOpacity: 0.8
    });
  });
}

// Given a incident type, return the layer where points of that type are located
function getPointLayer(type){
  switch(type){
    case "collision": return collisionsLayer;
    case "nearmiss": return nearmissesLayer;
    case "hazard": return hazardsLayer;
    case "theft": return theftsLayer;
  }
}

// Print different point types in a nice formatted list TODO move to new js file
function pprint(title, jsonData){
  $('#data').append('
'+ title +'
');
  var str = "
";
  if(jsonData.features.length === 0){
    str += "No reports this week";
  }
  else{
    jsonData.features.forEach(function(obj) {
      if(title === "Collisions"){
        str += "
"
          + "Date: " + moment(obj.properties.incident_date).calendar() + "
"
          + "Type: " + obj.properties.incident + " (" + obj.properties.incident_with + ")
";
          if(obj.properties.incident_detail != ''){
            str+= "Description: " + obj.properties.incident_detail + "
";
          }
      }
      else if(title === "Nearmisses") {
        str += "
"
          + "Date: " + moment(obj.properties.incident_date).calendar() + "
"
          + "Type: " + obj.properties.incident + " (" + obj.properties.incident_with + ")
";
          if(obj.properties.incident_detail != ''){
            str+= "Description: " + obj.properties.incident_detail + "
";
          }
      }
      else if(title === "Hazards"){
        str += "
"
          + "Date: " + moment(obj.properties.hazard_date).calendar() + "
"
          + "Type: " + obj.properties.hazard + "
";
          if(obj.properties.hazard_detail != ''){
            str += "Description: " + obj.properties.hazard_detail + "
";
          };
      }
      else if(title === "Thefts"){
        str += "
"
          + "Date: " + moment(obj.properties.theft_date).calendar() + "
"
          + "Type: " + obj.properties.theft + "
";
        if(obj.properties.theft_detail != ''){
          str += "Description: " + obj.properties.theft_detail + "
";
        };
      }
      else{
        console.log("Unknown type error in pprint");
      }
    });
  }
  $('#data').append(str + "
");
};

// Listen for and handle hovering over list item in data pane
function listenForHover(){
  // Toggle data and point highlighting
  $('#data li').hover(function(){
    // Highlight list item
    $(this).toggleClass('highlight');
    // highlight corresponding point (call to cfax.js)
    highlightPoint($(this).attr('layer'), $(this).attr('pk'));
  });
  $('#data li').mouseleave(function(){
    // unhighlight all points (call to cfax.js)
    unhighlightPoint($(this).attr('layer'));
  });
}
<|endoftext|>


<commit_before>janus.registerElement('avatar_simple', {
  headsrc: 'media/assets/hoverscreen.obj',
  bodysrc: '',
  handsrc: '',
  create() {
    if (this.headsrc) {
      this.head = this.createObject('object', {id: this.headsrc});
    }
    if (this.bodysrc) {
      this.body = this.createObject('object', {id: this.bodysrc});
    }
    if (this.handsrc) {
      this.hands = this.createObject('object', {id: this.handsrc});
    }
    //this.attachToPlayer();
  },
  attachToPlayer(player) {
    // THIS IS WHERE I STOPPED
    // I need to attach this avatar object to the player so it can do whatever it needs to position its sub-objects
    // In this case this is the simple head-and-hands avatar style, but it should be done in a way that can work
    // with more advanced types, like rigged models with IK
    player.addEventListener('xrframe', (ev) => this.handleXRFrame(ev));
  },
  //update(dt) {
  //},
  handleXRFrame(ev) {
    console.log('update it', ev.target, ev.data);
  }
});
janus.registerElement('avatar_rigged', {
  create() {
  },
  update(dt) {
  },
  updateXR(pose, referenceSpace) {
  }
});

elation.elements.define('janus-avatar-picker', class extends elation.elements.base {
  init() {
    this.defineAttribute('src', { type: 'string' });
    this.defineAttribute('previewpos', { type: 'vector3', default: V(-2, 0, -3) });
    this.defineAttribute('hidereset', { type: 'boolean', default: false });
    this.selected = false;
  }
  create() {
    let tpl = `
      <collection-jsonapi id="avatarlist" endpoint="${this.src}"></collection-jsonapi>
      <ui-list name="avatar" selectable="1" collection="avatarlist" itemcomponent="janus-avatar-picker-item"></ui-list>
      <ui-button name="confirm" disabled="1">Confirm</ui-button>
    `;
    if (!this.hidereset) {
      tpl += '<ui-button name="reset">Reset</ui-button>';
    }
    this.elements = elation.elements.fromString(tpl, this);

    elation.events.add(this.elements['avatar'], 'select', (ev) => this.handleAvatarSelect(ev));
    elation.events.add(this.elements['confirm'], 'click', (ev) => this.handleAvatarConfirm(ev));
    if (this.elements['reset']) {
      elation.events.add(this.elements['reset'], 'click', (ev) => this.handleAvatarReset(ev));
    }

    // Observer watches for visibility events and hides the 3d avatar preview when the 2d ui disappears
    let threshold = [];
    for (let i = 0; i < 100; i++) threshold.push(i / 100);
    let observer = new IntersectionObserver(ev => this.handleIntersectionChange(ev), { root: document, rootMargin: '0px', threshold: threshold });
    observer.observe(this);
    document.addEventListener('scroll', ev => this.handleScroll(ev));
  }
  selectAvatar(avatar) {
    let items = this.elements.avatarlist.items,
        list = this.elements.avatar;
    console.log('avatar?', items, list, avatar);
  }
  hidePreview(hidelight=false) {
    if (this.avatarpreview) {
      this.avatarpreview.die();
      this.avatarpreview = false;
    }
    if (this.avatarlight && hidelight) {
      this.avatarlight.die();
      this.avatarlight = false;
    }
  }
  showPreview(src) {
    if (!src) {
      src = 'data:text/plain,' + escape(player.getCurrentAvatarData());
    }
    this.hidePreview();
    this.avatarpreview = player.createObject('ghost', { avatar_src: src, pos: this.previewpos, rotate_deg_per_sec: 10, });
    if (!this.avatarlight) {
      this.avatarlight = player.createObject('light', {
        light_target: this.avatarpreview,
        light_cone_angle: .5,
        light_range: 4,
        light_decay: 1,
        col: 'white',
        pos: V(0, 3, 1)
      });
    }
    this.updatePreviewPosition();
  }
  updatePreviewPosition() {
    if (this.avatarpreview) {
      let rect = this.getBoundingClientRect();
      let middle = (rect.top + rect.bottom) / 2;
      let pos = middle / window.innerHeight;
      this.avatarpreview.pos.y = 0.5 - pos;
    }
  }
  handleAvatarSelect(ev) {
    console.log('selected an avatar', ev.data);
    this.selected = ev.data;
    this.showPreview(ev.data.url);
    this.elements.confirm.disabled = false;
  }
  handleAvatarConfirm(ev) {
    this.hidePreview(true);
    if (this.selected) {
      fetch(this.selected.url)
        .then(r => r.text())
        .then(t => {
          player.setAvatar(t);
          this.dispatchEvent(new CustomEvent('select', { detail: this.selected }));
        });
    }
  }
  handleAvatarReset(ev) {
    player.setAvatar(player.defaultAvatar);
  }
  handleIntersectionChange(intersections) {
    if (this.avatarpreview && !intersections[0].isIntersecting) {
      this.hidePreview(true);
    } else if (!this.avatarpreview && intersections[0].isIntersecting) {
      this.showPreview();
    }
  }
  handleScroll(ev) {
    this.updatePreviewPosition();
  }
});
elation.elements.define('janus-avatar-picker-item', class extends elation.elements.ui.item {
  create() {
    elation.events.add(this, 'click', (ev) => { console.log('duh', this); this.click(ev) });
    let item = this.value;
    this.elements = elation.elements.fromString(`

    `, this);
  }
});
<commit_msg>Clone vector when spawning avatar preview<commit_after>janus.registerElement('avatar_simple', {
  headsrc: 'media/assets/hoverscreen.obj',
  bodysrc: '',
  handsrc: '',
  create() {
    if (this.headsrc) {
      this.head = this.createObject('object', {id: this.headsrc});
    }
    if (this.bodysrc) {
      this.body = this.createObject('object', {id: this.bodysrc});
    }
    if (this.handsrc) {
      this.hands = this.createObject('object', {id: this.handsrc});
    }
    //this.attachToPlayer();
  },
  attachToPlayer(player) {
    // THIS IS WHERE I STOPPED
    // I need to attach this avatar object to the player so it can do whatever it needs to position its sub-objects
    // In this case this is the simple head-and-hands avatar style, but it should be done in a way that can work
    // with more advanced types, like rigged models with IK
    player.addEventListener('xrframe', (ev) => this.handleXRFrame(ev));
  },
  //update(dt) {
  //},
  handleXRFrame(ev) {
    console.log('update it', ev.target, ev.data);
  }
});
janus.registerElement('avatar_rigged', {
  create() {
  },
  update(dt) {
  },
  updateXR(pose, referenceSpace) {
  }
});

elation.elements.define('janus-avatar-picker', class extends elation.elements.base {
  init() {
    this.defineAttribute('src', { type: 'string' });
    this.defineAttribute('previewpos', { type: 'vector3', default: V(-2, 0, -3) });
    this.defineAttribute('hidereset', { type: 'boolean', default: false });
    this.selected = false;
  }
  create() {
    let tpl = `
      <collection-jsonapi id="avatarlist" endpoint="${this.src}"></collection-jsonapi>
      <ui-list name="avatar" selectable="1" collection="avatarlist" itemcomponent="janus-avatar-picker-item"></ui-list>
      <ui-button name="confirm" disabled="1">Confirm</ui-button>
    `;
    if (!this.hidereset) {
      tpl += '<ui-button name="reset">Reset</ui-button>';
    }
    this.elements = elation.elements.fromString(tpl, this);

    elation.events.add(this.elements['avatar'], 'select', (ev) => this.handleAvatarSelect(ev));
    elation.events.add(this.elements['confirm'], 'click', (ev) => this.handleAvatarConfirm(ev));
    if (this.elements['reset']) {
      elation.events.add(this.elements['reset'], 'click', (ev) => this.handleAvatarReset(ev));
    }

    // Observer watches for visibility events and hides the 3d avatar preview when the 2d ui disappears
    let threshold = [];
    for (let i = 0; i < 100; i++) threshold.push(i / 100);
    let observer = new IntersectionObserver(ev => this.handleIntersectionChange(ev), { root: document, rootMargin: '0px', threshold: threshold });
    observer.observe(this);
    document.addEventListener('scroll', ev => this.handleScroll(ev));
  }
  selectAvatar(avatar) {
    let items = this.elements.avatarlist.items,
        list = this.elements.avatar;
    console.log('avatar?', items, list, avatar);
  }
  hidePreview(hidelight=false) {
    if (this.avatarpreview) {
      this.avatarpreview.die();
      this.avatarpreview = false;
    }
    if (this.avatarlight && hidelight) {
      this.avatarlight.die();
      this.avatarlight = false;
    }
  }
  showPreview(src) {
    if (!src) {
      src = 'data:text/plain,' + escape(player.getCurrentAvatarData());
    }
    this.hidePreview();
    this.avatarpreview = player.createObject('ghost', { avatar_src: src, pos: V(this.previewpos), rotate_deg_per_sec: 10, });
    if (!this.avatarlight) {
      this.avatarlight = player.createObject('light', {
        light_target: this.avatarpreview,
        light_cone_angle: .5,
        light_range: 4,
        light_decay: 1,
        col: 'white',
        pos: V(0, 3, 1)
      });
    }
    this.updatePreviewPosition();
  }
  updatePreviewPosition() {
    if (this.avatarpreview) {
      let rect = this.getBoundingClientRect();
      let middle = (rect.top + rect.bottom) / 2;
      let pos = middle / window.innerHeight;
      this.avatarpreview.pos.y = 0.5 - pos;
    }
  }
  handleAvatarSelect(ev) {
    console.log('selected an avatar', ev.data);
    this.selected = ev.data;
    this.showPreview(ev.data.url);
    this.elements.confirm.disabled = false;
  }
  handleAvatarConfirm(ev) {
    this.hidePreview(true);
    if (this.selected) {
      fetch(this.selected.url)
        .then(r => r.text())
        .then(t => {
          player.setAvatar(t);
          this.dispatchEvent(new CustomEvent('select', { detail: this.selected }));
        });
    }
  }
  handleAvatarReset(ev) {
    player.setAvatar(player.defaultAvatar);
  }
  handleIntersectionChange(intersections) {
    if (this.avatarpreview && !intersections[0].isIntersecting) {
      this.hidePreview(true);
    } else if (!this.avatarpreview && intersections[0].isIntersecting) {
      this.showPreview();
    }
  }
  handleScroll(ev) {
    this.updatePreviewPosition();
  }
});
elation.elements.define('janus-avatar-picker-item', class extends elation.elements.ui.item {
  create() {
    elation.events.add(this, 'click', (ev) => { console.log('duh', this); this.click(ev) });
    let item = this.value;
    this.elements = elation.elements.fromString(`

    `, this);
  }
});
<|endoftext|>


<commit_before>import timeSeriesToDygraph from 'src/utils/timeSeriesToDygraph';
import {STROKE_WIDTH} from 'src/shared/constants';

const {light: strokeWidth} = STROKE_WIDTH;

describe('timeSeriesToDygraph', () => {
  it('parses a raw InfluxDB response into a dygraph friendly data format', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                  "tags": {
                    tk1: "tv1",
                    tk2: "tv2",
                  },
                },
              ]
            },
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                  "tags": {
                    tk3: "tv3",
                  },
                },
              ]
            },
          ],
        },
      }
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        `m1.f1[tk1=tv1][tk2=tv2]`,
        `m1.f2[tk3=tv3]`,
      ],
      timeSeries: [
        [new Date(1000), 1, null],
        [new Date(2000), 2, 3],
        [new Date(4000), null, 4],
      ],
      dygraphSeries: {
        'm1.f1[tk1=tv1][tk2=tv2]': {
          axis: 'y',
          strokeWidth,
        },
        'm1.f2[tk3=tv3]': {
          axis: 'y',
          strokeWidth,
        },
      },
    };

    expect(actual).to.deep.equal(expected);
  });

  it('can sort numerical timestamps correctly', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[100, 1],[3000, 3],[200, 2]],
                },
              ]
            },
          ],
        },
      }
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        'm1.f1',
      ],
      timeSeries: [
        [new Date(100), 1],
        [new Date(200), 2],
        [new Date(3000), 3],
      ],
    };

    expect(actual.timeSeries).to.deep.equal(expected.timeSeries);
  });

  it('can parse multiple responses into two axes', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m3",
                  "columns": ["time","f3"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      'm1.f1': {
        axis: 'y',
        strokeWidth,
      },
      'm1.f2': {
        axis: 'y',
        strokeWidth,
      },
      'm3.f3': {
        axis: 'y2',
        strokeWidth,
      },
    };

    // console.log('BEAP EXPECTED', JSON.stringify(expected, null, 2))

    expect(actual.dygraphSeries).to.deep.equal(expected);
  });

  it('can parse multiple responses with the same field and measurement', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
    ];

    const actual = timeSeriesToDygraph(influxResponse);
    //  dygraphSeries: {
    //    'm1.f1': {
    //      axis: 'y',
    //      strokeWidth,
    //    },
    //    'm1.f1-1': {
    //      axis: 'y2',
    //      strokeWidth,
    //    },
    //  },

    const expected = {
      labels: [
        'time',
        `m1.f1`,
        `m1.f1`,
      ],
      timeSeries: [
        [new Date(1000), 1, null],
        [new Date(2000), 2, 3],
        [new Date(4000), null, 4],
      ],
      dygraphSeries: {
        'm1.f1': {
          axis: 'y',
          strokeWidth,
        },
        'm1.f1': {
          axis: 'y2',
          strokeWidth,
        },
      },
    };

    expect(actual).to.deep.equal(expected);
  });

  it('it does not use multiple axes if being used for the DataExplorer', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
    ];

    const isInDataExplorer = true;
    const actual = timeSeriesToDygraph(influxResponse, undefined, isInDataExplorer);

    const expected = {
      'm1.f1': {
        strokeWidth,
      },
      'm1.f2': {
        strokeWidth,
      },
    };

    expect(actual.dygraphSeries).to.deep.equal(expected);
  });

  it('it highlights the appropriate response', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m2",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
    ];

    const highlightIndex = 1;
    const actual = timeSeriesToDygraph(influxResponse, highlightIndex);
    const {dygraphSeries} = actual;

    expect(dygraphSeries["m2.f2"].strokeWidth).to.be.above(dygraphSeries["m1.f1"].strokeWidth);
  });

  it('parses labels alphabetically with the correct field values for multiple series', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"mb",
                  "columns": ["time","fa"],
                  "values": [
                    [1000, 200],
                    [2000, 300],
                    [4000, 400],
                  ],
                },
                {
                  "name":"mc",
                  "columns": ["time","fa"],
                  "values": [
                    [1000, 400],
                    [2000, 600],
                    [3000, 800],
                    [5000, 1000],
                  ],
                },
              ]
            },
            {
              "series": [
                {
                  "name":"ma",
                  "columns": ["time","fa","fc","fb"],
                  "values": [
                    [1000, 20, 10, 10],
                    [2000, 30, 15, 9],
                    [3000, 40, 20, 8],
                  ],
                },
              ]
            },
          ],
        },
      }
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        `ma.fa`,
        `ma.fb`,
        `ma.fc`,
        `mb.fa`,
        `mc.fa`,
      ],
      timeSeries: [
        [new Date(1000), 20, 10, 10, 200, 400],
        [new Date(2000), 30, 9, 15, 300, 600],
        [new Date(3000), 40, 8, 20, null, 800],
        [new Date(4000), null, null, null, 400, null],
        [new Date(5000), null, null, null, null, 1000],
      ],
    };

    // console.log(actual.timeSeries);
    expect(actual.labels).to.deep.equal(expected.labels);
    expect(actual.timeSeries).to.deep.equal(expected.timeSeries);
  });
});
<commit_msg>Cleanup comments<commit_after>import timeSeriesToDygraph from 'src/utils/timeSeriesToDygraph';
import {STROKE_WIDTH} from 'src/shared/constants';

const {light: strokeWidth} = STROKE_WIDTH;

describe('timeSeriesToDygraph', () => {
  it('parses a raw InfluxDB response into a dygraph friendly data format', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                  "tags": {
                    tk1: "tv1",
                    tk2: "tv2",
                  },
                },
              ]
            },
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                  "tags": {
                    tk3: "tv3",
                  },
                },
              ]
            },
          ],
        },
      }
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        `m1.f1[tk1=tv1][tk2=tv2]`,
        `m1.f2[tk3=tv3]`,
      ],
      timeSeries: [
        [new Date(1000), 1, null],
        [new Date(2000), 2, 3],
        [new Date(4000), null, 4],
      ],
      dygraphSeries: {
        'm1.f1[tk1=tv1][tk2=tv2]': {
          axis: 'y',
          strokeWidth,
        },
        'm1.f2[tk3=tv3]': {
          axis: 'y',
          strokeWidth,
        },
      },
    };

    expect(actual).to.deep.equal(expected);
  });

  it('can sort numerical timestamps correctly', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[100, 1],[3000, 3],[200, 2]],
                },
              ]
            },
          ],
        },
      }
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        'm1.f1',
      ],
      timeSeries: [
        [new Date(100), 1],
        [new Date(200), 2],
        [new Date(3000), 3],
      ],
    };

    expect(actual.timeSeries).to.deep.equal(expected.timeSeries);
  });

  it('can parse multiple responses into two axes', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m3",
                  "columns": ["time","f3"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      'm1.f1': {
        axis: 'y',
        strokeWidth,
      },
      'm1.f2': {
        axis: 'y',
        strokeWidth,
      },
      'm3.f3': {
        axis: 'y2',
        strokeWidth,
      },
    };

    expect(actual.dygraphSeries).to.deep.equal(expected);
  });

  it('can parse multiple responses with the same field and measurement', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        `m1.f1`,
        `m1.f1`,
      ],
      timeSeries: [
        [new Date(1000), 1, null],
        [new Date(2000), 2, 3],
        [new Date(4000), null, 4],
      ],
      dygraphSeries: {
        'm1.f1': {
          axis: 'y',
          strokeWidth,
        },
        'm1.f1': {
          axis: 'y2',
          strokeWidth,
        },
      },
    };

    expect(actual).to.deep.equal(expected);
  });

  it('it does not use multiple axes if being used for the DataExplorer', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
    ];

    const isInDataExplorer = true;
    const actual = timeSeriesToDygraph(influxResponse, undefined, isInDataExplorer);

    const expected = {
      'm1.f1': {
        strokeWidth,
      },
      'm1.f2': {
        strokeWidth,
      },
    };

    expect(actual.dygraphSeries).to.deep.equal(expected);
  });

  it('it highlights the appropriate response', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m1",
                  "columns": ["time","f1"],
                  "values": [[1000, 1],[2000, 2]],
                },
              ]
            },
          ],
        },
      },
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"m2",
                  "columns": ["time","f2"],
                  "values": [[2000, 3],[4000, 4]],
                },
              ]
            },
          ],
        },
      },
    ];

    const highlightIndex = 1;
    const actual = timeSeriesToDygraph(influxResponse, highlightIndex);
    const {dygraphSeries} = actual;

    expect(dygraphSeries["m2.f2"].strokeWidth).to.be.above(dygraphSeries["m1.f1"].strokeWidth);
  });

  it('parses labels alphabetically with the correct field values for multiple series', () => {
    const influxResponse = [
      {
        "response":
        {
          "results": [
            {
              "series": [
                {
                  "name":"mb",
                  "columns": ["time","fa"],
                  "values": [
                    [1000, 200],
                    [2000, 300],
                    [4000, 400],
                  ],
                },
                {
                  "name":"mc",
                  "columns": ["time","fa"],
                  "values": [
                    [1000, 400],
                    [2000, 600],
                    [3000, 800],
                    [5000, 1000],
                  ],
                },
              ]
            },
            {
              "series": [
                {
                  "name":"ma",
                  "columns": ["time","fa","fc","fb"],
                  "values": [
                    [1000, 20, 10, 10],
                    [2000, 30, 15, 9],
                    [3000, 40, 20, 8],
                  ],
                },
              ]
            },
          ],
        },
      }
    ];

    const actual = timeSeriesToDygraph(influxResponse);

    const expected = {
      labels: [
        'time',
        `ma.fa`,
        `ma.fb`,
        `ma.fc`,
        `mb.fa`,
        `mc.fa`,
      ],
      timeSeries: [
        [new Date(1000), 20, 10, 10, 200, 400],
        [new Date(2000), 30, 9, 15, 300, 600],
        [new Date(3000), 40, 8, 20, null, 800],
        [new Date(4000), null, null, null, 400, null],
        [new Date(5000), null, null, null, null, 1000],
      ],
    };

    expect(actual.labels).to.deep.equal(expected.labels);
    expect(actual.timeSeries).to.deep.equal(expected.timeSeries);
  });
});
<|endoftext|>


<commit_before>function boomChunkAssetEditor(pageId, slotname, visibleElements) {
	this.pageId = pageId;
	this.slotname = slotname;
	this.visibleElements = visibleElements;

	boomChunkAssetEditor.prototype.bind = function() {
		var chunkAssetEditor = this;

		this.asset.on('click', function() {
			new boomAssetPicker(chunkAssetEditor.asset.attr("data-asset-id"))
				.done(function(assetId) {
					chunkAssetEditor.setAsset(assetId);
				});
		});

		this.link.on('focus', 'input', function() {
			var $this = $(this);

			new boomLinkPicker(new boomLink($this.val()))
				.done(function(link) {
					chunkAssetEditor.setLink(link.getUrl());
				});
		});
	};

	boomChunkAssetEditor.prototype.dialogOpened = function() {
		this.title = this.dialog.contents.find('.b-title');
		this.caption = this.dialog.contents.find('.b-caption');
		this.link = this.dialog.contents.find('.b-link');
		this.asset = this.dialog.contents.find('a');

		this.bind();
		this.toggleElements();
	};

	boomChunkAssetEditor.prototype.getData = function() {
		return {
			asset_id : this.asset.attr('data-asset-id'),
			caption : this.caption.find('textarea').val(),
			url : this.link.find('input').val(),
			title : this.title.find('input').val()
		};
	};

	boomChunkAssetEditor.prototype.open = function() {
		var chunkAssetEditor = this;
		this.deferred = new $.Deferred();

		this.dialog = new boomDialog({
			url : '/cms/chunk/asset/edit/' + this.pageId + '?slotname=' + this.slotname,
			id : 'b-assets-chunk-editor',
			open : function() {
				chunkAssetEditor.dialogOpened();
			}
		})
		.done(function() {
			chunkAssetEditor.deferred.resolve(chunkAssetEditor.getData());
		 })
		.fail(function() {
			chunkAssetEditor.deferred.reject();
		 });

		return this.deferred;
	};

	boomChunkAssetEditor.prototype.setAsset = function(assetId) {
		this.asset.attr('data-asset-id', assetId);

		var $img = this.asset.find('img');

		if ( ! $img.length) {
			$img = $('<img />');
			this.asset.find('p').replaceWith($img);
		}

		$img.attr('src', '/asset/view/' + assetId + '/400');
	};

	boomChunkAssetEditor.prototype.setLink = function(link) {
		this.link
			.find('input')
			.val(link)
			.blur();
	};

	boomChunkAssetEditor.prototype.toggleElements = function() {
		var elements = ['title', 'caption', 'link'],
			i,
			element;

		for (i = 0; i < elements.length; i++) {
			element = elements[i];

			if ( ! this.visibleElements[element]) {
				this[element].hide();
			}
		}
	};

	return this.open();
};<commit_msg>Bugfix: asset chunk title not saving<commit_after>function boomChunkAssetEditor(pageId, slotname, visibleElements) {
	this.pageId = pageId;
	this.slotname = slotname;
	this.visibleElements = visibleElements;

	boomChunkAssetEditor.prototype.bind = function() {
		var chunkAssetEditor = this;

		this.asset.on('click', function() {
			new boomAssetPicker(chunkAssetEditor.asset.attr("data-asset-id"))
				.done(function(assetId) {
					chunkAssetEditor.setAsset(assetId);
				});
		});

		this.link.on('focus', 'input', function() {
			var $this = $(this);

			new boomLinkPicker(new boomLink($this.val()))
				.done(function(link) {
					chunkAssetEditor.setLink(link.getUrl());
				});
		});
	};

	boomChunkAssetEditor.prototype.dialogOpened = function() {
		this.title = this.dialog.contents.find('.b-title');
		this.caption = this.dialog.contents.find('.b-caption');
		this.link = this.dialog.contents.find('.b-link');
		this.asset = this.dialog.contents.find('a');

		this.bind();
		this.toggleElements();
	};

	boomChunkAssetEditor.prototype.getData = function() {
		return {
			asset_id : this.asset.attr('data-asset-id'),
			caption : this.caption.find('textarea').val(),
			url : this.link.find('input').val(),
			title : this.title.find('textarea').val()
		};
	};

	boomChunkAssetEditor.prototype.open = function() {
		var chunkAssetEditor = this;
		this.deferred = new $.Deferred();

		this.dialog = new boomDialog({
			url : '/cms/chunk/asset/edit/' + this.pageId + '?slotname=' + this.slotname,
			id : 'b-assets-chunk-editor',
			open : function() {
				chunkAssetEditor.dialogOpened();
			}
		})
		.done(function() {
			chunkAssetEditor.deferred.resolve(chunkAssetEditor.getData());
		 })
		.fail(function() {
			chunkAssetEditor.deferred.reject();
		 });

		return this.deferred;
	};

	boomChunkAssetEditor.prototype.setAsset = function(assetId) {
		this.asset.attr('data-asset-id', assetId);

		var $img = this.asset.find('img');

		if ( ! $img.length) {
			$img = $('<img />');
			this.asset.find('p').replaceWith($img);
		}

		$img.attr('src', '/asset/view/' + assetId + '/400');
	};

	boomChunkAssetEditor.prototype.setLink = function(link) {
		this.link
			.find('input')
			.val(link)
			.blur();
	};

	boomChunkAssetEditor.prototype.toggleElements = function() {
		var elements = ['title', 'caption', 'link'],
			i,
			element;

		for (i = 0; i < elements.length; i++) {
			element = elements[i];

			if ( ! this.visibleElements[element]) {
				this[element].hide();
			}
		}
	};

	return this.open();
};<|endoftext|>


<commit_before>import React, {PropTypes} from 'react'
import DatabaseRow from 'src/admin/components/DatabaseRow'
import ConfirmButtons from 'src/admin/components/ConfirmButtons'

const {
  func,
  shape,
} = PropTypes

const DatabaseTable = ({
  database,
  onEditDatabase,
  onKeyDownDatabase,
  onCancelDatabase,
  onConfirmDatabase,
  onStartDeleteDatabase,
  onDatabaseDeleteConfirm,
  onAddRetentionPolicy,
  onEditRetentionPolicy,
  onStopEditRetentionPolicy,
  onKeyDownRetentionPolicy,
  onCancelRetentionPolicy,
  onCreateRetentionPolicy,
}) => {
  return (

      <DatabaseTableHeader
        database={database}
        onEdit={onEditDatabase}
        onKeyDown={onKeyDownDatabase}
        onCancel={onCancelDatabase}
        onConfirm={onConfirmDatabase}
        onStartDelete={onStartDeleteDatabase}
        onDatabaseDeleteConfirm={onDatabaseDeleteConfirm}
        onAddRetentionPolicy={onAddRetentionPolicy}
      />

              Retention Policy
              Duration
              Replication Factor

            {
              database.retentionPolicies.map(rp => {
                return (
                  <DatabaseRow
                    key={rp.id}
                    database={database}
                    retentionPolicy={rp}
                    onEdit={onEditRetentionPolicy}
                    onKeyDown={onKeyDownRetentionPolicy}
                    onCancel={onCancelRetentionPolicy}
                    onConfirm={onCreateRetentionPolicy}
                    onStopEdit={onStopEditRetentionPolicy}
                    handleClickOutside={() => onStopEditRetentionPolicy({database, retentionPolicy: rp})}
                  />
                )
              })
            }

  )
}

DatabaseTable.propTypes = {
  onEditDatabase: func,
  database: shape(),
  onKeyDownDatabase: func,
  onCancelDatabase: func,
  onConfirmDatabase: func,
  onStartDeleteDatabase: func,
  onDatabaseDeleteConfirm: func,
  onAddRetentionPolicy: func,
  onEditRetentionPolicy: func,
  onStopEditRetentionPolicy: func,
  onKeyDownRetentionPolicy: func,
  onCancelRetentionPolicy: func,
  onCreateRetentionPolicy: func,
}

const DatabaseTableHeader = ({
  database,
  onEdit,
  onKeyDown,
  onConfirm,
  onCancel,
  onStartDelete,
  onDatabaseDeleteConfirm,
  onAddRetentionPolicy,
}) => {
  if (database.isEditing) {
    return (
      <EditHeader
        database={database}
        onEdit={onEdit}
        onKeyDown={onKeyDown}
        onConfirm={onConfirm}
        onCancel={onCancel}
      />
    )
  }

  return (

  )
}

DatabaseTableHeader.propTypes = {
  onEdit: func,
  database: shape(),
  onKeyDown: func,
  onCancel: func,
  onConfirm: func,
  onStartDelete: func,
  onDatabaseDeleteConfirm: func,
  onAddRetentionPolicy: func,
}

const Header = ({
  database,
  onStartDelete,
  onDatabaseDeleteConfirm,
  onAddRetentionPolicy,
}) => {
  const confirmStyle = {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
  }

  const buttons = (

      <button className="btn btn-xs btn-danger" onClick={() => onStartDelete(database)}>
        Delete
      </button>
      <button className="btn btn-xs btn-primary" onClick={() => onAddRetentionPolicy(database)}>
        Add retention policy
      </button>

  )

  const deleteConfirm = (

        <input
          className="form-control"
          name="name"
          type="text"
          value={database.deleteCode || ''}
          placeholder="type DELETE to confirm"
          onChange={(e) => onDatabaseDeleteConfirm(database, e)}
          onKeyDown={(e) => onDatabaseDeleteConfirm(database, e)}
          autoFocus={true}
        />

      <ConfirmButtons item={database} onConfirm={() => {}} onCancel={() => {}} />

  )

  return (

{database.name}

      {database.hasOwnProperty('deleteCode') ? deleteConfirm : buttons}

  )
}

Header.propTypes = {
  database: shape(),
  onStartDelete: func,
  onDatabaseDeleteConfirm: func,
  onAddRetentionPolicy: func,
}

const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (

        <input
          className="form-control"
          name="name"
          type="text"
          value={database.name}
          placeholder="database name"
          onChange={(e) => onEdit(e.target.value, database)}
          onKeyDown={(e) => onKeyDown(e, database)}
          autoFocus={true}
        />

    <ConfirmButtons item={database} onConfirm={onConfirm} onCancel={onCancel} />

)

EditHeader.propTypes = {
  database: shape(),
  onEdit: func,
  onKeyDown: func,
  onCancel: func,
  onConfirm: func,
}

export default DatabaseTable
<commit_msg>Handle new rp on click outside<commit_after>import React, {PropTypes} from 'react'
import DatabaseRow from 'src/admin/components/DatabaseRow'
import ConfirmButtons from 'src/admin/components/ConfirmButtons'

const {
  func,
  shape,
} = PropTypes

const DatabaseTable = ({
  database,
  onEditDatabase,
  onKeyDownDatabase,
  onCancelDatabase,
  onConfirmDatabase,
  onStartDeleteDatabase,
  onDatabaseDeleteConfirm,
  onAddRetentionPolicy,
  onEditRetentionPolicy,
  onStopEditRetentionPolicy,
  onKeyDownRetentionPolicy,
  onCancelRetentionPolicy,
  onCreateRetentionPolicy,
}) => {
  return (

      <DatabaseTableHeader
        database={database}
        onEdit={onEditDatabase}
        onKeyDown={onKeyDownDatabase}
        onCancel={onCancelDatabase}
        onConfirm={onConfirmDatabase}
        onStartDelete={onStartDeleteDatabase}
        onDatabaseDeleteConfirm={onDatabaseDeleteConfirm}
        onAddRetentionPolicy={onAddRetentionPolicy}
      />

              Retention Policy
              Duration
              Replication Factor

            {
              database.retentionPolicies.map(rp => {
                return (
                  <DatabaseRow
                    key={rp.id}
                    database={database}
                    retentionPolicy={rp}
                    onEdit={onEditRetentionPolicy}
                    onKeyDown={onKeyDownRetentionPolicy}
                    onCancel={onCancelRetentionPolicy}
                    onConfirm={onCreateRetentionPolicy}
                    onStopEdit={onStopEditRetentionPolicy}
                    handleClickOutside={() => rp.isNew ? onCancelRetentionPolicy({database, retentionPolicy: rp}) : onStopEditRetentionPolicy({database, retentionPolicy: rp})}
                  />
                )
              })
            }

  )
}

DatabaseTable.propTypes = {
  onEditDatabase: func,
  database: shape(),
  onKeyDownDatabase: func,
  onCancelDatabase: func,
  onConfirmDatabase: func,
  onStartDeleteDatabase: func,
  onDatabaseDeleteConfirm: func,
  onAddRetentionPolicy: func,
  onEditRetentionPolicy: func,
  onStopEditRetentionPolicy: func,
  onKeyDownRetentionPolicy: func,
  onCancelRetentionPolicy: func,
  onCreateRetentionPolicy: func,
}

const DatabaseTableHeader = ({
  database,
  onEdit,
  onKeyDown,
  onConfirm,
  onCancel,
  onStartDelete,
  onDatabaseDeleteConfirm,
  onAddRetentionPolicy,
}) => {
  if (database.isEditing) {
    return (
      <EditHeader
        database={database}
        onEdit={onEdit}
        onKeyDown={onKeyDown}
        onConfirm={onConfirm}
        onCancel={onCancel}
      />
    )
  }

  return (

  )
}

DatabaseTableHeader.propTypes = {
  onEdit: func,
  database: shape(),
  onKeyDown: func,
  onCancel: func,
  onConfirm: func,
  onStartDelete: func,
  onDatabaseDeleteConfirm: func,
  onAddRetentionPolicy: func,
}

const Header = ({
  database,
  onStartDelete,
  onDatabaseDeleteConfirm,
  onAddRetentionPolicy,
}) => {
  const confirmStyle = {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
  }

  const buttons = (

      <button className="btn btn-xs btn-danger" onClick={() => onStartDelete(database)}>
        Delete
      </button>
      <button className="btn btn-xs btn-primary" onClick={() => onAddRetentionPolicy(database)}>
        Add retention policy
      </button>

  )

  const deleteConfirm = (

        <input
          className="form-control"
          name="name"
          type="text"
          value={database.deleteCode || ''}
          placeholder="type DELETE to confirm"
          onChange={(e) => onDatabaseDeleteConfirm(database, e)}
          onKeyDown={(e) => onDatabaseDeleteConfirm(database, e)}
          autoFocus={true}
        />

      <ConfirmButtons item={database} onConfirm={() => {}} onCancel={() => {}} />

  )

  return (

{database.name}

      {database.hasOwnProperty('deleteCode') ? deleteConfirm : buttons}

  )
}

Header.propTypes = {
  database: shape(),
  onStartDelete: func,
  onDatabaseDeleteConfirm: func,
  onAddRetentionPolicy: func,
}

const EditHeader = ({database, onEdit, onKeyDown, onConfirm, onCancel}) => (

        <input
          className="form-control"
          name="name"
          type="text"
          value={database.name}
          placeholder="database name"
          onChange={(e) => onEdit(e.target.value, database)}
          onKeyDown={(e) => onKeyDown(e, database)}
          autoFocus={true}
        />

    <ConfirmButtons item={database} onConfirm={onConfirm} onCancel={onCancel} />

)

EditHeader.propTypes = {
  database: shape(),
  onEdit: func,
  onKeyDown: func,
  onCancel: func,
  onConfirm: func,
}

export default DatabaseTable
<|endoftext|>


<commit_before>var Datagrid = Backbone.View.extend({
  initialize: function() {
    this.columns = this.options.columns;
    this.options = _.defaults(this.options, {
      paginated:      false,
      page:           1,
      perPage:        10,
      tableClassName: 'table'
    });

    this.collection.on('reset', this.render, this);
    this._prepare();
  },

  render: function() {
    this.$el.empty();
    this.renderTable();
    if (this.options.paginated) {
      this.renderPagination();
    }

    return this;
  },

  renderTable: function() {
    var $table = $('

', {'class': this.options.tableClassName});
    this.$el.append($table);

    var header = new Header({columns: this.columns, sorter: this.sorter});
    $table.append(header.render().el);

    $table.append('

');

    this.collection.forEach(this.renderRow, this);
  },

  renderPagination: function() {
    var pagination = new Pagination({pager: this.pager});
    this.$el.append(pagination.render().el);
  },

  renderRow: function(model) {
    var options = {
      model: model,
      columns: this.columns
    };
    var rowClassName = this.options.rowClassName;
    if (_.isFunction(rowClassName)) {
      rowClassName = rowClassName(model);
    }
    options.className = rowClassName;

    var row = new Row(options);
    this.$('tbody').append(row.render(this.columns).el);
  },

  refresh: function() {
    if (this.options.inMemory) {
      this.collection.trigger('reset', this.collection);
    } else {
      this._request();
    }
  },

  sort: function(column, order) {
    this.sorter.sort(column, order);
  },

  page: function(page) {
    this.pager.page(page);
  },

  perPage: function(perPage) {
    this.pager.set('perPage', perPage);
  },

  _sort: function() {
    if (this.options.inMemory) {
      this._sortInMemory();
    } else {
      this._sortRequest();
    }
  },

  _sortInMemory: function() {
    if (this.options.paginated) {
      this._originalCollection.comparator = _.bind(this._comparator, this);
      this._originalCollection.sort();
      this.page(1);
    } else {
      this.collection.comparator = _.bind(this._comparator, this);
      this.collection.sort();
    }
  },

  _comparator: function(model1, model2) {
    var columnComparator = this._comparatorForColumn(this.sorter.get('column'));
    var order = columnComparator(model1, model2);
    return this.sorter.sortedASC() ? order : -order;
  },

  _comparatorForColumn: function(column) {
    var c = _.find(this.columns, function(c) {
      return c.property === column || c.index === column;
    });
    return c ? c.comparator : undefined;
  },

  _sortRequest: function() {
    this._request();
  },

  _page: function(options) {
    if (this.options.inMemory) {
      this._pageInMemory(options);
    } else {
      this._pageRequest(options);
    }
  },

  _pageRequest: function(options) {
    this._request(options);
  },

  _request: function(options) {
    options     = options || {};
    var success = options.success;
    var silent  = options.silent;

    options.data = this._getRequestData();
    options.success = _.bind(function(collection) {
      if (success) {
        success();
      }
      if (this.options.paginated) {
        this.pager.update(collection);
      }
      if (!silent) {
        collection.trigger('reset', collection);
      }
    }, this);
    options.silent = true;

    this.collection.fetch(options);
  },

  _getRequestData: function() {
    if (this.collection.data && _.isFunction(this.collection.data)) {
      return this.collection.data(this.pager, this.sorter);
    } else if (this.collection.data && typeof this.collection.data === 'object') {
      var data = {};
      _.each(this.collection.data, function(value, param) {
        if (_.isFunction(value)) {
          value = value(this.pager, this.sorter);
        }
        data[param] = value;
      }, this);
      return data;
    } else if (this.options.paginated) {
      return {
        page:     this.pager.get('currentPage'),
        per_page: this.pager.get('perPage')
      };
    }

    return {};
  },

  _pageInMemory: function(options) {
    if (!this._originalCollection) {
      this._originalCollection = this.collection.clone();
    }

    var page    = this.pager.get('currentPage');
    var perPage = this.pager.get('perPage');

    var begin = (page - 1) * perPage;
    var end   = begin + perPage;

    if (options && options.success) {
      options.success();
    }
    this.pager.set('total', this._originalCollection.size());

    this.collection.reset(this._originalCollection.slice(begin, end), options);
  },

  _prepare: function() {
    this._prepareSorter();
    if (this.options.paginated) {
      this._preparePager();
      this._page({
        success: _.bind(this._prepareColumns, this)
      });
    } else {
      this._prepareColumns();
      this.refresh();
    }
  },

  _prepareSorter: function() {
    this.sorter = new Sorter();
    this.sorter.on('change', function() {
      this._sort(this.sorter.get('column'), this.sorter.get('order'));
    }, this);
  },

  _preparePager: function() {
    this.pager = new Pager({
      currentPage: this.options.page,
      perPage:     this.options.perPage
    });

    this.pager.on('change:currentPage', this._page, this);
    this.pager.on('change:perPage', function() {
      this.page(1);
    }, this);
  },

  _prepareColumns: function() {
    if (!this.columns || _.isEmpty(this.columns)) {
      this._defaultColumns();
    } else {
      _.each(this.columns, function(column, i) {
        this.columns[i] = this._prepareColumn(column, i);
      }, this);
    }
  },

  _prepareColumn: function(column, index) {
    if (_.isString(column)) {
      column = { property: column };
    }
    if (_.isObject(column)) {
      column.index = index;
      if (column.property) {
        column.title = column.title || this._formatTitle(column.property);
      } else if (!column.property && !column.view) {
        throw new Error('Column \'' + column.title + '\' has no property and must accordingly define a custom cell view.');
      }
      if (this.options.inMemory && column.sortable) {
        if (!column.comparator && !column.property && !column.sortedProperty) {
          throw new Error('Invalid column definition: a sortable column must have a comparator, property or sortedProperty defined.');
        }
        column.comparator = column.comparator || this._defaultComparator(column.sortedProperty || column.property);
      }
    }
    return column;
  },

  _formatTitle: function(title) {
    return _.map(title.split(/\s|_/), function(word) {
      return word.charAt(0).toUpperCase() + word.substr(1);
    }).join(' ');
  },

  _defaultColumns: function() {
    this.columns = [];
    var model = this.collection.first(), i = 0;
    if (model) {
      for (var p in model.toJSON()) {
        this.columns.push(this._prepareColumn(p, i++));
      }
    }
  },

  _defaultComparator: function(column) {
    return function(model1, model2) {
      var val1 = model1.has(column) ? model1.get(column) : '';
      var val2 = model2.has(column) ? model2.get(column) : '';
      return val1.localeCompare(val2);
    };
  }
});
<commit_msg>Refactor datagrid preparation.<commit_after>var Datagrid = Backbone.View.extend({
  initialize: function() {
    this.columns = this.options.columns;
    this.options = _.defaults(this.options, {
      paginated:      false,
      page:           1,
      perPage:        10,
      tableClassName: 'table'
    });

    this.collection.on('reset', this.render, this);
    this._prepare();
  },

  render: function() {
    this.$el.empty();
    this.renderTable();
    if (this.options.paginated) {
      this.renderPagination();
    }

    return this;
  },

  renderTable: function() {
    var $table = $('

', {'class': this.options.tableClassName});
    this.$el.append($table);

    var header = new Header({columns: this.columns, sorter: this.sorter});
    $table.append(header.render().el);

    $table.append('

');

    this.collection.forEach(this.renderRow, this);
  },

  renderPagination: function() {
    var pagination = new Pagination({pager: this.pager});
    this.$el.append(pagination.render().el);
  },

  renderRow: function(model) {
    var options = {
      model: model,
      columns: this.columns
    };
    var rowClassName = this.options.rowClassName;
    if (_.isFunction(rowClassName)) {
      rowClassName = rowClassName(model);
    }
    options.className = rowClassName;

    var row = new Row(options);
    this.$('tbody').append(row.render(this.columns).el);
  },

  refresh: function(options) {
    if (this.options.paginated) {
      this._page(options);
    } else {
      if (this.options.inMemory) {
        this.collection.trigger('reset', this.collection);
        if (options && options.success) {
          options.success();
        }
      } else {
        this._request(options);
      }
    }
  },

  sort: function(column, order) {
    this.sorter.sort(column, order);
  },

  page: function(page) {
    this.pager.page(page);
  },

  perPage: function(perPage) {
    this.pager.set('perPage', perPage);
  },

  _sort: function() {
    if (this.options.inMemory) {
      this._sortInMemory();
    } else {
      this._sortRequest();
    }
  },

  _sortInMemory: function() {
    if (this.options.paginated) {
      this._originalCollection.comparator = _.bind(this._comparator, this);
      this._originalCollection.sort();
      this.page(1);
    } else {
      this.collection.comparator = _.bind(this._comparator, this);
      this.collection.sort();
    }
  },

  _comparator: function(model1, model2) {
    var columnComparator = this._comparatorForColumn(this.sorter.get('column'));
    var order = columnComparator(model1, model2);
    return this.sorter.sortedASC() ? order : -order;
  },

  _comparatorForColumn: function(column) {
    var c = _.find(this.columns, function(c) {
      return c.property === column || c.index === column;
    });
    return c ? c.comparator : undefined;
  },

  _sortRequest: function() {
    this._request();
  },

  _page: function(options) {
    if (this.options.inMemory) {
      this._pageInMemory(options);
    } else {
      this._pageRequest(options);
    }
  },

  _pageRequest: function(options) {
    this._request(options);
  },

  _request: function(options) {
    options     = options || {};
    var success = options.success;
    var silent  = options.silent;

    options.data = this._getRequestData();
    options.success = _.bind(function(collection) {
      if (!this.columns || _.isEmpty(this.columns)) {
        this._prepareColumns();
      }
      if (success) {
        success();
      }
      if (this.options.paginated) {
        this.pager.update(collection);
      }
      if (!silent) {
        collection.trigger('reset', collection);
      }
    }, this);
    options.silent = true;

    this.collection.fetch(options);
  },

  _getRequestData: function() {
    if (this.collection.data && _.isFunction(this.collection.data)) {
      return this.collection.data(this.pager, this.sorter);
    } else if (this.collection.data && typeof this.collection.data === 'object') {
      var data = {};
      _.each(this.collection.data, function(value, param) {
        if (_.isFunction(value)) {
          value = value(this.pager, this.sorter);
        }
        data[param] = value;
      }, this);
      return data;
    } else if (this.options.paginated) {
      return {
        page:     this.pager.get('currentPage'),
        per_page: this.pager.get('perPage')
      };
    }

    return {};
  },

  _pageInMemory: function(options) {
    if (!this._originalCollection) {
      this._originalCollection = this.collection.clone();
    }

    var page    = this.pager.get('currentPage');
    var perPage = this.pager.get('perPage');

    var begin = (page - 1) * perPage;
    var end   = begin + perPage;

    if (options && options.success) {
      options.success();
    }
    this.pager.set('total', this._originalCollection.size());

    this.collection.reset(this._originalCollection.slice(begin, end), options);
  },

  _prepare: function() {
    this._prepareSorter();
    this._preparePager();
    this._prepareColumns();
    this.refresh();
  },

  _prepareSorter: function() {
    this.sorter = new Sorter();
    this.sorter.on('change', function() {
      this._sort(this.sorter.get('column'), this.sorter.get('order'));
    }, this);
  },

  _preparePager: function() {
    this.pager = new Pager({
      currentPage: this.options.page,
      perPage:     this.options.perPage
    });

    this.pager.on('change:currentPage', this._page, this);
    this.pager.on('change:perPage', function() {
      this.page(1);
    }, this);
  },

  _prepareColumns: function() {
    if (!this.columns || _.isEmpty(this.columns)) {
      this._defaultColumns();
    } else {
      _.each(this.columns, function(column, i) {
        this.columns[i] = this._prepareColumn(column, i);
      }, this);
    }
  },

  _prepareColumn: function(column, index) {
    if (_.isString(column)) {
      column = { property: column };
    }
    if (_.isObject(column)) {
      column.index = index;
      if (column.property) {
        column.title = column.title || this._formatTitle(column.property);
      } else if (!column.property && !column.view) {
        throw new Error('Column \'' + column.title + '\' has no property and must accordingly define a custom cell view.');
      }
      if (this.options.inMemory && column.sortable) {
        if (!column.comparator && !column.property && !column.sortedProperty) {
          throw new Error('Invalid column definition: a sortable column must have a comparator, property or sortedProperty defined.');
        }
        column.comparator = column.comparator || this._defaultComparator(column.sortedProperty || column.property);
      }
    }
    return column;
  },

  _formatTitle: function(title) {
    return _.map(title.split(/\s|_/), function(word) {
      return word.charAt(0).toUpperCase() + word.substr(1);
    }).join(' ');
  },

  _defaultColumns: function() {
    this.columns = [];
    var model = this.collection.first(), i = 0;
    if (model) {
      for (var p in model.toJSON()) {
        this.columns.push(this._prepareColumn(p, i++));
      }
    }
  },

  _defaultComparator: function(column) {
    return function(model1, model2) {
      var val1 = model1.has(column) ? model1.get(column) : '';
      var val2 = model2.has(column) ? model2.get(column) : '';
      return val1.localeCompare(val2);
    };
  }
});
<|endoftext|>