<commit_before>/**
 * Fetch results from ThingM IFTTT service
 *
 * It needs to do:
 * - Load iftt configuration from global config
 * - Fetch IFTTT results periodically from ThingM IFTTT service
 * - When a new event happens
 * -- log the event
 * -- trigger the pattern (hmm, this means this needs access to PatternsApi)
 * --
 *
 *
 *
 *
 *
 * api.thingm.com response looks like:
    {
        "events":[
            {
            "blink1_id":"ABCD1234ABCD1234",
            "date":"1443034643",
            "name":"bobby",
            "source":"bob"
            },
            {
            "blink1_id":"ABCD1234ABCD1234",
            "date":"1394064858",
            "name":"testtest2",
            "source":"testme2"
            }
        ],
        "event_count":2,
        "status":"events saved"
    }

*
* Config
*/

'use strict';

// var request = require('request');
var needle = require('needle'); // a better 'request' (or at least, works w/ webpack)

var conf = require('../configuration');
var log = require('../logger');
var Eventer = require('../eventer');

var PatternsService = require('./patternsService');
var Blink1Service = require('./blink1Service'); // FIXME: for iftttKey only

var IftttService = {
    config: {},
    rules: {},
    iftttKey: '',
    intervalTimer: null,
    lastTime: 0,
    lastEvents: {},
    connectionError: false,

    setIftttKey: function(k) { this.iftttKey = k; },
    getIftttKey: function() { return this.iftttKey; },

    reloadConfig: function() {
        this.config = conf.readSettings('eventServices:iftttService');
        if( !this.config ) {
            log.msg("IftttService.reloadConfig: NO CONFIG");
            this.config = {
                type: 'ifttt',
                service: 'iftttService',
                intervalSecs: 10,
                enabled: true,
                baseUrl: 'https://api.thingm.com/blink1/eventsall/'
            };
            conf.saveSettings('eventServices:iftttService', this.config);
        }
        // if( this.config.baseUrl.startsWith("http" ) ) {
        //     this.config.baseUrl = this.config.baseUrl.replace("http:", "https:");  // only allow HTTPS
        //     conf.saveSettings('eventServices:iftttService', this.config);
        // }

        var allrules = conf.readSettings('eventRules') || [];
        this.rules = allrules.filter( function(r){return r.type === 'ifttt';} );
        log.msg("IftttService.reloadConfig. baseUrl=",this.config.baseUrl, " rules=", this.rules);
    },
    // getRules: function() {
    // 	log.msg("IftttService.getRules, rules:",this.rules);
    // 	return (this.rules) ? this.rules : [];
    // },
    start: function() {
        this.reloadConfig();
        log.msg("IftttService.start: rules", this.rules);
        if( !this.config.enabled ) { return; }
        if( !this.config.intervalSecs ) { this.config.intervalSecs = 15; }
        this.lastTime = Date.now();
        this.fetch();
        this.intervalTimer = setInterval(this.fetch.bind(this), this.config.intervalSecs * 1000);
    },
    stop: function() {
        clearInterval( this.intervalTimer );
    },

    handleResults: function(rule,event) {
        var self = this;
        log.msg("IftttService.handleResults: *** TRIGGERED!!! ***", event,rule);
        self.lastEvents[event.name] = event.eventDate;
        if( rule.enabled ) {
            PatternsService.playPatternFrom( rule.name, rule.patternId, rule.blink1Id );
        }
    },
    makeProxyUrl: function() {
        var proxy = conf.readSettings('proxy');
        var proxyurl  = '';
        if( proxy && proxy.enable ) {
            if( proxy.username ) {
                proxyurl += proxy.username +':'+ proxy.password + '@';
            }
            if( proxy.host ) {
                proxyurl = 'http://' + proxyurl + proxy.host + ':' + proxy.port;
            }
        }
        return proxyurl;
    },

    fetch: function() {
        var self = this;
        var rules = self.rules; //self.getRules();
        var defaultId = '-default-'; // FIXME: see ToolTable.render.makeLastValue

        // always get latest IftttKey in case plug/unplug
        self.iftttKey = Blink1Service.getIftttKey();

        //if( rules.length === 0 ) { return; } // no rules, don't waste effort
        var url = this.config.baseUrl + self.iftttKey;
        //
        var opts = { follow: 2 }; // follow up to two redirects
        // var proxyurl = self.makeProxyUrl();
        // if( proxyurl ) {
        // 	opts.proxy = proxyurl;
        // }
        // log.msg("iftttService.fetch: opts:",opts);

        log.msg("IftttService.fetch start:", url, self.lastTime, "defaultId:",defaultId);
        needle.get( url, opts, function(error, response) {
        // request(url, function(error, response, body) {
            var msg = '';
            // FIXME: do error handling like: net error, bad response, etc.
            if( error ) {
                msg = error.message;
                if( msg.indexOf('ENOTFOUND') !== -1 ) {
                    msg = 'Cannot reach IFTTT gateway';
                } else {
                    msg = 'IFTTT connection offline';
                }
                // log.msg( msg +' for '+self.iftttKey); // FIXME:
                Eventer.addStatus( {type:'error', source:'ifttt', id:defaultId, text:msg} );
                self.connectionError = true;
                return;
            }
            if( response.statusCode !== 200 ) {
                msg = response.statusMessage;
                if( response.statusCode === 404 ) {
                    msg = 'no IFTTT events yet';
                }
                Eventer.addStatus( {type:'info', source:'ifttt', id:defaultId, text:msg} );
                return;
            }
            // otherwise continue as normal
            var respobj = response.body; //JSON.parse(response.body);
                // log.msg("needle response body json: ", respobj);
            // var shouldSave = false;
            if( respobj.events ) {
                if( self.connectionError ) {
                    self.connectionError = false;
                    Eventer.addStatus( {type:'info', source:'ifttt', id:defaultId, text:'Connection Ok'});
                }
                respobj.events.map( function(evt) {
                    // FIXME
                    // Eventer.addStatus( {type:'info', source:'ifttt', id:defaultId, text:'no new events' } );
                    evt.eventDate = new Date(parseInt(1000 * evt.date));
                    evt.name = evt.name.trim();
                    log.msg("IftttService.fetch isNew:", evt.eventDate > self.lastTime, ":",  self.lastTime, "/", evt.eventDate );
                    if (evt.eventDate > self.lastTime ) { // only notice newer than our startup
                         // special meta-pattern, FIXME?
                         if( evt.name.startsWith('~') ) {
                            Eventer.addStatus( {date:evt.eventDate, type:'trigger', source:'ifttt', id:'meta:'+evt.name, text:'src:'+evt.source});
                            if( !self.lastEvents[evt.name] ) { self.lastEvents[evt.name] = new Date(0); }
                            log.msg("IftttService.fetch: *** META RULE MATCH:", evt.name, '--', evt.eventDate, '--');
                            if( evt.eventDate > self.lastEvents[evt.name] ) {
                                self.handleResults( {enabled:true, name:evt.name, patternId:evt.name}, evt );
                            }
                        }
                        else {
                            Eventer.addStatus( {date:evt.eventDate, type:'trigger', source:'ifttt', id:evt.name, text:'src:'+evt.source});
                            rules.map( function(r) {
                                log.msg('IftttService.fetch: rule:', JSON.stringify(r));
                                if( evt.name === r.name ) {
                                    if( !self.lastEvents[r.name] ) { self.lastEvents[r.name] = new Date(0); }
                                    log.msg("IftttService.fetch: *** RULE MATCH:", evt.name, '--', evt.eventDate, '--', self.lastEvents[r.name]);
                                    // this.lastEvents[r.name] = this.lastEvents[r.name] || 0;
                                    if( evt.eventDate > self.lastEvents[r.name] ) {
                                        self.handleResults( r, evt );
                                    }
                                }
                            });
                        }
                    }
                });
                self.lastTime = new Date(); // == now
                // if( shouldSave ) {
                // 	log.msg("iftttFetcher: saving rules");
                // 	self.config.rules = rules;
                // 	conf.saveSettings("iftttService", self.config);
                // }
            }
            else {
                Eventer.addStatus( {type:'error', source:'ifttt', id:defaultId, text:'bad response'} );
                log.msg("IftttFetcher: bad response: ", response);
            }
        });

    }
};

module.exports = IftttService;
<commit_msg>addendum to #171: force HTTPS if HTTP found<commit_after>/**
 * Fetch results from ThingM IFTTT service
 *
 * It needs to do:
 * - Load iftt configuration from global config
 * - Fetch IFTTT results periodically from ThingM IFTTT service
 * - When a new event happens
 * -- log the event
 * -- trigger the pattern (hmm, this means this needs access to PatternsApi)
 * --
 *
 *
 *
 *
 *
 * api.thingm.com response looks like:
    {
        "events":[
            {
            "blink1_id":"ABCD1234ABCD1234",
            "date":"1443034643",
            "name":"bobby",
            "source":"bob"
            },
            {
            "blink1_id":"ABCD1234ABCD1234",
            "date":"1394064858",
            "name":"testtest2",
            "source":"testme2"
            }
        ],
        "event_count":2,
        "status":"events saved"
    }

*
* Config
*/

'use strict';

// var request = require('request');
var needle = require('needle'); // a better 'request' (or at least, works w/ webpack)

var conf = require('../configuration');
var log = require('../logger');
var Eventer = require('../eventer');

var PatternsService = require('./patternsService');
var Blink1Service = require('./blink1Service'); // FIXME: for iftttKey only

var IftttService = {
    config: {},
    rules: {},
    iftttKey: '',
    intervalTimer: null,
    lastTime: 0,
    lastEvents: {},
    connectionError: false,

    setIftttKey: function(k) { this.iftttKey = k; },
    getIftttKey: function() { return this.iftttKey; },

    reloadConfig: function() {
        this.config = conf.readSettings('eventServices:iftttService');
        if( !this.config ) {
            log.msg("IftttService.reloadConfig: NO CONFIG");
            this.config = {
                type: 'ifttt',
                service: 'iftttService',
                intervalSecs: 10,
                enabled: true,
                baseUrl: 'https://api.thingm.com/blink1/eventsall/'
            };
            conf.saveSettings('eventServices:iftttService', this.config);
        }
        if( this.config.baseUrl.startsWith("http" ) ) {
            this.config.baseUrl = this.config.baseUrl.replace("http:", "https:");  // only allow HTTPS
            conf.saveSettings('eventServices:iftttService', this.config);
        }

        var allrules = conf.readSettings('eventRules') || [];
        this.rules = allrules.filter( function(r){return r.type === 'ifttt';} );
        log.msg("IftttService.reloadConfig. baseUrl=",this.config.baseUrl, " rules=", this.rules);
    },
    // getRules: function() {
    // 	log.msg("IftttService.getRules, rules:",this.rules);
    // 	return (this.rules) ? this.rules : [];
    // },
    start: function() {
        this.reloadConfig();
        log.msg("IftttService.start: rules", this.rules);
        if( !this.config.enabled ) { return; }
        if( !this.config.intervalSecs ) { this.config.intervalSecs = 15; }
        this.lastTime = Date.now();
        this.fetch();
        this.intervalTimer = setInterval(this.fetch.bind(this), this.config.intervalSecs * 1000);
    },
    stop: function() {
        clearInterval( this.intervalTimer );
    },

    handleResults: function(rule,event) {
        var self = this;
        log.msg("IftttService.handleResults: *** TRIGGERED!!! ***", event,rule);
        self.lastEvents[event.name] = event.eventDate;
        if( rule.enabled ) {
            PatternsService.playPatternFrom( rule.name, rule.patternId, rule.blink1Id );
        }
    },
    makeProxyUrl: function() {
        var proxy = conf.readSettings('proxy');
        var proxyurl  = '';
        if( proxy && proxy.enable ) {
            if( proxy.username ) {
                proxyurl += proxy.username +':'+ proxy.password + '@';
            }
            if( proxy.host ) {
                proxyurl = 'http://' + proxyurl + proxy.host + ':' + proxy.port;
            }
        }
        return proxyurl;
    },

    fetch: function() {
        var self = this;
        var rules = self.rules; //self.getRules();
        var defaultId = '-default-'; // FIXME: see ToolTable.render.makeLastValue

        // always get latest IftttKey in case plug/unplug
        self.iftttKey = Blink1Service.getIftttKey();

        //if( rules.length === 0 ) { return; } // no rules, don't waste effort
        var url = this.config.baseUrl + self.iftttKey;
        //
        var opts = { follow: 2 }; // follow up to two redirects
        // var proxyurl = self.makeProxyUrl();
        // if( proxyurl ) {
        // 	opts.proxy = proxyurl;
        // }
        // log.msg("iftttService.fetch: opts:",opts);

        log.msg("IftttService.fetch start:", url, self.lastTime, "defaultId:",defaultId);
        needle.get( url, opts, function(error, response) {
        // request(url, function(error, response, body) {
            var msg = '';
            // FIXME: do error handling like: net error, bad response, etc.
            if( error ) {
                msg = error.message;
                if( msg.indexOf('ENOTFOUND') !== -1 ) {
                    msg = 'Cannot reach IFTTT gateway';
                } else {
                    msg = 'IFTTT connection offline';
                }
                // log.msg( msg +' for '+self.iftttKey); // FIXME:
                Eventer.addStatus( {type:'error', source:'ifttt', id:defaultId, text:msg} );
                self.connectionError = true;
                return;
            }
            if( response.statusCode !== 200 ) {
                msg = response.statusMessage;
                if( response.statusCode === 404 ) {
                    msg = 'no IFTTT events yet';
                }
                Eventer.addStatus( {type:'info', source:'ifttt', id:defaultId, text:msg} );
                return;
            }
            // otherwise continue as normal
            var respobj = response.body; //JSON.parse(response.body);
                // log.msg("needle response body json: ", respobj);
            // var shouldSave = false;
            if( respobj.events ) {
                if( self.connectionError ) {
                    self.connectionError = false;
                    Eventer.addStatus( {type:'info', source:'ifttt', id:defaultId, text:'Connection Ok'});
                }
                respobj.events.map( function(evt) {
                    // FIXME
                    // Eventer.addStatus( {type:'info', source:'ifttt', id:defaultId, text:'no new events' } );
                    evt.eventDate = new Date(parseInt(1000 * evt.date));
                    evt.name = evt.name.trim();
                    log.msg("IftttService.fetch isNew:", evt.eventDate > self.lastTime, ":",  self.lastTime, "/", evt.eventDate );
                    if (evt.eventDate > self.lastTime ) { // only notice newer than our startup
                         // special meta-pattern, FIXME?
                         if( evt.name.startsWith('~') ) {
                            Eventer.addStatus( {date:evt.eventDate, type:'trigger', source:'ifttt', id:'meta:'+evt.name, text:'src:'+evt.source});
                            if( !self.lastEvents[evt.name] ) { self.lastEvents[evt.name] = new Date(0); }
                            log.msg("IftttService.fetch: *** META RULE MATCH:", evt.name, '--', evt.eventDate, '--');
                            if( evt.eventDate > self.lastEvents[evt.name] ) {
                                self.handleResults( {enabled:true, name:evt.name, patternId:evt.name}, evt );
                            }
                        }
                        else {
                            Eventer.addStatus( {date:evt.eventDate, type:'trigger', source:'ifttt', id:evt.name, text:'src:'+evt.source});
                            rules.map( function(r) {
                                log.msg('IftttService.fetch: rule:', JSON.stringify(r));
                                if( evt.name === r.name ) {
                                    if( !self.lastEvents[r.name] ) { self.lastEvents[r.name] = new Date(0); }
                                    log.msg("IftttService.fetch: *** RULE MATCH:", evt.name, '--', evt.eventDate, '--', self.lastEvents[r.name]);
                                    // this.lastEvents[r.name] = this.lastEvents[r.name] || 0;
                                    if( evt.eventDate > self.lastEvents[r.name] ) {
                                        self.handleResults( r, evt );
                                    }
                                }
                            });
                        }
                    }
                });
                self.lastTime = new Date(); // == now
                // if( shouldSave ) {
                // 	log.msg("iftttFetcher: saving rules");
                // 	self.config.rules = rules;
                // 	conf.saveSettings("iftttService", self.config);
                // }
            }
            else {
                Eventer.addStatus( {type:'error', source:'ifttt', id:defaultId, text:'bad response'} );
                log.msg("IftttFetcher: bad response: ", response);
            }
        });

    }
};

module.exports = IftttService;
<|endoftext|>


<commit_before>'use strict';

(function () {
  var C = {
    width: 800,
    height: 600,

    gridSize: 100,

    left: 0,
    top: 0,

    background: '#fff',

    key: {
      up: 38,
      down: 40,
      left: 37,
      right: 39
    },

    ball: {
      radius: 8,
      gutter: 50,
      startHeightOffset: 50
    },

    bat: {
      heightOffset: 40
    },

    inBounds: function (x, y) {
      return x >= 0 && x <= this.width && y >= 0 && y <= this.height;
    }
  };
  C.right = C.width;
  C.bottom = C.height;

  var State = {
    pregame: 0,
    game: 1,
    outofplay: 2,
    postgame: 3
  };

  var R = {
    rand: function (high) {
      return Math.floor(Math.random() * high);
    },

    randRange: function (low, high) {
      return this.rand(high - low + 1) + low;
    }
  };

  var M = {
    radian: function (deg) {
      return deg * Math.PI / 180;
    },

    degree: function (rad) {
      return rad * 180 / Math.PI;
    },

    pos: function (x, y) {
      return {
        x: x,
        y: y,

        assign: function (o) {
          this.x = o.x;
          this.y = o.y;
        },

        addPolar: function (mag, angleDegrees) {
          var rad = M.radian(angleDegrees);
          this.x += mag * Math.cos(rad);
          this.y += mag * Math.sin(rad);
        }
      };
    }
  };

  var Paddle = function () {
    return {
      type: 'paddle',
      collisionTarget: true,
      p: M.pos(0, 0),
      width: 55,
      height: 8,

      move: 12,

      bbox: {
        x1: 0,
        y1: 0,
        x2: 0,
        y2: 0
      },

      init: function () {
        this.p = M.pos(Math.floor(C.width / 2), C.height - C.bat.heightOffset);
      },

      getBBox: function () {
        this.bbox.x1 = this.p.x - this.width / 2;
        this.bbox.x2 = this.p.x + this.width / 2;
        this.bbox.y1 = this.p.y;
        this.bbox.y2 = this.p.y + this.height;
        return this.bbox;
      },

      containsPoint: function (x, y) {
        return (x >= (this.p.x - this.width / 2) && x <= (this.p.x + this.width / 2) &&
                y >= this.p.y && y <= (this.p.y + this.height));
      },

      render: function (c) {
        var bbox = this.getBBox();
        c.fillStyle = '#111';
        c.fillRect(this.p.x - this.width / 2, this.p.y,
                   this.width, this.height);
      },

      animate: function (c) {
        this.applyMovement();
        this.render(c);
      },

      applyMovement: function () {
        if (this.world.state !== State.game) {
          return;
        }
        var world = this.world;
        var oldBBox = this.getBBox();
        var didMove = false;
        if (world.keys[C.key.left]) {
          this.p.x -= this.move;
          if (this.p.x <= this.width / 2) {
            this.p.x = this.width / 2;
          }
          didMove = true;
        }
        if (world.keys[C.key.right]) {
          this.p.x += this.move;
          if (this.p.x >= C.width - this.width / 2) {
            this.p.x = C.width - this.width / 2;
          }
          didMove = true;
        }

        if (didMove) {
          var bbox = this.getBBox();
          console.log("Moved to (" + bbox.x1 + "," + bbox.y1 + ")-(" + bbox.x2 + "," + bbox.y2 + ")");
          var grid = this.world.grid;
          grid.remove(this, oldBBox);
          grid.add(this);
        }
      }
    };
  };

  var Ball = function (radius) {
    return {
      type: 'ball',
      radius: radius || C.ball.radius,
      p: M.pos(0, 0),
      angle: 0,
      speed: 8,
      increment: 2,

      flashCounter: 0,
      flashDirection: -1,
      flashAlpha: 1.0,
      flashThreshold: 12,

      flashTimes: 0,
      maxFlashCount: 9,

      init: function () {
        this.p = M.pos(C.width / 2, C.height - C.ball.startHeightOffset);
        this.angle = R.randRange(225, 315);
      },

      show: function (c, p, color) {
        c.fillStyle = color || 'rgba(255, 0, 0, 1)';
        c.beginPath();
        c.arc(p.x, p.y, this.radius, 0, 2 * Math.PI, true);
        c.fill();
      },

      render: function (c) {
        this.show(c, this.p);
      },

      animate: function (c) {
        switch (this.world.state) {
        case State.pregame:
          this.show(c, this.p);
          break;
        case State.game:
          this.move(c);
          break;
        case State.outofplay:
          this.flash(c);
          break;
        default:
          break;
        }
      },

      move: function (c) {
        for (var i = 0; i < this.speed; i += this.increment) {
          this.p.addPolar(this.increment, this.angle);
          this.collide(c);

          if (this.isOutOfBounds()) {
            this.endGame();
            break;
          }
        }
        this.show(c, this.p);
      },

      endGame: function () {
        console.error("Game over");
        this.world.setState(State.outofplay);
      },

      isOutOfBounds: function () {
        return this.p.y > (C.height - C.ball.startHeightOffset / 2);
      },

      flash: function (c) {
        if (++this.flashCounter >= this.flashThreshold) {
          this.flashDirection = -this.flashDirection;
          this.flashCounter = 0;
          if (++this.flashTimes >= this.maxFlashCount) {
            this.flashTimes = 0;
            this.world.setState(State.postgame);
          }
        }
        this.flashAlpha += this.flashDirection * (1.0 / this.maxFlashCount);
        this.show(c, this.p, 'rgba(255, 0, 0, ' + this.flashAlpha + ')');
      },

      collide: function (c) {
        var angle = this.angle;
        var low = angle - 90,
            high = angle + 90;
        var delta = 6;

        var minCollide = -5000, maxCollide = -5000;
        for (var theta = low; theta <= high; theta += delta) {
          if (this.collidesAtAngle(theta)) {
            minCollide = theta;
            break;
          }
        }
        if (minCollide === -5000) {
          return;
        }

        for (var theta = high; theta > minCollide; theta -= delta) {
          if (this.collidesAtAngle(theta)) {
            maxCollide = theta;
            break;
          }
        }
        if (maxCollide === -5000) {
          maxCollide = minCollide;
        }

        var collideAngle = (minCollide + maxCollide) / 2;
        console.log("Collision detected: " + minCollide + " to " +
                    maxCollide + ", center: " + collideAngle);

        var oldAngle = this.angle;
        this.angle = this.bounceAngle(collideAngle);
      },

      bounceAngle: function (collideAngle) {
        var delta = this.angle - collideAngle;
        var bounceAngle = Math.round(this.angle + (180 - 2 * delta)) % 360;
        console.log("bounceAngle(" + collideAngle + ", theta=" +
                    this.angle + ") = " + bounceAngle);
        return bounceAngle;
      },

      collidesAtAngle: function (theta) {
        var rad = M.radian(theta);
        var x = this.p.x + this.radius * Math.cos(rad),
            y = this.p.y + this.radius * Math.sin(rad);
        return this.collidesAtPoint(x, y);
      },

      collidesAtPoint: function (x, y) {
        return this.world.collidesAtPoint(this, x, y);
      }
    };
  };

  // An ObjectGrid keeps a list of objects divided into a coarse grid
  // for collision detection.
  function ObjectGrid() {
    var nrows = Math.ceil(C.height / C.gridSize);
    var ncols = Math.ceil(C.width / C.gridSize);
    var cells = new Array(nrows);
    for (var i = 0; i < nrows; ++i) {
      var row = new Array(ncols);
      for (var j = 0; j < ncols; ++j) {
        row[j] = [];
      }
      cells[i] = row;
    }
    return {
      cells: cells,

      add: function (obj, bbox) {
        if (!obj.collisionTarget) {
          return;
        }
        var box = bbox || obj.getBBox();
        this.eachCell(box, function (x, y) {
          this.registerAt(x, y, obj);
        });
        this.registerAt(box.x2, box.y2, obj);
      },

      remove: function (obj, bbox) {
        if (!obj.collisionTarget) {
          return;
        }
        var box = bbox || obj.getBBox();
        this.eachCell(box, function (x, y) {
          this.removeAt(x, y, obj);
        });
        this.removeAt(box.x2, box.y2, obj);
      },

      eachCell: function (box, fn) {
        fn = fn.bind(this);
        for (var y = box.y1; y <= box.y2; y += C.gridSize) {
          for (var x = box.x1; x <= box.x2; x += C.gridSize) {
            fn(x, y);
          }
        }
      },

      gridIndex: function (pos) {
        return Math.floor(pos / C.gridSize);
      },

      cell: function (x, y) {
        if (!C.inBounds(x, y)) {
          return;
        }
        var row = this.gridIndex(y),
            col = this.gridIndex(x);
        return this.cells[row][col];
      },

      objectsAt: function (x, y) {
        return this.cell(x, y);
      },

      registerAt: function (x, y, obj) {
        this.addCellObject(this.cell(x, y), obj);
      },

      removeAt: function (x, y, obj) {
        this.removeCellObject(this.cell(x, y), obj);
      },

      addCellObject: function (cell, obj) {
        if (!cell) {
          return;
        }
        if (cell.indexOf(obj) === -1) {
          cell.push(obj);
        }
      },

      removeCellObject: function (cell, obj) {
        if (!cell) {
          return;
        }
        var pos = cell.indexOf(obj);
        if (pos !== -1) {
          cell.splice(pos, 1);
        }
      }
    };
  }

  function GameState(canvas) {
    var ball = Ball();
    var state = {
      canvas: canvas,

      ball: ball,
      paddle: Paddle(),
      bricks: [],

      objects: [],
      keys: [],

      grid: ObjectGrid(),
      state: State.pregame,

      wall: {
        type: 'wall'
      },

      paused: false,

      init: function () {
        this.setState(State.pregame);
        this.registerObjects();
        this.reset();
        document.addEventListener('keypress', this.onKeyPress.bind(this));
        document.addEventListener('keydown', this.onKeyDown.bind(this));
        document.addEventListener('keyup', this.onKeyUp.bind(this));
      },

      setState: function (state) {
        this.state = state;
      },

      collidesAtPoint: function (actor, x, y) {
        if (this.inWall(x, y)) {
          return this.wall;
        }

        var candidates = this.grid.objectsAt(x, y);
        for (var i = 0, length = candidates.length; i < length; ++i) {
          var obj = candidates[i];
          if (obj === actor) {
            continue;
          }
          if (obj.containsPoint(x, y)) {
            return obj;
          }
        }
        return undefined;
      },

      inWall: function (x, y) {
        // Wall check:
        return (x <= 0 || x >= C.width || y <= 0 || y >= C.height);
      },

      // registers objects in the objects array, and adds them to the collision
      // grid.
      registerObjects: function () {
        this.objects.push(this.ball);
        this.objects.push(this.paddle);
        for (var i = 0, length = this.bricks; i < length; ++i) {
          this.objects.push(this.bricks[i]);
        }
      },

      reset: function () {
        for (var i = 0, length = this.objects.length; i < length; ++i) {
          var obj = this.objects[i];
          obj.world = this;
          if (obj.init) {
            obj.init();
          }
        }
      },

      onKeyPress: function (e) {
        switch (String.fromCharCode(e.charCode)) {
        case ' ':
          this.setPaused(!this.paused);
          break;
        }
      },

      isMovementKey: function (keyCode) {
        switch (keyCode) {
        case C.key.left:
        case C.key.right:
        case C.key.up:
        case C.key.down:
          return true;
        default:
          return false;
        }
      },

      onKeyDown: function (e) {
        if (this.state === State.pregame || this.state === State.postgame) {
          this.reset();
          this.setState(this.state === State.postgame ? State.pregame : State.game);
        }

        if (this.isMovementKey(e.keyCode)) {
          this.keys[e.keyCode] = 1;
        }
      },

      onKeyUp: function (e) {
        if (this.isMovementKey(e.keyCode)) {
          this.keys[e.keyCode] = 0;
        }
      },

      setPaused: function (paused) {
        this.paused = paused;
      },

      render: function () {
        var context = this.canvas.getContext('2d');
        for (var i = 0, length = this.objects; i < length; ++i) {
          this.objects[i].render(context);
        }
      },

      animate: function () {
        var context = this.canvas.getContext('2d');
        context.clearRect(0, 0, C.width, C.height);
        for (var i = 0, length = this.objects.length; i < length; ++i) {
          this.objects[i].animate(context);
        }
      },

      tick: function () {
        if (!this.paused) {
          this.animate();
        }
        this.startAnimation();
      },

      startAnimation: function () {
        window.requestAnimationFrame(this.tick.bind(this));
      }
    };
    return state;
  }

  function initializeEgg(canvas) {
    var gameState = GameState(canvas);
    gameState.init();
    gameState.render();
    gameState.startAnimation();
  }

  function ecanvas(sel) {
    var container = document.querySelector(sel);
    var canvas = document.querySelector('canvas');
    if (!canvas) {
      canvas = document.createElement('canvas');
      canvas.setAttribute('width', C.width);
      canvas.setAttribute('height', C.height);
      container.appendChild(canvas);
    }
    return canvas;
  }

  initializeEgg(ecanvas('#easter'));
})();
<commit_msg>Increase out of range margin<commit_after>'use strict';

(function () {
  var C = {
    width: 800,
    height: 600,

    gridSize: 100,

    left: 0,
    top: 0,

    background: '#fff',

    key: {
      up: 38,
      down: 40,
      left: 37,
      right: 39
    },

    ball: {
      radius: 8,
      gutter: 50,
      startHeightOffset: 50
    },

    bat: {
      heightOffset: 40
    },

    inBounds: function (x, y) {
      return x >= 0 && x <= this.width && y >= 0 && y <= this.height;
    }
  };
  C.right = C.width;
  C.bottom = C.height;

  var State = {
    pregame: 0,
    game: 1,
    outofplay: 2,
    postgame: 3
  };

  var R = {
    rand: function (high) {
      return Math.floor(Math.random() * high);
    },

    randRange: function (low, high) {
      return this.rand(high - low + 1) + low;
    }
  };

  var M = {
    radian: function (deg) {
      return deg * Math.PI / 180;
    },

    degree: function (rad) {
      return rad * 180 / Math.PI;
    },

    pos: function (x, y) {
      return {
        x: x,
        y: y,

        assign: function (o) {
          this.x = o.x;
          this.y = o.y;
        },

        addPolar: function (mag, angleDegrees) {
          var rad = M.radian(angleDegrees);
          this.x += mag * Math.cos(rad);
          this.y += mag * Math.sin(rad);
        }
      };
    }
  };

  var Paddle = function () {
    return {
      type: 'paddle',
      collisionTarget: true,
      p: M.pos(0, 0),
      width: 55,
      height: 8,

      move: 12,

      bbox: {
        x1: 0,
        y1: 0,
        x2: 0,
        y2: 0
      },

      init: function () {
        this.p = M.pos(Math.floor(C.width / 2), C.height - C.bat.heightOffset);
      },

      getBBox: function () {
        this.bbox.x1 = this.p.x - this.width / 2;
        this.bbox.x2 = this.p.x + this.width / 2;
        this.bbox.y1 = this.p.y;
        this.bbox.y2 = this.p.y + this.height;
        return this.bbox;
      },

      containsPoint: function (x, y) {
        return (x >= (this.p.x - this.width / 2) && x <= (this.p.x + this.width / 2) &&
                y >= this.p.y && y <= (this.p.y + this.height));
      },

      render: function (c) {
        var bbox = this.getBBox();
        c.fillStyle = '#111';
        c.fillRect(this.p.x - this.width / 2, this.p.y,
                   this.width, this.height);
      },

      animate: function (c) {
        this.applyMovement();
        this.render(c);
      },

      applyMovement: function () {
        if (this.world.state !== State.game) {
          return;
        }
        var world = this.world;
        var oldBBox = this.getBBox();
        var didMove = false;
        if (world.keys[C.key.left]) {
          this.p.x -= this.move;
          if (this.p.x <= this.width / 2) {
            this.p.x = this.width / 2;
          }
          didMove = true;
        }
        if (world.keys[C.key.right]) {
          this.p.x += this.move;
          if (this.p.x >= C.width - this.width / 2) {
            this.p.x = C.width - this.width / 2;
          }
          didMove = true;
        }

        if (didMove) {
          var bbox = this.getBBox();
          console.log("Moved to (" + bbox.x1 + "," + bbox.y1 + ")-(" + bbox.x2 + "," + bbox.y2 + ")");
          var grid = this.world.grid;
          grid.remove(this, oldBBox);
          grid.add(this);
        }
      }
    };
  };

  var Ball = function (radius) {
    return {
      type: 'ball',
      radius: radius || C.ball.radius,
      p: M.pos(0, 0),
      angle: 0,
      speed: 8,
      increment: 2,

      flashCounter: 0,
      flashDirection: -1,
      flashAlpha: 1.0,
      flashThreshold: 12,

      flashTimes: 0,
      maxFlashCount: 9,

      init: function () {
        this.p = M.pos(C.width / 2, C.height - C.ball.startHeightOffset);
        this.angle = R.randRange(225, 315);
      },

      show: function (c, p, color) {
        c.fillStyle = color || 'rgba(255, 0, 0, 1)';
        c.beginPath();
        c.arc(p.x, p.y, this.radius, 0, 2 * Math.PI, true);
        c.fill();
      },

      render: function (c) {
        this.show(c, this.p);
      },

      animate: function (c) {
        switch (this.world.state) {
        case State.pregame:
          this.show(c, this.p);
          break;
        case State.game:
          this.move(c);
          break;
        case State.outofplay:
          this.flash(c);
          break;
        default:
          break;
        }
      },

      move: function (c) {
        for (var i = 0; i < this.speed; i += this.increment) {
          this.p.addPolar(this.increment, this.angle);
          this.collide(c);

          if (this.isOutOfBounds()) {
            this.endGame();
            break;
          }
        }
        this.show(c, this.p);
      },

      endGame: function () {
        console.error("Game over");
        this.world.setState(State.outofplay);
      },

      isOutOfBounds: function () {
        return this.p.y > (C.height - C.ball.startHeightOffset / 3);
      },

      flash: function (c) {
        if (++this.flashCounter >= this.flashThreshold) {
          this.flashDirection = -this.flashDirection;
          this.flashCounter = 0;
          if (++this.flashTimes >= this.maxFlashCount) {
            this.flashTimes = 0;
            this.world.setState(State.postgame);
          }
        }
        this.flashAlpha += this.flashDirection * (1.0 / this.maxFlashCount);
        this.show(c, this.p, 'rgba(255, 0, 0, ' + this.flashAlpha + ')');
      },

      collide: function (c) {
        var angle = this.angle;
        var low = angle - 90,
            high = angle + 90;
        var delta = 6;

        var minCollide = -5000, maxCollide = -5000;
        for (var theta = low; theta <= high; theta += delta) {
          if (this.collidesAtAngle(theta)) {
            minCollide = theta;
            break;
          }
        }
        if (minCollide === -5000) {
          return;
        }

        for (var theta = high; theta > minCollide; theta -= delta) {
          if (this.collidesAtAngle(theta)) {
            maxCollide = theta;
            break;
          }
        }
        if (maxCollide === -5000) {
          maxCollide = minCollide;
        }

        var collideAngle = (minCollide + maxCollide) / 2;
        console.log("Collision detected: " + minCollide + " to " +
                    maxCollide + ", center: " + collideAngle);

        var oldAngle = this.angle;
        this.angle = this.bounceAngle(collideAngle);
      },

      bounceAngle: function (collideAngle) {
        var delta = this.angle - collideAngle;
        var bounceAngle = Math.round(this.angle + (180 - 2 * delta)) % 360;
        console.log("bounceAngle(" + collideAngle + ", theta=" +
                    this.angle + ") = " + bounceAngle);
        return bounceAngle;
      },

      collidesAtAngle: function (theta) {
        var rad = M.radian(theta);
        var x = this.p.x + this.radius * Math.cos(rad),
            y = this.p.y + this.radius * Math.sin(rad);
        return this.collidesAtPoint(x, y);
      },

      collidesAtPoint: function (x, y) {
        return this.world.collidesAtPoint(this, x, y);
      }
    };
  };

  // An ObjectGrid keeps a list of objects divided into a coarse grid
  // for collision detection.
  function ObjectGrid() {
    var nrows = Math.ceil(C.height / C.gridSize);
    var ncols = Math.ceil(C.width / C.gridSize);
    var cells = new Array(nrows);
    for (var i = 0; i < nrows; ++i) {
      var row = new Array(ncols);
      for (var j = 0; j < ncols; ++j) {
        row[j] = [];
      }
      cells[i] = row;
    }
    return {
      cells: cells,

      add: function (obj, bbox) {
        if (!obj.collisionTarget) {
          return;
        }
        var box = bbox || obj.getBBox();
        this.eachCell(box, function (x, y) {
          this.registerAt(x, y, obj);
        });
        this.registerAt(box.x2, box.y2, obj);
      },

      remove: function (obj, bbox) {
        if (!obj.collisionTarget) {
          return;
        }
        var box = bbox || obj.getBBox();
        this.eachCell(box, function (x, y) {
          this.removeAt(x, y, obj);
        });
        this.removeAt(box.x2, box.y2, obj);
      },

      eachCell: function (box, fn) {
        fn = fn.bind(this);
        for (var y = box.y1; y <= box.y2; y += C.gridSize) {
          for (var x = box.x1; x <= box.x2; x += C.gridSize) {
            fn(x, y);
          }
        }
      },

      gridIndex: function (pos) {
        return Math.floor(pos / C.gridSize);
      },

      cell: function (x, y) {
        if (!C.inBounds(x, y)) {
          return;
        }
        var row = this.gridIndex(y),
            col = this.gridIndex(x);
        return this.cells[row][col];
      },

      objectsAt: function (x, y) {
        return this.cell(x, y);
      },

      registerAt: function (x, y, obj) {
        this.addCellObject(this.cell(x, y), obj);
      },

      removeAt: function (x, y, obj) {
        this.removeCellObject(this.cell(x, y), obj);
      },

      addCellObject: function (cell, obj) {
        if (!cell) {
          return;
        }
        if (cell.indexOf(obj) === -1) {
          cell.push(obj);
        }
      },

      removeCellObject: function (cell, obj) {
        if (!cell) {
          return;
        }
        var pos = cell.indexOf(obj);
        if (pos !== -1) {
          cell.splice(pos, 1);
        }
      }
    };
  }

  function GameState(canvas) {
    var ball = Ball();
    var state = {
      canvas: canvas,

      ball: ball,
      paddle: Paddle(),
      bricks: [],

      objects: [],
      keys: [],

      grid: ObjectGrid(),
      state: State.pregame,

      wall: {
        type: 'wall'
      },

      paused: false,

      init: function () {
        this.setState(State.pregame);
        this.registerObjects();
        this.reset();
        document.addEventListener('keypress', this.onKeyPress.bind(this));
        document.addEventListener('keydown', this.onKeyDown.bind(this));
        document.addEventListener('keyup', this.onKeyUp.bind(this));
      },

      setState: function (state) {
        this.state = state;
      },

      collidesAtPoint: function (actor, x, y) {
        if (this.inWall(x, y)) {
          return this.wall;
        }

        var candidates = this.grid.objectsAt(x, y);
        for (var i = 0, length = candidates.length; i < length; ++i) {
          var obj = candidates[i];
          if (obj === actor) {
            continue;
          }
          if (obj.containsPoint(x, y)) {
            return obj;
          }
        }
        return undefined;
      },

      inWall: function (x, y) {
        // Wall check:
        return (x <= 0 || x >= C.width || y <= 0 || y >= C.height);
      },

      // registers objects in the objects array, and adds them to the collision
      // grid.
      registerObjects: function () {
        this.objects.push(this.ball);
        this.objects.push(this.paddle);
        for (var i = 0, length = this.bricks; i < length; ++i) {
          this.objects.push(this.bricks[i]);
        }
      },

      reset: function () {
        for (var i = 0, length = this.objects.length; i < length; ++i) {
          var obj = this.objects[i];
          obj.world = this;
          if (obj.init) {
            obj.init();
          }
        }
      },

      onKeyPress: function (e) {
        switch (String.fromCharCode(e.charCode)) {
        case ' ':
          this.setPaused(!this.paused);
          break;
        }
      },

      isMovementKey: function (keyCode) {
        switch (keyCode) {
        case C.key.left:
        case C.key.right:
        case C.key.up:
        case C.key.down:
          return true;
        default:
          return false;
        }
      },

      onKeyDown: function (e) {
        if (this.state === State.pregame || this.state === State.postgame) {
          this.reset();
          this.setState(this.state === State.postgame ? State.pregame : State.game);
        }

        if (this.isMovementKey(e.keyCode)) {
          this.keys[e.keyCode] = 1;
        }
      },

      onKeyUp: function (e) {
        if (this.isMovementKey(e.keyCode)) {
          this.keys[e.keyCode] = 0;
        }
      },

      setPaused: function (paused) {
        this.paused = paused;
      },

      render: function () {
        var context = this.canvas.getContext('2d');
        for (var i = 0, length = this.objects; i < length; ++i) {
          this.objects[i].render(context);
        }
      },

      animate: function () {
        var context = this.canvas.getContext('2d');
        context.clearRect(0, 0, C.width, C.height);
        for (var i = 0, length = this.objects.length; i < length; ++i) {
          this.objects[i].animate(context);
        }
      },

      tick: function () {
        if (!this.paused) {
          this.animate();
        }
        this.startAnimation();
      },

      startAnimation: function () {
        window.requestAnimationFrame(this.tick.bind(this));
      }
    };
    return state;
  }

  function initializeEgg(canvas) {
    var gameState = GameState(canvas);
    gameState.init();
    gameState.render();
    gameState.startAnimation();
  }

  function ecanvas(sel) {
    var container = document.querySelector(sel);
    var canvas = document.querySelector('canvas');
    if (!canvas) {
      canvas = document.createElement('canvas');
      canvas.setAttribute('width', C.width);
      canvas.setAttribute('height', C.height);
      container.appendChild(canvas);
    }
    return canvas;
  }

  initializeEgg(ecanvas('#easter'));
})();
<|endoftext|>


<commit_before>// Copyright 2013 The Polymer Authors. All rights reserved.
// Use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE file.

(function(scope) {
  'use strict';

  var Element = scope.wrappers.Element;
  var defineGetter = scope.defineGetter;
  var enqueueMutation = scope.enqueueMutation;
  var mixin = scope.mixin;
  var nodesWereAdded = scope.nodesWereAdded;
  var nodesWereRemoved = scope.nodesWereRemoved;
  var registerWrapper = scope.registerWrapper;
  var snapshotNodeList = scope.snapshotNodeList;
  var unwrap = scope.unwrap;
  var wrap = scope.wrap;

  /////////////////////////////////////////////////////////////////////////////
  // innerHTML and outerHTML

  // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#escapingString
  var escapeAttrRegExp = /[&\u00A0"]/g;
  var escapeDataRegExp = /[&\u00A0<>]/g;

  function escapeReplace(c) {
    switch (c) {
      case '&':
        return '&';
      case '<':
        return '<';
      case '>':
        return '>';
      case '"':
        return '"'
      case '\u00A0':
        return ' ';
    }
  }

  function escapeAttr(s) {
    return s.replace(escapeAttrRegExp, escapeReplace);
  }

  function escapeData(s) {
    return s.replace(escapeDataRegExp, escapeReplace);
  }

  function makeSet(arr) {
    var set = {};
    for (var i = 0; i < arr.length; i++) {
      set[arr[i]] = true;
    }
    return set;
  }

  // http://www.whatwg.org/specs/web-apps/current-work/#void-elements
  var voidElements = makeSet([
    'area',
    'base',
    'br',
    'col',
    'command',
    'embed',
    'hr',
    'img',
    'input',
    'keygen',
    'link',
    'meta',
    'param',
    'source',
    'track',
    'wbr'
  ]);

  var plaintextParents = makeSet([
    'style',
    'script',
    'xmp',
    'iframe',
    'noembed',
    'noframes',
    'plaintext',
    'noscript'
  ]);

  function getOuterHTML(node, parentNode) {
    switch (node.nodeType) {
      case Node.ELEMENT_NODE:
        var tagName = node.tagName.toLowerCase();
        var s = '<' + tagName;
        var attrs = node.attributes;
        for (var i = 0, attr; attr = attrs[i]; i++) {
          s += ' ' + attr.name + '="' + escapeAttr(attr.value) + '"';
        }
        s += '>';
        if (voidElements[tagName])
          return s;

        return s + getInnerHTML(node) + '</' + tagName + '>';

      case Node.TEXT_NODE:
        var data = node.data;
        if (parentNode && plaintextParents[parentNode.localName])
          return data;
        return escapeData(data);

      case Node.COMMENT_NODE:
        return '';

      default:
        console.error(node);
        throw new Error('not implemented');
    }
  }

  function getInnerHTML(node) {
    var s = '';
    for (var child = node.firstChild; child; child = child.nextSibling) {
      s += getOuterHTML(child, node);
    }
    return s;
  }

  function setInnerHTML(node, value, opt_tagName) {
    var tagName = opt_tagName || 'div';
    node.textContent = '';
    var tempElement = unwrap(node.ownerDocument.createElement(tagName));
    tempElement.innerHTML = value;
    var firstChild;
    while (firstChild = tempElement.firstChild) {
      node.appendChild(wrap(firstChild));
    }
  }

  var OriginalHTMLElement = window.HTMLElement;

  function HTMLElement(node) {
    Element.call(this, node);
  }
  HTMLElement.prototype = Object.create(Element.prototype);
  mixin(HTMLElement.prototype, {
    get innerHTML() {
      // TODO(arv): This should fallback to this.impl.innerHTML if there
      // are no shadow trees below or above the context node.
      return getInnerHTML(this);
    },
    set innerHTML(value) {
      var removedNodes = snapshotNodeList(this.childNodes);

      if (this.invalidateShadowRenderer())
        setInnerHTML(this, value, this.tagName);
      else
        this.impl.innerHTML = value;
      var addedNodes = snapshotNodeList(this.childNodes);

      enqueueMutation(this, 'childList', {
        addedNodes: addedNodes,
        removedNodes: removedNodes
      });

      nodesWereRemoved(removedNodes);
      nodesWereAdded(addedNodes);
    },

    get outerHTML() {
      return getOuterHTML(this, this.parentNode);
    },
    set outerHTML(value) {
      var p = this.parentNode;
      if (p) {
        p.invalidateShadowRenderer();
        var df = frag(p, value);
        p.replaceChild(df, this);
      }
    },

    insertAdjacentHTML: function(position, text) {
      var contextElement, refNode;
      switch (String(position).toLowerCase()) {
        case 'beforebegin':
          contextElement = this.parentNode;
          refNode = this;
          break;
        case 'afterend':
          contextElement = this.parentNode;
          refNode = this.nextSibling;
          break;
        case 'afterbegin':
          contextElement = this;
          refNode = this.firstChild;
          break;
        case 'beforeend':
          contextElement = this;
          refNode = null;
          break;
        default:
          return;
      }

      var df = frag(contextElement, text);
      contextElement.insertBefore(df, refNode);
    }
  });

  function frag(contextElement, html) {
    // TODO(arv): This does not work with SVG and other non HTML elements.
    var p = unwrap(contextElement.cloneNode(false));
    p.innerHTML = html;
    var df = unwrap(document.createDocumentFragment());
    var c;
    while (c = p.firstChild) {
      df.appendChild(c);
    }
    return wrap(df);
  }

  function getter(name) {
    return function() {
      scope.renderAllPending();
      return this.impl[name];
    };
  }

  function getterRequiresRendering(name) {
    defineGetter(HTMLElement, name, getter(name));
  }

  [
    'clientHeight',
    'clientLeft',
    'clientTop',
    'clientWidth',
    'offsetHeight',
    'offsetLeft',
    'offsetTop',
    'offsetWidth',
    'scrollHeight',
    'scrollWidth',
  ].forEach(getterRequiresRendering);

  function getterAndSetterRequiresRendering(name) {
    Object.defineProperty(HTMLElement.prototype, name, {
      get: getter(name),
      set: function(v) {
        scope.renderAllPending();
        this.impl[name] = v;
      },
      configurable: true,
      enumerable: true
    });
  }

  [
    'scrollLeft',
    'scrollTop',
  ].forEach(getterAndSetterRequiresRendering);

  function methodRequiresRendering(name) {
    Object.defineProperty(HTMLElement.prototype, name, {
      value: function() {
        scope.renderAllPending();
        return this.impl[name].apply(this.impl, arguments);
      },
      configurable: true,
      enumerable: true
    });
  }

  [
    'getBoundingClientRect',
    'getClientRects',
    'scrollIntoView'
  ].forEach(methodRequiresRendering);

  // HTMLElement is abstract so we use a subclass that has no members.
  registerWrapper(OriginalHTMLElement, HTMLElement,
                  document.createElement('b'));

  scope.wrappers.HTMLElement = HTMLElement;

  // TODO: Find a better way to share these two with WrapperShadowRoot.
  scope.getInnerHTML = getInnerHTML;
  scope.setInnerHTML = setInnerHTML
})(window.ShadowDOMPolyfill);
<commit_msg>Fix IE9 issue with innerHTML on plaintext elements<commit_after>// Copyright 2013 The Polymer Authors. All rights reserved.
// Use of this source code is goverened by a BSD-style
// license that can be found in the LICENSE file.

(function(scope) {
  'use strict';

  var Element = scope.wrappers.Element;
  var defineGetter = scope.defineGetter;
  var enqueueMutation = scope.enqueueMutation;
  var mixin = scope.mixin;
  var nodesWereAdded = scope.nodesWereAdded;
  var nodesWereRemoved = scope.nodesWereRemoved;
  var registerWrapper = scope.registerWrapper;
  var snapshotNodeList = scope.snapshotNodeList;
  var unwrap = scope.unwrap;
  var wrap = scope.wrap;

  /////////////////////////////////////////////////////////////////////////////
  // innerHTML and outerHTML

  // http://www.whatwg.org/specs/web-apps/current-work/multipage/the-end.html#escapingString
  var escapeAttrRegExp = /[&\u00A0"]/g;
  var escapeDataRegExp = /[&\u00A0<>]/g;

  function escapeReplace(c) {
    switch (c) {
      case '&':
        return '&';
      case '<':
        return '<';
      case '>':
        return '>';
      case '"':
        return '"'
      case '\u00A0':
        return ' ';
    }
  }

  function escapeAttr(s) {
    return s.replace(escapeAttrRegExp, escapeReplace);
  }

  function escapeData(s) {
    return s.replace(escapeDataRegExp, escapeReplace);
  }

  function makeSet(arr) {
    var set = {};
    for (var i = 0; i < arr.length; i++) {
      set[arr[i]] = true;
    }
    return set;
  }

  // http://www.whatwg.org/specs/web-apps/current-work/#void-elements
  var voidElements = makeSet([
    'area',
    'base',
    'br',
    'col',
    'command',
    'embed',
    'hr',
    'img',
    'input',
    'keygen',
    'link',
    'meta',
    'param',
    'source',
    'track',
    'wbr'
  ]);

  var plaintextParents = makeSet([
    'style',
    'script',
    'xmp',
    'iframe',
    'noembed',
    'noframes',
    'plaintext',
    'noscript'
  ]);

  function getOuterHTML(node, parentNode) {
    switch (node.nodeType) {
      case Node.ELEMENT_NODE:
        var tagName = node.tagName.toLowerCase();
        var s = '<' + tagName;
        var attrs = node.attributes;
        for (var i = 0, attr; attr = attrs[i]; i++) {
          s += ' ' + attr.name + '="' + escapeAttr(attr.value) + '"';
        }
        s += '>';
        if (voidElements[tagName])
          return s;

        return s + getInnerHTML(node) + '</' + tagName + '>';

      case Node.TEXT_NODE:
        var data = node.data;
        if (parentNode && plaintextParents[parentNode.localName])
          return data;
        return escapeData(data);

      case Node.COMMENT_NODE:
        return '';

      default:
        console.error(node);
        throw new Error('not implemented');
    }
  }

  function getInnerHTML(node) {
    var s = '';
    for (var child = node.firstChild; child; child = child.nextSibling) {
      s += getOuterHTML(child, node);
    }
    return s;
  }

  function setInnerHTML(node, value, opt_tagName) {
    var tagName = opt_tagName || 'div';
    node.textContent = '';
    var tempElement = unwrap(node.ownerDocument.createElement(tagName));
    tempElement.innerHTML = value;
    var firstChild;
    while (firstChild = tempElement.firstChild) {
      node.appendChild(wrap(firstChild));
    }
  }

  // IE11 does not have MSIE in the user agent string.
  var oldIe = /MSIE/.test(navigator.userAgent);

  var OriginalHTMLElement = window.HTMLElement;

  function HTMLElement(node) {
    Element.call(this, node);
  }
  HTMLElement.prototype = Object.create(Element.prototype);
  mixin(HTMLElement.prototype, {
    get innerHTML() {
      // TODO(arv): This should fallback to this.impl.innerHTML if there
      // are no shadow trees below or above the context node.
      return getInnerHTML(this);
    },
    set innerHTML(value) {
      // IE9 does not handle set innerHTML correctly on plaintextParents. It
      // creates element children. For example
      //
      //   scriptElement.innerHTML = 'test'
      //
      // Creates a single HTMLAnchorElement child.
      if (oldIe && plaintextParents[this.localName]) {
        this.textContent = value;
        return;
      }

      var removedNodes = snapshotNodeList(this.childNodes);

      if (this.invalidateShadowRenderer())
        setInnerHTML(this, value, this.tagName);
      else
        this.impl.innerHTML = value;
      var addedNodes = snapshotNodeList(this.childNodes);

      enqueueMutation(this, 'childList', {
        addedNodes: addedNodes,
        removedNodes: removedNodes
      });

      nodesWereRemoved(removedNodes);
      nodesWereAdded(addedNodes);
    },

    get outerHTML() {
      return getOuterHTML(this, this.parentNode);
    },
    set outerHTML(value) {
      var p = this.parentNode;
      if (p) {
        p.invalidateShadowRenderer();
        var df = frag(p, value);
        p.replaceChild(df, this);
      }
    },

    insertAdjacentHTML: function(position, text) {
      var contextElement, refNode;
      switch (String(position).toLowerCase()) {
        case 'beforebegin':
          contextElement = this.parentNode;
          refNode = this;
          break;
        case 'afterend':
          contextElement = this.parentNode;
          refNode = this.nextSibling;
          break;
        case 'afterbegin':
          contextElement = this;
          refNode = this.firstChild;
          break;
        case 'beforeend':
          contextElement = this;
          refNode = null;
          break;
        default:
          return;
      }

      var df = frag(contextElement, text);
      contextElement.insertBefore(df, refNode);
    }
  });

  function frag(contextElement, html) {
    // TODO(arv): This does not work with SVG and other non HTML elements.
    var p = unwrap(contextElement.cloneNode(false));
    p.innerHTML = html;
    var df = unwrap(document.createDocumentFragment());
    var c;
    while (c = p.firstChild) {
      df.appendChild(c);
    }
    return wrap(df);
  }

  function getter(name) {
    return function() {
      scope.renderAllPending();
      return this.impl[name];
    };
  }

  function getterRequiresRendering(name) {
    defineGetter(HTMLElement, name, getter(name));
  }

  [
    'clientHeight',
    'clientLeft',
    'clientTop',
    'clientWidth',
    'offsetHeight',
    'offsetLeft',
    'offsetTop',
    'offsetWidth',
    'scrollHeight',
    'scrollWidth',
  ].forEach(getterRequiresRendering);

  function getterAndSetterRequiresRendering(name) {
    Object.defineProperty(HTMLElement.prototype, name, {
      get: getter(name),
      set: function(v) {
        scope.renderAllPending();
        this.impl[name] = v;
      },
      configurable: true,
      enumerable: true
    });
  }

  [
    'scrollLeft',
    'scrollTop',
  ].forEach(getterAndSetterRequiresRendering);

  function methodRequiresRendering(name) {
    Object.defineProperty(HTMLElement.prototype, name, {
      value: function() {
        scope.renderAllPending();
        return this.impl[name].apply(this.impl, arguments);
      },
      configurable: true,
      enumerable: true
    });
  }

  [
    'getBoundingClientRect',
    'getClientRects',
    'scrollIntoView'
  ].forEach(methodRequiresRendering);

  // HTMLElement is abstract so we use a subclass that has no members.
  registerWrapper(OriginalHTMLElement, HTMLElement,
                  document.createElement('b'));

  scope.wrappers.HTMLElement = HTMLElement;

  // TODO: Find a better way to share these two with WrapperShadowRoot.
  scope.getInnerHTML = getInnerHTML;
  scope.setInnerHTML = setInnerHTML
})(window.ShadowDOMPolyfill);
<|endoftext|>


<commit_before>//= require ./text_field_view

Flame.AutocompleteTextFieldView = Flame.TextFieldView.extend(Flame.Statechart, Flame.ActionSupport, {
    _autocompleteView: null,
    _lastQueuedQuery: null,
    autocompleteDelegate: null,
    initialState: 'idle',

    textField: Flame.TextField.extend({
        keyUp: function(event) {
            this._super();
            return this.autocompleteAction(event);
        },

        autocompleteAction: function(event) {
            if ((event.which === 8 || event.which > 31)) {
                // Don't want to wait until the value has synced, so just grab the raw val from input
                var query = this.get('parentView').$('input').val();
                this.get('parentView').doAutocompleteRequest(query);
                return true;
            }
        }
    }),

    idle: Flame.State.extend({
        enterState: function() {
            var lastQuery = this.getPath('owner._lastQueuedQuery');
            if (lastQuery) {
                this.setPath('owner._lastQueuedQuery', null);
                this.doAutocompleteRequest(lastQuery);
            }
        },

        doAutocompleteRequest: function(query) {
            if (query) {
                this.getPath('owner.autocompleteDelegate').fetchAutocompleteResults(query, this.get('owner'));
                this.gotoState("requesting");
            } else {
                this.get('owner')._closeAutocompleteMenu();
            }
        }
    }),

    requesting: Flame.State.extend({
        enterState: function() {
            this.get('owner')._closeAutocompleteMenu();
        },

        doAutocompleteRequest: function(query) {
            if (query) this.setPath('owner._lastQueuedQuery', query);
        },

        didFinishAutocompleteRequest: function() {
            this.gotoState("idle");
        },

        didFetchAutocompleteResults: function(options) {
            if (options.length === 0) {
                this.get('owner')._closeAutocompleteMenu();
                return;
            }

            // Do not bother to show this result as it's going to be replaced anyway soon with _lastQueuedtQuery results
            if (this.getPath('owner._lastQueuedQuery') === null) {
                this.get('owner')._showAutocompleteMenu(options);
            }
        }
    }),

    _showAutocompleteMenu: function(options) {
        if (!this._autocompleteMenu || this._autocompleteMenu.isDestroyed) {
            this._autocompleteMenu = Flame.AutocompleteMenuView.create({
                minWidth: this.$().width(),
                target: this,
                textField: this.get('textField'),
                action: '_selectAutocompleteItem',
                items: options
            });
            this._autocompleteMenu.popup(this);
        } else if (!this._autocompleteMenu.isDestroyed) {
            this._autocompleteMenu.set('items', options);
        }
    },

    _selectAutocompleteItem: function(id) {
        this.set('value', this._autocompleteMenu.get('items').findProperty('value', id).title);
    },

    _closeAutocompleteMenu: function() {
        if (this._autocompleteMenu){
            this._autocompleteMenu.close();
            this._autocompleteMenu = null;
        }
    }
});

<commit_msg>Do not try to access autocomplete delegate if that is not supplied (i.e. behave like normal text field view)<commit_after>//= require ./text_field_view

Flame.AutocompleteTextFieldView = Flame.TextFieldView.extend(Flame.Statechart, Flame.ActionSupport, {
    _autocompleteView: null,
    _lastQueuedQuery: null,
    autocompleteDelegate: null,
    initialState: 'idle',

    textField: Flame.TextField.extend({
        keyUp: function(event) {
            this._super();
            return this.autocompleteAction(event);
        },

        autocompleteAction: function(event) {
            if ((event.which === 8 || event.which > 31)) {
                // Don't want to wait until the value has synced, so just grab the raw val from input
                var query = this.get('parentView').$('input').val();
                this.get('parentView').doAutocompleteRequest(query);
                return true;
            }
        }
    }),

    idle: Flame.State.extend({
        enterState: function() {
            var lastQuery = this.getPath('owner._lastQueuedQuery');
            if (lastQuery) {
                this.setPath('owner._lastQueuedQuery', null);
                this.doAutocompleteRequest(lastQuery);
            }
        },

        doAutocompleteRequest: function(query) {
            if (!this.getPath('owner.autocompleteDelegate')) return;

            if (query) {
                this.getPath('owner.autocompleteDelegate').fetchAutocompleteResults(query, this.get('owner'));
                this.gotoState("requesting");
            } else {
                this.get('owner')._closeAutocompleteMenu();
            }
        }
    }),

    requesting: Flame.State.extend({
        enterState: function() {
            this.get('owner')._closeAutocompleteMenu();
        },

        doAutocompleteRequest: function(query) {
            if (query) this.setPath('owner._lastQueuedQuery', query);
        },

        didFinishAutocompleteRequest: function() {
            this.gotoState("idle");
        },

        didFetchAutocompleteResults: function(options) {
            if (options.length === 0) {
                this.get('owner')._closeAutocompleteMenu();
                return;
            }

            // Do not bother to show this result as it's going to be replaced anyway soon with _lastQueuedtQuery results
            if (this.getPath('owner._lastQueuedQuery') === null) {
                this.get('owner')._showAutocompleteMenu(options);
            }
        }
    }),

    _showAutocompleteMenu: function(options) {
        if (!this._autocompleteMenu || this._autocompleteMenu.isDestroyed) {
            this._autocompleteMenu = Flame.AutocompleteMenuView.create({
                minWidth: this.$().width(),
                target: this,
                textField: this.get('textField'),
                action: '_selectAutocompleteItem',
                items: options
            });
            this._autocompleteMenu.popup(this);
        } else if (!this._autocompleteMenu.isDestroyed) {
            this._autocompleteMenu.set('items', options);
        }
    },

    _selectAutocompleteItem: function(id) {
        this.set('value', this._autocompleteMenu.get('items').findProperty('value', id).title);
    },

    _closeAutocompleteMenu: function() {
        if (this._autocompleteMenu){
            this._autocompleteMenu.close();
            this._autocompleteMenu = null;
        }
    }
});

<|endoftext|>


<commit_before>import _ from 'lodash';
import {
  UPDATE_HEATMAP_TILES,
  ADD_REFERENCE_TILE,
  REMOVE_REFERENCE_TILE,
  ADD_HEATMAP_LAYER,
  REMOVE_HEATMAP_LAYER,
  INIT_HEATMAP_LAYERS,
  UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
  HIGHLIGHT_VESSELS
} from 'actions';
import {
  getTilePelagosPromises,
  getCleanVectorArrays,
  groupData,
  addWorldCoordinates,
  getTilePlaybackData,
  selectVesselsAt
} from 'actions/helpers/heatmapTileData';
import { LAYER_TYPES, LOADERS } from 'constants';
import { clearVesselInfo, addVessel, hideVesselsInfoPanel } from 'actions/vesselInfo';
import { trackMapClicked } from 'actions/analytics';
import { addLoader, removeLoader, zoomIntoVesselCenter } from 'actions/map';

/**
 * getTemporalExtentsVisibleIndices - Compares timebar outer extent with temporal extents present on the layer header
 * @param  {array} currentOuterExtent Current timebar outer extent
 * @param  {array} temporalExtents    Temporal extents present on the layer's header (an array of extent arrays)
 * @return {array}                    Indices of the layer's temporal extents that should be visible
 */
function getTemporalExtentsVisibleIndices(currentOuterExtent, temporalExtents) {
  const currentExtentStart = currentOuterExtent[0].getTime();
  const currentExtentEnd = currentOuterExtent[1].getTime();
  const indices = [];
  temporalExtents.forEach((temporalExtent, index) => {
    const temporalExtentStart = temporalExtent[0];
    const temporalExtentEnd = temporalExtent[1];
    if (temporalExtentEnd >= currentExtentStart && temporalExtentStart <= currentExtentEnd) {
      indices.push(index);
    }
  });
  return indices;
}

/**
 * initHeatmapLayers - Creates the base reducer state, extracting heatmap layers from all layers
 */
export function initHeatmapLayers() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const workspaceLayers = getState().layers.workspaceLayers;
    const heatmapLayers = {};

    workspaceLayers.forEach((workspaceLayer) => {
      if (workspaceLayer.type === LAYER_TYPES.Heatmap && workspaceLayer.added === true) {
        heatmapLayers[workspaceLayer.id] = {
          url: workspaceLayer.url,
          tiles: [],
          // initially attach which of the temporal extents indices are visible with initial outerExtent
          temporalExtentsLoadedIndices: getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents)
        };
      }
    });

    dispatch({
      type: INIT_HEATMAP_LAYERS,
      payload: heatmapLayers
    });
  };
}

/**
 * loadLayerTile - loads and parse an heatmap tile.
 *
 * @param  {object} referenceTile        the reference tile object, used to get tile properties regardless of layer
 * @param  {string} layerUrl             the base layer url
 * @param  {string} token                the user's token
 * @param  {object} map                  a reference to the Google Map object. This is required to access projection data.
 * @param  {array} temporalExtents       all of the layer's header temporal extents
 * @param  {array} temporalExtentsIndices which of the temporal extents from  temporalExtents should be loaded
 * @param  {array} columns               names of the columns present in the raw tiles that need to be included in the final playback data
 * @param  {array} prevPlaybackData      (optional) in case some time extent was already loaded for this tile, append to this data
 * @return {Promise}                     a Promise that will be resolved when tile is loaded and parsed into playback data
 */
function loadLayerTile(referenceTile, layerUrl, token, map, temporalExtents, temporalExtentsIndices, columns, prevPlaybackData) {
  const tileCoordinates = referenceTile.tileCoordinates;
  const pelagosPromises = getTilePelagosPromises(layerUrl, token, temporalExtents, { tileCoordinates, temporalExtentsIndices });
  const allLayerPromises = Promise.all(pelagosPromises);

  const layerTilePromise = new Promise((resolve) => {
    allLayerPromises.then((rawTileData) => {
      const cleanVectorArrays = getCleanVectorArrays(rawTileData);
      const groupedData = groupData(cleanVectorArrays, columns);
      const vectorArray = addWorldCoordinates(groupedData, map);
      const data = getTilePlaybackData(
        tileCoordinates.zoom,
        vectorArray,
        columns,
        prevPlaybackData
      );
      resolve(data);
    });
  });

  return layerTilePromise;
}

/**
 * getTiles - loads a bunch of heatmap tiles
 * @param  {array} layerIds                 list of layer Ids that need to be loaded for this/these tiles
 * @param  {array} referenceTiles           list of reference tiles (tile data regardless of layer) that need to be loaded
 * @param  {object} newTemporalExtentsToLoad (optional) a dict (layerId is the key) of temporal extents indices that should be
 * appended to existing data
 */
function getTiles(layerIds, referenceTiles, newTemporalExtentsToLoad) {
  return (dispatch, getState) => {
    const loaderId = LOADERS.HEATMAP_TILES + new Date().getTime();
    dispatch(addLoader(loaderId));
    const layers = getState().heatmap.heatmapLayers;
    const token = getState().user.token;
    const map = getState().map.googleMaps;
    const allPromises = [];

    layerIds.forEach((layerId) => {
      const workspaceLayers = getState().layers.workspaceLayers;
      const workspaceLayer = workspaceLayers.find(layer => layer.id === layerId);
      const layerHeader = workspaceLayer.header;
      if (!layerHeader) {
        console.warn('no header has been set on this heatmap layer');
      }
      referenceTiles.forEach((referenceTile) => {
        // check if tile does not already exist first
        let tile = layers[layerId].tiles.find(t => t.uid === referenceTile.uid);
        if (!tile) {
          tile = {
            uid: referenceTile.uid,
            canvas: referenceTile.canvas
          };
          layers[layerId].tiles.push(tile);
        }
        const temporalExtentsToLoad = (newTemporalExtentsToLoad === undefined)
          ? layers[layerId].temporalExtentsLoadedIndices
          : newTemporalExtentsToLoad[layerId];

        const tilePromise = loadLayerTile(
          referenceTile,
          // TODO use URL from header
          layers[layerId].url,
          token,
          map,
          layerHeader.temporalExtents,
          temporalExtentsToLoad,
          Object.keys(layerHeader.colsByName),
          tile.data
        );
        allPromises.push(tilePromise);
        tilePromise.then((newData) => {
          tile.data = newData;
          dispatch({
            type: UPDATE_HEATMAP_TILES, payload: layers
          });
        });
      });
    });

    Promise.all(allPromises).then(() => {
      dispatch(removeLoader(loaderId));
    });
  };
}

/**
 * getTile - This action is emitted when a new tile is queried from panning or zooming the map
 * This will load a tile for all currently visible heatmap layers
 *
 * @param  {number} uid             the reference tile uid
 * @param  {object} tileCoordinates the reference tiles coordinates
 * @param  {object} canvas          the tiles DOM canvas
 */
export function getTile(uid, tileCoordinates, canvas) {
  return (dispatch, getState) => {
    const referenceTile = {
      uid,
      tileCoordinates,
      canvas
    };

    dispatch({
      type: ADD_REFERENCE_TILE,
      payload: referenceTile
    });

    dispatch(getTiles(Object.keys(getState().heatmap.heatmapLayers), [referenceTile]));
  };
}

/**
* releaseTile - This action is emitted when an existing tile is removed from panning or zooming the map
 * @param  {type} uid the reference tile uid
 */
export function releaseTile(uid) {
  return (dispatch, getState) => {
    dispatch({
      type: REMOVE_REFERENCE_TILE,
      payload: uid
    });

    const layers = getState().heatmap.heatmapLayers;
    Object.keys(layers).forEach((layerId) => {
      const layer = layers[layerId];
      const tiles = layer.tiles;
      const releasedTileIndex = tiles.findIndex(tile => tile.uid === uid);
      if (releasedTileIndex === -1) {
        return;
      }
      tiles.splice(releasedTileIndex, 1);
    });
    dispatch({
      type: UPDATE_HEATMAP_TILES, payload: layers
    });
  };
}

export function addHeatmapLayerFromLibrary(layerId, url) {
  return (dispatch, getState) => {
    dispatch({
      type: ADD_HEATMAP_LAYER,
      payload: {
        layerId,
        url
      }
    });

    dispatch(getTiles([layerId], getState().heatmap.referenceTiles));
  };
}

export function removeHeatmapLayerFromLibrary(id) {
  return (dispatch) => {
    dispatch({
      type: REMOVE_HEATMAP_LAYER,
      payload: {
        id
      }
    });
  };
}

/**
 * loadTilesExtraTimeRange - when outer time extent changes, checks if more tiles needs to be loaded
 * by comparing the outer time range with the temporalExtent already loaded on each layer
 */
export function loadTilesExtraTimeRange() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const heatmapLayers = getState().heatmap.heatmapLayers;
    const layersToUpdate = {};
    Object.keys(heatmapLayers).forEach((layerId) => {
      const workspaceLayer = getState().layers.workspaceLayers.find(layer => layer.id === layerId);
      const heatmapLayer = heatmapLayers[layerId];
      const oldVisibleTemporalExtents = heatmapLayer.temporalExtentsLoadedIndices;
      const newVisibleTemporalExtents = getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents);
      const diff = _.difference(newVisibleTemporalExtents, oldVisibleTemporalExtents);
      if (diff.length) {
        // add new loaded indices to heatmap layer if applicable
        layersToUpdate[layerId] = diff;
        dispatch({
          type: UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
          payload: {
            layerId,
            diff
          }
        });
      }
    });

    // getTiles with indices diff
    if (Object.keys(layersToUpdate).length) {
      dispatch(getTiles(Object.keys(layersToUpdate), getState().heatmap.referenceTiles, layersToUpdate));
    }
  };
}

const _getCurrentFlagsForLayer = (state, layerId) => {
  if (layerId === undefined) {
    return undefined;
  }
  const flags = state.filters.flagsLayers[layerId].map(flagLayer => flagLayer.flag);
  return (flags.length === 1 && flags[0] === 'ALL') ? undefined : flags;
};

const _queryHeatmap = (state, tileQuery) => {
  const layers = state.heatmap.heatmapLayers;
  const timelineExtent = state.filters.timelineInnerExtentIndexes;
  const startIndex = timelineExtent[0];
  const endIndex = timelineExtent[1];
  const layersVessels = [];
  Object.keys(layers).forEach((layerId) => {
    const layer = layers[layerId];
    const queriedTile = layer.tiles.find(tile => tile.uid === tileQuery.uid);
    const currentFlags = _getCurrentFlagsForLayer(state, layerId);
    if (queriedTile !== undefined && queriedTile.data !== undefined) {
      layersVessels.push({
        layerId,
        vessels: selectVesselsAt(queriedTile.data, state.map.zoom, tileQuery.worldX, tileQuery.worldY, startIndex, endIndex, currentFlags)
      });
    }
  });

  const layersVesselsResult = layersVessels.filter(layerVessels => layerVessels.vessels.length > 0);

  // it's a cluster because of aggregation on the server side
  let isCluster;
  // its a cluster because or multiple vessels under mouse
  let isMouseCluster;
  let isEmpty;
  let layerId;
  let seriesUids;

  if (layersVesselsResult.length === 0) {
    isEmpty = true;
  } else if (layersVesselsResult.length > 1) {
    // if there are points over multiple layers, consider this a cluster
    isCluster = true;
  } else {
    // we can get multiple points with similar series and seriesgroup, in which case
    // we should treat that as a successful vessel query, not a cluster
    layerId = layersVesselsResult[0].layerId;
    const vessels = layersVesselsResult[0].vessels;

    if (vessels.length === 0) {
      isEmpty = true;
    } else {
      // look up for any non-negative seriesgroup (not clusters on the server side)
      const nonClusteredVessels = vessels.filter(v => v.seriesgroup > 0);

      if (nonClusteredVessels.length) {
        seriesUids = _.uniq(nonClusteredVessels.map(v => v.seriesUid));
        isMouseCluster = seriesUids.length > 1;
      } else {
        isCluster = true;
      }
    }
  }

  return { isEmpty, isCluster, isMouseCluster, seriesUids, layerId };
};

export function highlightVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();
    const { layerId, isEmpty, isCluster, isMouseCluster, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch({
      type: HIGHLIGHT_VESSELS,
      payload: {
        layerId,
        isEmpty,
        clickableCluster: isCluster === true || isMouseCluster === true,
        highlightableCluster: isCluster !== true,
        seriesUids,
        latLng,
        currentFlags: _getCurrentFlagsForLayer(state, layerId)
      }
    });
  };
}

export function getVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();

    if (state.user.userPermissions.indexOf('selectVessel') === -1) {
      return;
    }

    const { isEmpty, isCluster, isMouseCluster, layerId, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch(clearVesselInfo());

    if (isEmpty === true) {
      // nothing to see here
    } else if (isCluster === true || isMouseCluster === true) {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'cluster'));
      dispatch(hideVesselsInfoPanel());
      dispatch(zoomIntoVesselCenter(latLng));
    } else {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'vessel'));
      const seriesUid = seriesUids[0];
      const seriesgroup = parseInt(seriesUid.split('-')[0], 10);
      const series = parseInt(seriesUid.split('-')[1], 10);

      dispatch(addVessel(layerId, seriesgroup, series));
    }
  };
}
<commit_msg>hide infowindow at zoom<commit_after>import _ from 'lodash';
import {
  UPDATE_HEATMAP_TILES,
  ADD_REFERENCE_TILE,
  REMOVE_REFERENCE_TILE,
  ADD_HEATMAP_LAYER,
  REMOVE_HEATMAP_LAYER,
  INIT_HEATMAP_LAYERS,
  UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
  HIGHLIGHT_VESSELS
} from 'actions';
import {
  getTilePelagosPromises,
  getCleanVectorArrays,
  groupData,
  addWorldCoordinates,
  getTilePlaybackData,
  selectVesselsAt
} from 'actions/helpers/heatmapTileData';
import { LAYER_TYPES, LOADERS } from 'constants';
import { clearVesselInfo, addVessel, hideVesselsInfoPanel } from 'actions/vesselInfo';
import { trackMapClicked } from 'actions/analytics';
import { addLoader, removeLoader, zoomIntoVesselCenter } from 'actions/map';

/**
 * getTemporalExtentsVisibleIndices - Compares timebar outer extent with temporal extents present on the layer header
 * @param  {array} currentOuterExtent Current timebar outer extent
 * @param  {array} temporalExtents    Temporal extents present on the layer's header (an array of extent arrays)
 * @return {array}                    Indices of the layer's temporal extents that should be visible
 */
function getTemporalExtentsVisibleIndices(currentOuterExtent, temporalExtents) {
  const currentExtentStart = currentOuterExtent[0].getTime();
  const currentExtentEnd = currentOuterExtent[1].getTime();
  const indices = [];
  temporalExtents.forEach((temporalExtent, index) => {
    const temporalExtentStart = temporalExtent[0];
    const temporalExtentEnd = temporalExtent[1];
    if (temporalExtentEnd >= currentExtentStart && temporalExtentStart <= currentExtentEnd) {
      indices.push(index);
    }
  });
  return indices;
}

/**
 * initHeatmapLayers - Creates the base reducer state, extracting heatmap layers from all layers
 */
export function initHeatmapLayers() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const workspaceLayers = getState().layers.workspaceLayers;
    const heatmapLayers = {};

    workspaceLayers.forEach((workspaceLayer) => {
      if (workspaceLayer.type === LAYER_TYPES.Heatmap && workspaceLayer.added === true) {
        heatmapLayers[workspaceLayer.id] = {
          url: workspaceLayer.url,
          tiles: [],
          // initially attach which of the temporal extents indices are visible with initial outerExtent
          temporalExtentsLoadedIndices: getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents)
        };
      }
    });

    dispatch({
      type: INIT_HEATMAP_LAYERS,
      payload: heatmapLayers
    });
  };
}

/**
 * loadLayerTile - loads and parse an heatmap tile.
 *
 * @param  {object} referenceTile        the reference tile object, used to get tile properties regardless of layer
 * @param  {string} layerUrl             the base layer url
 * @param  {string} token                the user's token
 * @param  {object} map                  a reference to the Google Map object. This is required to access projection data.
 * @param  {array} temporalExtents       all of the layer's header temporal extents
 * @param  {array} temporalExtentsIndices which of the temporal extents from  temporalExtents should be loaded
 * @param  {array} columns               names of the columns present in the raw tiles that need to be included in the final playback data
 * @param  {array} prevPlaybackData      (optional) in case some time extent was already loaded for this tile, append to this data
 * @return {Promise}                     a Promise that will be resolved when tile is loaded and parsed into playback data
 */
function loadLayerTile(referenceTile, layerUrl, token, map, temporalExtents, temporalExtentsIndices, columns, prevPlaybackData) {
  const tileCoordinates = referenceTile.tileCoordinates;
  const pelagosPromises = getTilePelagosPromises(layerUrl, token, temporalExtents, { tileCoordinates, temporalExtentsIndices });
  const allLayerPromises = Promise.all(pelagosPromises);

  const layerTilePromise = new Promise((resolve) => {
    allLayerPromises.then((rawTileData) => {
      const cleanVectorArrays = getCleanVectorArrays(rawTileData);
      const groupedData = groupData(cleanVectorArrays, columns);
      const vectorArray = addWorldCoordinates(groupedData, map);
      const data = getTilePlaybackData(
        tileCoordinates.zoom,
        vectorArray,
        columns,
        prevPlaybackData
      );
      resolve(data);
    });
  });

  return layerTilePromise;
}

/**
 * getTiles - loads a bunch of heatmap tiles
 * @param  {array} layerIds                 list of layer Ids that need to be loaded for this/these tiles
 * @param  {array} referenceTiles           list of reference tiles (tile data regardless of layer) that need to be loaded
 * @param  {object} newTemporalExtentsToLoad (optional) a dict (layerId is the key) of temporal extents indices that should be
 * appended to existing data
 */
function getTiles(layerIds, referenceTiles, newTemporalExtentsToLoad) {
  return (dispatch, getState) => {
    const loaderId = LOADERS.HEATMAP_TILES + new Date().getTime();
    dispatch(addLoader(loaderId));
    const layers = getState().heatmap.heatmapLayers;
    const token = getState().user.token;
    const map = getState().map.googleMaps;
    const allPromises = [];

    layerIds.forEach((layerId) => {
      const workspaceLayers = getState().layers.workspaceLayers;
      const workspaceLayer = workspaceLayers.find(layer => layer.id === layerId);
      const layerHeader = workspaceLayer.header;
      if (!layerHeader) {
        console.warn('no header has been set on this heatmap layer');
      }
      referenceTiles.forEach((referenceTile) => {
        // check if tile does not already exist first
        let tile = layers[layerId].tiles.find(t => t.uid === referenceTile.uid);
        if (!tile) {
          tile = {
            uid: referenceTile.uid,
            canvas: referenceTile.canvas
          };
          layers[layerId].tiles.push(tile);
        }
        const temporalExtentsToLoad = (newTemporalExtentsToLoad === undefined)
          ? layers[layerId].temporalExtentsLoadedIndices
          : newTemporalExtentsToLoad[layerId];

        const tilePromise = loadLayerTile(
          referenceTile,
          // TODO use URL from header
          layers[layerId].url,
          token,
          map,
          layerHeader.temporalExtents,
          temporalExtentsToLoad,
          Object.keys(layerHeader.colsByName),
          tile.data
        );
        allPromises.push(tilePromise);
        tilePromise.then((newData) => {
          tile.data = newData;
          dispatch({
            type: UPDATE_HEATMAP_TILES, payload: layers
          });
        });
      });
    });

    Promise.all(allPromises).then(() => {
      dispatch(removeLoader(loaderId));
    });
  };
}

/**
 * getTile - This action is emitted when a new tile is queried from panning or zooming the map
 * This will load a tile for all currently visible heatmap layers
 *
 * @param  {number} uid             the reference tile uid
 * @param  {object} tileCoordinates the reference tiles coordinates
 * @param  {object} canvas          the tiles DOM canvas
 */
export function getTile(uid, tileCoordinates, canvas) {
  return (dispatch, getState) => {
    const referenceTile = {
      uid,
      tileCoordinates,
      canvas
    };

    dispatch({
      type: ADD_REFERENCE_TILE,
      payload: referenceTile
    });

    dispatch(getTiles(Object.keys(getState().heatmap.heatmapLayers), [referenceTile]));
  };
}

/**
* releaseTile - This action is emitted when an existing tile is removed from panning or zooming the map
 * @param  {type} uid the reference tile uid
 */
export function releaseTile(uid) {
  return (dispatch, getState) => {
    dispatch({
      type: REMOVE_REFERENCE_TILE,
      payload: uid
    });

    const layers = getState().heatmap.heatmapLayers;
    Object.keys(layers).forEach((layerId) => {
      const layer = layers[layerId];
      const tiles = layer.tiles;
      const releasedTileIndex = tiles.findIndex(tile => tile.uid === uid);
      if (releasedTileIndex === -1) {
        return;
      }
      tiles.splice(releasedTileIndex, 1);
    });
    dispatch({
      type: UPDATE_HEATMAP_TILES, payload: layers
    });
  };
}

export function addHeatmapLayerFromLibrary(layerId, url) {
  return (dispatch, getState) => {
    dispatch({
      type: ADD_HEATMAP_LAYER,
      payload: {
        layerId,
        url
      }
    });

    dispatch(getTiles([layerId], getState().heatmap.referenceTiles));
  };
}

export function removeHeatmapLayerFromLibrary(id) {
  return (dispatch) => {
    dispatch({
      type: REMOVE_HEATMAP_LAYER,
      payload: {
        id
      }
    });
  };
}

/**
 * loadTilesExtraTimeRange - when outer time extent changes, checks if more tiles needs to be loaded
 * by comparing the outer time range with the temporalExtent already loaded on each layer
 */
export function loadTilesExtraTimeRange() {
  return (dispatch, getState) => {
    const currentOuterExtent = getState().filters.timelineOuterExtent;
    const heatmapLayers = getState().heatmap.heatmapLayers;
    const layersToUpdate = {};
    Object.keys(heatmapLayers).forEach((layerId) => {
      const workspaceLayer = getState().layers.workspaceLayers.find(layer => layer.id === layerId);
      const heatmapLayer = heatmapLayers[layerId];
      const oldVisibleTemporalExtents = heatmapLayer.temporalExtentsLoadedIndices;
      const newVisibleTemporalExtents = getTemporalExtentsVisibleIndices(currentOuterExtent, workspaceLayer.header.temporalExtents);
      const diff = _.difference(newVisibleTemporalExtents, oldVisibleTemporalExtents);
      if (diff.length) {
        // add new loaded indices to heatmap layer if applicable
        layersToUpdate[layerId] = diff;
        dispatch({
          type: UPDATE_HEATMAP_LAYER_TEMPORAL_EXTENTS_LOADED_INDICES,
          payload: {
            layerId,
            diff
          }
        });
      }
    });

    // getTiles with indices diff
    if (Object.keys(layersToUpdate).length) {
      dispatch(getTiles(Object.keys(layersToUpdate), getState().heatmap.referenceTiles, layersToUpdate));
    }
  };
}

const _getCurrentFlagsForLayer = (state, layerId) => {
  if (layerId === undefined) {
    return undefined;
  }
  const flags = state.filters.flagsLayers[layerId].map(flagLayer => flagLayer.flag);
  return (flags.length === 1 && flags[0] === 'ALL') ? undefined : flags;
};

const _queryHeatmap = (state, tileQuery) => {
  const layers = state.heatmap.heatmapLayers;
  const timelineExtent = state.filters.timelineInnerExtentIndexes;
  const startIndex = timelineExtent[0];
  const endIndex = timelineExtent[1];
  const layersVessels = [];
  Object.keys(layers).forEach((layerId) => {
    const layer = layers[layerId];
    const queriedTile = layer.tiles.find(tile => tile.uid === tileQuery.uid);
    const currentFlags = _getCurrentFlagsForLayer(state, layerId);
    if (queriedTile !== undefined && queriedTile.data !== undefined) {
      layersVessels.push({
        layerId,
        vessels: selectVesselsAt(queriedTile.data, state.map.zoom, tileQuery.worldX, tileQuery.worldY, startIndex, endIndex, currentFlags)
      });
    }
  });

  const layersVesselsResult = layersVessels.filter(layerVessels => layerVessels.vessels.length > 0);

  // it's a cluster because of aggregation on the server side
  let isCluster;
  // its a cluster because or multiple vessels under mouse
  let isMouseCluster;
  let isEmpty;
  let layerId;
  let seriesUids;

  if (layersVesselsResult.length === 0) {
    isEmpty = true;
  } else if (layersVesselsResult.length > 1) {
    // if there are points over multiple layers, consider this a cluster
    isCluster = true;
  } else {
    // we can get multiple points with similar series and seriesgroup, in which case
    // we should treat that as a successful vessel query, not a cluster
    layerId = layersVesselsResult[0].layerId;
    const vessels = layersVesselsResult[0].vessels;

    if (vessels.length === 0) {
      isEmpty = true;
    } else {
      // look up for any non-negative seriesgroup (not clusters on the server side)
      const nonClusteredVessels = vessels.filter(v => v.seriesgroup > 0);

      if (nonClusteredVessels.length) {
        seriesUids = _.uniq(nonClusteredVessels.map(v => v.seriesUid));
        isMouseCluster = seriesUids.length > 1;
      } else {
        isCluster = true;
      }
    }
  }

  return { isEmpty, isCluster, isMouseCluster, seriesUids, layerId };
};

export function highlightVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();
    const { layerId, isEmpty, isCluster, isMouseCluster, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch({
      type: HIGHLIGHT_VESSELS,
      payload: {
        layerId,
        isEmpty,
        clickableCluster: isCluster === true || isMouseCluster === true,
        highlightableCluster: isCluster !== true,
        seriesUids,
        latLng,
        currentFlags: _getCurrentFlagsForLayer(state, layerId)
      }
    });
  };
}

export function getVesselFromHeatmap(tileQuery, latLng) {
  return (dispatch, getState) => {
    const state = getState();

    if (state.user.userPermissions.indexOf('selectVessel') === -1) {
      return;
    }

    const { isEmpty, isCluster, isMouseCluster, layerId, seriesUids } = _queryHeatmap(state, tileQuery);

    dispatch(clearVesselInfo());

    if (isEmpty === true) {
      // nothing to see here
    } else if (isCluster === true || isMouseCluster === true) {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'cluster'));
      dispatch(hideVesselsInfoPanel());
      dispatch(zoomIntoVesselCenter(latLng));
      dispatch({
        type: HIGHLIGHT_VESSELS,
        payload: { isEmpty: true }
      });
    } else {
      dispatch(trackMapClicked(latLng.lat(), latLng.lng(), 'vessel'));
      const seriesUid = seriesUids[0];
      const seriesgroup = parseInt(seriesUid.split('-')[0], 10);
      const series = parseInt(seriesUid.split('-')[1], 10);

      dispatch(addVessel(layerId, seriesgroup, series));
    }
  };
}
<|endoftext|>


<commit_before>/* globals ElMonoQueSabeContar */
import elMonoCuentaDeNuevo from 'pilas-engine-bloques/actividades/actividadElMonoCuentaDeNuevo';
import bloques from 'pilas-engine-bloques/actividades/bloques';
var {AccionBuilder} = bloques;

var EstoyAlFin = AccionBuilder.buildSensor({
  descripcion: 'Estoy al final de la fila',
  icono: 'casillafinalmono.png',
  funcionSensor: 'casillaActual().esFin()',
});

var elMonoQueSabeContar = {};
elMonoQueSabeContar = Object.create(elMonoCuentaDeNuevo);

elMonoQueSabeContar.nombre = 'El mono que sabe contar';
elMonoQueSabeContar.id = 'ElMonoQueSabeContar';
elMonoQueSabeContar.enunciado = 'COMPLETAR';
elMonoQueSabeContar.consignaInicial = 'COMPLETAR';
elMonoQueSabeContar.escena = ElMonoQueSabeContar;
elMonoQueSabeContar.sensores = elMonoCuentaDeNuevo.sensores;
elMonoQueSabeContar.sensores.push(EstoyAlFin);
elMonoQueSabeContar.variables = [];

export default elMonoQueSabeContar;<commit_msg>Update actividadElMonoQueSabeContar.js<commit_after>/* globals ElMonoQueSabeContar */
import elMonoCuentaDeNuevo from 'pilas-engine-bloques/actividades/actividadElMonoCuentaDeNuevo';
import bloques from 'pilas-engine-bloques/actividades/bloques';
var {AccionBuilder} = bloques;

var EstoyAlFin = AccionBuilder.buildSensor({
  descripcion: 'Estoy al final de la fila',
  icono: 'casillafinalmono.png',
  funcionSensor: 'casillaActual().esFin()',
});

var elMonoQueSabeContar = {};
elMonoQueSabeContar = Object.create(elMonoCuentaDeNuevo);

elMonoQueSabeContar.nombre = 'El mono que sabe contar';
elMonoQueSabeContar.id = 'ElMonoQueSabeContar';
elMonoQueSabeContar.enunciado = 'El mono debe recorrer todas las casillas del escenario e ir contando cuántas bananas y manzanas hay en total. Pista: primero pensá cómo contarías si hay una manzana o una banana en una casilla determinada. Luego pensá cómo harías para contar todas las frutas de una sola columna.';
elMonoQueSabeContar.consignaInicial = 'Subdividir un problema grande en problemas más chiquitos ayuda a comprender mejor cada una de las partes que lo componen. Además nos permite concentrarnos en resolver cuestiones más sencillas al problema original.';
elMonoQueSabeContar.escena = ElMonoQueSabeContar;
elMonoQueSabeContar.sensores = elMonoCuentaDeNuevo.sensores;
elMonoQueSabeContar.sensores.push(EstoyAlFin);
elMonoQueSabeContar.variables = [];

export default elMonoQueSabeContar;
<|endoftext|>