<commit_before>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.dao',
  name: 'SyncDAO',
  extends: 'foam.dao.ProxyDAO',

  documentation: `SyncDAO synchronizes data between multiple clients' offline
      caches and a server. When syncronizing, each client tracks the last-seen
      version of each object, or if the object was deleted. The most recent
      version is retained.

      Make sure to set the syncProperty to use to track each object's version.
      It will automatically be incremented as changes are put() into the
      SyncDAO.

      Remote DAOs that interact with SyncDAO clients should be decorated with
      foam.dao.VersionNoDAO, or similar, to provision new version numbers for
      records being written to the server.`,

  requires: [ 'foam.dao.sync.SyncRecord' ],

  implements: [ 'foam.mlang.Expressions' ],

  imports: [ 'setInterval' ],

  properties: [
    {
      class: 'foam.dao.DAOProperty',
      name: 'remoteDAO',
      documentation: 'The shared server DAO to synchronize to.',
      transient: true,
      required: true
    },
    {
      name: 'delegate',
      documentation: 'The local cache to sync with the server DAO.'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'syncRecordDAO',
      documentation: `The DAO in which to store SyncRecords for each object.
          Each client tracks their own sync state in a separate syncRecordDAO.`,
      transient: true,
      required: true
    },
    {
      name: 'syncProperty',
      documentation: `The property to use to store the object version. This
          value on each object will be incremented each time it is put() into
          the SyncDAO.`,
      required: true,
      transient: true
    },
    {
      name: 'of',
      documentation: 'The class of object this DAO will store.',
      required: true,
      transient: true
    },
    {
      class: 'Boolean',
      name: 'polling',
      documentation: `If using a remote DAO without push capabilities, such as
          an HTTP server, polling will periodically attempt to synchronize.`
    },
    {
      /**

      */
      class: 'Int',
      name: 'pollingFrequency',
      documentation: `If using polling, pollingFrequency will determine the
        number of milliseconds to wait between synchronization attempts.`,
      value: 1000
    },
    {
      name: 'synced',
      documentation: `A promise that resolves after any in-flight
          synchronization pass completes.`,
      factory: function() { return Promise.resolve(); }
    },
    {
      name: 'syncRecordWriteSync_',
      factory: function() { return Promise.resolve(); }
    }
  ],

  methods: [
    function init() {
      this.SUPER();

      // Only listen to DAOs that support push (i.e., do not require polling).
      if ( ! this.polling )
        this.remoteDAO$proxy.sub('on', this.onRemoteUpdate);

      // Push initial data from delegate.
      var self = this;
      self.synced = self.delegate.select().then(function(sink) {
        var array = sink.array;
        for ( var i = 0; i < array.length; i++ ) {
          self.syncRecordDAO.put(self.SyncRecord.create({
            id: array[i].id,
            syncNo: -1
          }));
        }
      }).
          // Like sync(), but on initial sync, syncFromRemote_() regardless of
          // whether "polling" is set.
          then(self.syncToRemote_.bind(self)).
          then(self.syncFromRemote_.bind(self));

      // Setup polling after initial sync.
      if ( ! self.polling ) return;
      self.synced.then(function() {
        self.setInterval(function() {
          self.sync();
        }, self.pollingFrequency);
      });
    },
    function sync() {
      // Sync after any sync(s) in progress complete.
      return this.synced = this.synced.then(function() {
        console.log('sync');
        var ret = this.syncToRemote_();

        // Only syncFromRemote_ when polling (otherwise, updates will get pushed
        // anyway).
        if ( this.polling ) ret = ret.then(this.syncFromRemote_.bind(this));

        return ret;
      }.bind(this));
    },

    //
    // DAO overrides.
    //

    function put_(x, obj) {
      var self = this;
      var ret;
      return self.withSyncRecordTx_(function() {
        return self.delegate.put_(x, obj).then(function(o) {
          ret = o;
          // Updates the object's last seen info.
          return self.syncRecordDAO.put_(x, self.SyncRecord.create({
            id: o.id,
            syncNo: -1
          }));
        });
      }).then(self.onLocalUpdate).then(function() { return ret; });
    },
    function remove_(x, obj) {
      var self = this;
      var ret;
      return self.withSyncRecordTx_(function() {
        return self.delegate.remove_(x, obj).then(function(o) {
          ret = o;
          // Marks the object as deleted.
          self.syncRecordDAO.put_(x, self.SyncRecord.create({
            id: obj.id,
            deleted: true,
            syncNo: -1
          }));
        });
      }).then(self.onLocalUpdate).then(function() { return ret; });
    },
    function removeAll_(x, skip, limit, order, predicate) {
      // Marks all the removed objects' sync records as deleted via remove_().
      return this.delegate.select_(x, null, skip, limit, order, predicate).
          then(function(a) {
            a = a.array;
            var promises = [];

            for ( var i = 0 ; i < a.length ; i++ ) {
              promises.push(this.remove_(x, a[i]));
            }

            return Promise.all(promises);
          }.bind(this));
    },

    //
    // Private synchronization details.
    //

    {
      name: 'putFromRemote_',
      documentation: 'Process a put() to cache from remote.',
      code: function(obj) {
        // console.log('putFromRemote_', foam.json.stringify(obj));
        var self = this;
        var ret;
        return self.withSyncRecordTx_(function() {
          return self.delegate.put(obj).then(function(o) {
            ret = o;
            // console.log('putFromRemote_: store syncRecord for', foam.json.stringify(o));
            return self.syncRecordDAO.put(self.SyncRecord.create({
              id: o.id,
              syncNo: o[self.syncProperty.name]
            }));
          });
        }).then(function() { return ret; });
      }
    },
    {
      name: 'removeFromRemote_',
      documentation: 'Process a remove() on cache from remote.',
      code: function(obj) {
        // console.log('removeFromRemote_', foam.json.stringify(obj));
        var self = this;
        var ret;
        return self.withSyncRecordTx_(function() {
          return self.delegate.remove(obj).then(function(o) {
            ret = o;
            // console.log('removeFromRemote_: store syncRecord for', foam.json.stringify(obj));
            return self.syncRecordDAO.put(self.SyncRecord.create({
              id: obj.id,
              syncNo: obj[self.syncProperty.name],
              deleted: true
            }));
          });
        }).then(function() { return ret; });
      }
    },
    {
      name: 'resetFromRemote_',
      documentation: 'Process a reset signal on cache from remote.',
      code: function(obj) {
        // console.log('resetFromRemote_', foam.json.stringify(obj));
        // Clear sync records and data not associated with unsynced data, then
        // sync.
        var self = this;
        var ret;
        return self.withSyncRecordTx_(function() {
          return self.syncRecordDAO(self.GT(self.SyncRecord.SYNC_NO, -1))
              .removeAll().
              then(self.syncRecordDAO.select.bind(self.syncRecordDAO)).
              then(function(sink) {
                var idsToKeep = sink.array.map(function(syncRecord) {
                  return syncRecord.id;
                });
                // console.log('resetFromRemote_: Keeping', idsToKeep,
                //             'not yet pushed');
                return self.delegate.where(
                    self.NOT(self.IN(self.of.ID, idsToKeep))).
                    removeAll();
              });
        }).then(self.sync.bind(self));
      }
    },
    {
      name: 'syncFromRemote_',
      documentation: 'Pull data from remote and sync it to cache.',
      code: function() {
        // console.log('syncFromRemote_');
        var self = this;
        var SYNC_NO = self.SyncRecord.SYNC_NO;

        return self.syncRecordDAO.
            // Like MAX(), but faster on DAOs that can optimize order+limit.
            orderBy(self.DESC(SYNC_NO)).limit(1).
            select().then(function(sink) {
              var value = sink.array[0] && SYNC_NO.f(sink.array[0]);
              // console.log('syncFromRemote_: version >', value || 0);
              return self.remoteDAO.
                  where(self.GT(self.syncProperty, value || 0)).
                  select().then(function(sink) {
                    var array = sink.array;
                    var promises = [];

                    for ( var i = 0 ; i < array.length ; i++ ) {
                      promises.push(self.putFromRemote_(array[i]));
                    }

                    return Promise.all(promises);
                  });
            });
      }
    },
    {
      name: 'syncToRemote_',
      documentation: 'Push data from cach to remote.',
      code: function() {
        // console.log('syncToRemote_');
        var self = this;

        return this.syncRecordDAO
          .where(self.EQ(this.SyncRecord.SYNC_NO, -1))
          .select().then(function(records) {
            records = records.array;
            var promises = [];

            for ( var i = 0 ; i < records.length ; i++ ) {
              var record = records[i];
              var id = record.id;
              var deleted = record.deleted;

              if ( deleted ) {
                var obj = self.of.create(undefined, self);
                obj.id = id;
                promises.push(self.remoteDAO.remove(obj));
              } else {
                // TODO: Stop sending updates if the first one fails.
                promises.push(self.delegate.find(id).then(function(obj) {
                  if ( ! obj ) return null;
                  var ret = self.remoteDAO.put(obj);

                  // When not polling, server result will processed when
                  // onRemoteUpdate listener is fired.
                  if ( self.polling ) {
                    ret = ret.then(function(obj) {
                      self.putFromRemote_(obj);
                    });
                  }

                  return ret;
                }));
              }
            }

            return Promise.all(promises);
          });
      }
    },
    {
      name: 'withSyncRecordTx_',
      documentation: `Run a computation that writes to syncRecordDAO
          transactionally with respect to other syncRecordDAO writes.`,
      code: function(f) {
        return this.syncRecordWriteSync_ = this.syncRecordWriteSync_.then(f);
      }
    }
  ],

  listeners: [
    {
      name: 'onRemoteUpdate',
      documentation: 'Respond to push event from remote.',
      code: function(s, on, event, obj) {
        if ( event == 'put' ) {
          this.putFromRemote_(obj);
        } else if ( event === 'remove' ) {
          this.removeFromRemote_(obj);
        } else if ( event === 'reset' ) {
          this.resetFromRemote_();
        }
      }
    },
    {
      name: 'onLocalUpdate',
      isMerged: true,
      mergeDelay: 100,
      code: function() {
        this.sync();
      }
    }
  ]
});
<commit_msg>SyncDAO bug fix: Polling SyncDAOs need to process removes after they are passed to server<commit_after>/**
 * @license
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

foam.CLASS({
  package: 'foam.dao',
  name: 'SyncDAO',
  extends: 'foam.dao.ProxyDAO',

  documentation: `SyncDAO synchronizes data between multiple clients' offline
      caches and a server. When syncronizing, each client tracks the last-seen
      version of each object, or if the object was deleted. The most recent
      version is retained.

      Make sure to set the syncProperty to use to track each object's version.
      It will automatically be incremented as changes are put() into the
      SyncDAO.

      Remote DAOs that interact with SyncDAO clients should be decorated with
      foam.dao.VersionNoDAO, or similar, to provision new version numbers for
      records being written to the server.`,

  requires: [ 'foam.dao.sync.SyncRecord' ],

  implements: [ 'foam.mlang.Expressions' ],

  imports: [ 'setInterval' ],

  properties: [
    {
      class: 'foam.dao.DAOProperty',
      name: 'remoteDAO',
      documentation: 'The shared server DAO to synchronize to.',
      transient: true,
      required: true
    },
    {
      name: 'delegate',
      documentation: 'The local cache to sync with the server DAO.'
    },
    {
      class: 'foam.dao.DAOProperty',
      name: 'syncRecordDAO',
      documentation: `The DAO in which to store SyncRecords for each object.
          Each client tracks their own sync state in a separate syncRecordDAO.`,
      transient: true,
      required: true
    },
    {
      name: 'syncProperty',
      documentation: `The property to use to store the object version. This
          value on each object will be incremented each time it is put() into
          the SyncDAO.`,
      required: true,
      transient: true
    },
    {
      name: 'of',
      documentation: 'The class of object this DAO will store.',
      required: true,
      transient: true
    },
    {
      class: 'Boolean',
      name: 'polling',
      documentation: `If using a remote DAO without push capabilities, such as
          an HTTP server, polling will periodically attempt to synchronize.`
    },
    {
      /**

      */
      class: 'Int',
      name: 'pollingFrequency',
      documentation: `If using polling, pollingFrequency will determine the
        number of milliseconds to wait between synchronization attempts.`,
      value: 1000
    },
    {
      name: 'synced',
      documentation: `A promise that resolves after any in-flight
          synchronization pass completes.`,
      factory: function() { return Promise.resolve(); }
    },
    {
      name: 'syncRecordWriteSync_',
      factory: function() { return Promise.resolve(); }
    }
  ],

  methods: [
    function init() {
      this.SUPER();

      // Only listen to DAOs that support push (i.e., do not require polling).
      if ( ! this.polling )
        this.remoteDAO$proxy.sub('on', this.onRemoteUpdate);

      // Push initial data from delegate.
      var self = this;
      self.synced = self.delegate.select().then(function(sink) {
        var array = sink.array;
        for ( var i = 0; i < array.length; i++ ) {
          self.syncRecordDAO.put(self.SyncRecord.create({
            id: array[i].id,
            syncNo: -1
          }));
        }
      }).
          // Like sync(), but on initial sync, syncFromRemote_() regardless of
          // whether "polling" is set.
          then(self.syncToRemote_.bind(self)).
          then(self.syncFromRemote_.bind(self));

      // Setup polling after initial sync.
      if ( ! self.polling ) return;
      self.synced.then(function() {
        self.setInterval(function() {
          self.sync();
        }, self.pollingFrequency);
      });
    },
    function sync() {
      // Sync after any sync(s) in progress complete.
      return this.synced = this.synced.then(function() {
        console.log('sync');
        var ret = this.syncToRemote_();

        // Only syncFromRemote_ when polling (otherwise, updates will get pushed
        // anyway).
        if ( this.polling ) ret = ret.then(this.syncFromRemote_.bind(this));

        return ret;
      }.bind(this));
    },

    //
    // DAO overrides.
    //

    function put_(x, obj) {
      var self = this;
      var ret;
      return self.withSyncRecordTx_(function() {
        return self.delegate.put_(x, obj).then(function(o) {
          ret = o;
          // Updates the object's last seen info.
          return self.syncRecordDAO.put_(x, self.SyncRecord.create({
            id: o.id,
            syncNo: -1
          }));
        });
      }).then(self.onLocalUpdate).then(function() { return ret; });
    },
    function remove_(x, obj) {
      var self = this;
      var ret;
      return self.withSyncRecordTx_(function() {
        return self.delegate.remove_(x, obj).then(function(o) {
          ret = o;
          // Marks the object as deleted.
          self.syncRecordDAO.put_(x, self.SyncRecord.create({
            id: obj.id,
            deleted: true,
            syncNo: -1
          }));
        });
      }).then(self.onLocalUpdate).then(function() { return ret; });
    },
    function removeAll_(x, skip, limit, order, predicate) {
      // Marks all the removed objects' sync records as deleted via remove_().
      return this.delegate.select_(x, null, skip, limit, order, predicate).
          then(function(a) {
            a = a.array;
            var promises = [];

            for ( var i = 0 ; i < a.length ; i++ ) {
              promises.push(this.remove_(x, a[i]));
            }

            return Promise.all(promises);
          }.bind(this));
    },

    //
    // Private synchronization details.
    //

    {
      name: 'putFromRemote_',
      documentation: 'Process a put() to cache from remote.',
      code: function(obj) {
        // console.log('putFromRemote_', foam.json.stringify(obj));
        var self = this;
        var ret;
        return self.withSyncRecordTx_(function() {
          return self.delegate.put(obj).then(function(o) {
            ret = o;
            // console.log('putFromRemote_: store syncRecord for', foam.json.stringify(o));
            return self.syncRecordDAO.put(self.SyncRecord.create({
              id: o.id,
              syncNo: o[self.syncProperty.name]
            }));
          });
        }).then(function() { return ret; });
      }
    },
    {
      name: 'removeFromRemote_',
      documentation: 'Process a remove() on cache from remote.',
      code: function(obj) {
        // console.log('removeFromRemote_', foam.json.stringify(obj));
        var self = this;
        var ret;
        return self.withSyncRecordTx_(function() {
          return self.delegate.remove(obj).then(function(o) {
            ret = o;
            // console.log('removeFromRemote_: store syncRecord for', foam.json.stringify(obj));
            return self.syncRecordDAO.put(self.SyncRecord.create({
              id: obj.id,
              syncNo: obj[self.syncProperty.name],
              deleted: true
            }));
          });
        }).then(function() { return ret; });
      }
    },
    {
      name: 'resetFromRemote_',
      documentation: 'Process a reset signal on cache from remote.',
      code: function(obj) {
        // console.log('resetFromRemote_', foam.json.stringify(obj));
        // Clear sync records and data not associated with unsynced data, then
        // sync.
        var self = this;
        var ret;
        return self.withSyncRecordTx_(function() {
          return self.syncRecordDAO(self.GT(self.SyncRecord.SYNC_NO, -1))
              .removeAll().
              then(self.syncRecordDAO.select.bind(self.syncRecordDAO)).
              then(function(sink) {
                var idsToKeep = sink.array.map(function(syncRecord) {
                  return syncRecord.id;
                });
                // console.log('resetFromRemote_: Keeping', idsToKeep,
                //             'not yet pushed');
                return self.delegate.where(
                    self.NOT(self.IN(self.of.ID, idsToKeep))).
                    removeAll();
              });
        }).then(self.sync.bind(self));
      }
    },
    {
      name: 'syncFromRemote_',
      documentation: 'Pull data from remote and sync it to cache.',
      code: function() {
        // console.log('syncFromRemote_');
        var self = this;
        var SYNC_NO = self.SyncRecord.SYNC_NO;

        return self.syncRecordDAO.
            // Like MAX(), but faster on DAOs that can optimize order+limit.
            orderBy(self.DESC(SYNC_NO)).limit(1).
            select().then(function(sink) {
              var value = sink.array[0] && SYNC_NO.f(sink.array[0]);
              // console.log('syncFromRemote_: version >', value || 0);
              return self.remoteDAO.
                  where(self.GT(self.syncProperty, value || 0)).
                  select().then(function(sink) {
                    var array = sink.array;
                    var promises = [];

                    for ( var i = 0 ; i < array.length ; i++ ) {
                      promises.push(self.putFromRemote_(array[i]));
                    }

                    return Promise.all(promises);
                  });
            });
      }
    },
    {
      name: 'syncToRemote_',
      documentation: 'Push data from cach to remote.',
      code: function() {
        // console.log('syncToRemote_');
        var self = this;

        return this.syncRecordDAO
          .where(self.EQ(this.SyncRecord.SYNC_NO, -1))
          .select().then(function(records) {
            records = records.array;
            var promises = [];

            for ( var i = 0 ; i < records.length ; i++ ) {
              var record = records[i];
              var id = record.id;
              var deleted = record.deleted;

              if ( deleted ) {
                var obj = self.of.create(undefined, self);
                obj.id = id;
                var promise = self.remoteDAO.remove(obj);

                // When not polling, server result will processed when
                // onRemoteUpdate listener is fired.
                if ( self.polling ) {
                  promises.push(promise.then(function() {
                    var propName = self.syncProperty.name;
                    // Ensure that obj SyncRecord does not remain queued (i.e.,
                    // does not have syncNo = -1).
                    obj[propName] = Math.max(obj[propName], 0);
                    self.removeFromRemote_(obj);
                  }));
                } else {
                  promises.push(promise);
                }
              } else {
                // TODO: Stop sending updates if the first one fails.
                promises.push(self.delegate.find(id).then(function(obj) {
                  if ( ! obj ) return null;
                  var ret = self.remoteDAO.put(obj);

                  // When not polling, server result will processed when
                  // onRemoteUpdate listener is fired.
                  if ( self.polling ) {
                    ret = ret.then(function(o) {
                      self.putFromRemote_(o);
                    });
                  }

                  return ret;
                }));
              }
            }

            return Promise.all(promises);
          });
      }
    },
    {
      name: 'withSyncRecordTx_',
      documentation: `Run a computation that writes to syncRecordDAO
          transactionally with respect to other syncRecordDAO writes.`,
      code: function(f) {
        return this.syncRecordWriteSync_ = this.syncRecordWriteSync_.then(f);
      }
    }
  ],

  listeners: [
    {
      name: 'onRemoteUpdate',
      documentation: 'Respond to push event from remote.',
      code: function(s, on, event, obj) {
        if ( event == 'put' ) {
          this.putFromRemote_(obj);
        } else if ( event === 'remove' ) {
          this.removeFromRemote_(obj);
        } else if ( event === 'reset' ) {
          this.resetFromRemote_();
        }
      }
    },
    {
      name: 'onLocalUpdate',
      isMerged: true,
      mergeDelay: 100,
      code: function() {
        this.sync();
      }
    }
  ]
});
<|endoftext|>


<commit_before>function GameManager(size, InputManager, Actuator, StorageManager) {
  this.size           = size; // Size of the grid
  this.inputManager   = new InputManager;
  this.storageManager = new StorageManager;
  this.actuator       = new Actuator;

  this.startTiles     = 1;

  this.inputManager.on("move", this.move.bind(this));
  this.inputManager.on("restart", this.restart.bind(this));
  this.inputManager.on("restartWithConfirmation", this.restartWithConfirmation.bind(this));
  this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));

  this.setup();
}

// Restart the game
GameManager.prototype.restart = function () {
  this.storageManager.clearGameState();
  this.actuator.continueGame(); // Clear the game won/lost message
  this.setup();
};

// Restart the game after user confirmation

GameManager.prototype.restartWithConfirmation = function () {
    // Open confirm message
    this.actuator.promptRestart();
};

// Keep playing after winning (allows going over 2048)
GameManager.prototype.keepPlaying = function () {
  this.keepPlaying = true;
  this.actuator.continueGame(); // Clear the game won/lost message
  this.actuate();
};

// Return true if the game is lost, or has won and the user hasn't kept playing
GameManager.prototype.isGameTerminated = function () {
  return this.over || (this.won && !this.keepPlaying);
};

// Set up the game
GameManager.prototype.setup = function () {
  var previousState = this.storageManager.getGameState();

  // Reload the game from a previous game if present
  if (previousState) {
    this.grid        = new Grid(previousState.grid.size,
                                previousState.grid.cells); // Reload grid
    this.score       = previousState.score;
    this.over        = previousState.over;
    this.won         = previousState.won;
    this.keepPlaying = previousState.keepPlaying;
  } else {
    this.grid        = new Grid(this.size);
    this.score       = 0;
    this.over        = false;
    this.won         = false;
    this.keepPlaying = false;

    // Add the initial tiles
    this.addStartTiles();
  }

  // Update the actuator
  this.actuate();
};

// Set up the initial tiles to start the game with
GameManager.prototype.addStartTiles = function () {
//  var tile = new Tile({ x: 0, y: 3 }, 2);
  this.block = new Tile({ x: 0, y: 3});
  this.grid.insertTile(this.block);
//  for (var i = 0; i < this.startTiles; i++) {
//    this.addRandomTile();
//  }
};

// Adds a tile in a random position
GameManager.prototype.addRandomTile = function () {
  if (this.grid.cellsAvailable()) {
    var value = Math.random() < 0.9 ? 2 : 4;
    var tile = new Tile(this.grid.randomAvailableCell(), value);

    this.grid.insertTile(tile);
  }
};

GameManager.prototype.addBlock = function(a) {
    var tile = new Tile({ x: a.x, y: a.y });
    this.grid.insertTile(tile);
}

// Sends the updated grid to the actuator
GameManager.prototype.actuate = function () {
  if (this.storageManager.getBestScore() < this.score) {
    this.storageManager.setBestScore(this.score);
  }

  // Clear the state when the game is over (game over only, not win)
  if (this.over) {
    this.storageManager.clearGameState();
  } else {
    this.storageManager.setGameState(this.serialize());
  }

  this.actuator.actuate(this.grid, {
    score:      this.score,
    over:       this.over,
    won:        this.won,
    bestScore:  this.storageManager.getBestScore(),
    terminated: this.isGameTerminated(),
    keepPlaying: this.keepPlaying
  });

};

// Represent the current game as an object
GameManager.prototype.serialize = function () {
  return {
    grid:        this.grid.serialize(),
    score:       this.score,
    over:        this.over,
    won:         this.won,
    keepPlaying: this.keepPlaying
  };
};

// Save all tile positions and remove merger info
GameManager.prototype.prepareTiles = function () {
  this.grid.eachCell(function (x, y, tile) {
    if (tile) {
      tile.mergedFrom = null;
      tile.savePosition();
    }
  });
};

// Move a tile and its representation
GameManager.prototype.moveTile = function (tile, cell) {
  this.grid.cells[tile.x][tile.y] = null;
  this.grid.cells[cell.x][cell.y] = tile;
  tile.updatePosition(cell);
};

GameManager.prototype.move = function (direction) {
  var self = this;
  var vector     = self.getVector(direction);
  var positions = self.findFarthestPosition(self.block, vector);

  self.addBlock(positions.farthest);
  this.block = positions.farthest;

  if (this.grid.availableCells().length == 0)
      self.won = true;
//  self.won = true;
  this.actuate();
};

// Move tiles on the grid in the specified direction
GameManager.prototype.move2 = function (direction) {
  // 0: up, 1: right, 2: down, 3: left
  var self = this;

  if (this.isGameTerminated()) return; // Don't do anything if the game's over

  var cell, tile;

  var vector     = this.getVector(direction);
  var traversals = this.buildTraversals(vector);
  var moved      = false;

  // Save the current tile positions and remove merger information
  this.prepareTiles();

  // Traverse the grid in the right direction and move tiles
  traversals.x.forEach(function (x) {
    traversals.y.forEach(function (y) {
      cell = { x: x, y: y };
      tile = self.grid.cellContent(cell);

      if (tile) {
        var positions = self.findFarthestPosition(cell, vector);
        var next      = self.grid.cellContent(positions.next);

        // Only one merger per row traversal?
        if (next && next.value === tile.value && !next.mergedFrom) {
          var merged = new Tile(positions.next, tile.value * 2);
          merged.mergedFrom = [tile, next];

//          self.grid.insertTile(merged);
//          self.grid.removeTile(tile);

          // Converge the two tiles' positions
          tile.updatePosition(positions.next);

          // Update the score
          self.score += 1; //merged.value;

          // The mighty 2048 tile
          if (merged.value === 2048) self.won = true;
        } else {
          self.moveTile(tile, positions.farthest);
        }

        if (!self.positionsEqual(cell, tile)) {
          moved = true; // The tile moved from its original cell!
        }
      }
    });
  });

  if (moved) {
//    this.addRandomTile();

    if (!this.movesAvailable()) {
      this.over = true; // Game over!
    }
	// UDI

    this.actuate();
  }
};

// Get the vector representing the chosen direction
GameManager.prototype.getVector = function (direction) {
  // Vectors representing tile movement
  var map = {
    0: { x: 0,  y: -1 }, // Up
    1: { x: 1,  y: 0 },  // Right
    2: { x: 0,  y: 1 },  // Down
    3: { x: -1, y: 0 }   // Left
  };

  return map[direction];
};

// Build a list of positions to traverse in the right order
GameManager.prototype.buildTraversals = function (vector) {
  var traversals = { x: [], y: [] };

  for (var pos = 0; pos < this.size; pos++) {
    traversals.x.push(pos);
    traversals.y.push(pos);
  }

  // Always traverse from the farthest cell in the chosen direction
  if (vector.x === 1) traversals.x = traversals.x.reverse();
  if (vector.y === 1) traversals.y = traversals.y.reverse();

  return traversals;
};

GameManager.prototype.findFarthestPosition = function (cell, vector) {
  var previous;

  // Progress towards the vector direction until an obstacle is found
  do {
    previous = cell;
    cell     = { x: previous.x + vector.x, y: previous.y + vector.y };
  } while (this.grid.withinBounds(cell) &&
           this.grid.cellAvailable(cell));

  return {
    farthest: previous,
    next: cell // Used to check if a merge is required
  };
};

GameManager.prototype.movesAvailable = function () {
  return this.grid.cellsAvailable() || this.tileMatchesAvailable();
};

// Check for available matches between tiles (more expensive check)
GameManager.prototype.tileMatchesAvailable = function () {
  var self = this;

  var tile;

  for (var x = 0; x < this.size; x++) {
    for (var y = 0; y < this.size; y++) {
      tile = this.grid.cellContent({ x: x, y: y });

      if (tile) {
        for (var direction = 0; direction < 2; direction++) {
          var vector = self.getVector(direction);
          var cell   = { x: x + vector.x, y: y + vector.y };

          var other  = self.grid.cellContent(cell);

          if (other && other.value === tile.value) {
            return true; // These two tiles can be merged
          }
        }
      }
    }
  }

  return false;
};

GameManager.prototype.positionsEqual = function (first, second) {
  return first.x === second.x && first.y === second.y;
};
<commit_msg>minor fixes to get things started...<commit_after>function GameManager(size, InputManager, Actuator, StorageManager) {
  this.size           = size; // Size of the grid
  this.inputManager   = new InputManager;
  this.storageManager = new StorageManager;
  this.actuator       = new Actuator;

  this.startTiles     = 1;

  this.inputManager.on("move", this.move.bind(this));
  this.inputManager.on("restart", this.restart.bind(this));
  this.inputManager.on("restartWithConfirmation", this.restartWithConfirmation.bind(this));
  this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));

  this.setup();
}

// Restart the game
GameManager.prototype.restart = function () {
  this.storageManager.clearGameState();
  this.actuator.continueGame(); // Clear the game won/lost message
  this.setup();
};

// Restart the game after user confirmation

GameManager.prototype.restartWithConfirmation = function () {
    // Open confirm message
    this.actuator.promptRestart();
};

// Keep playing after winning (allows going over 2048)
GameManager.prototype.keepPlaying = function () {
  this.keepPlaying = true;
  this.actuator.continueGame(); // Clear the game won/lost message
  this.actuate();
};

// Return true if the game is lost, or has won and the user hasn't kept playing
GameManager.prototype.isGameTerminated = function () {
  return this.over || (this.won && !this.keepPlaying);
};

// Set up the game
GameManager.prototype.setup = function () {
  var previousState = this.storageManager.getGameState();

  // Reload the game from a previous game if present
  if (previousState &&0) {
    this.grid        = new Grid(previousState.grid.size,
                                previousState.grid.cells); // Reload grid
    this.score       = previousState.score;
    this.over        = previousState.over;
    this.won         = previousState.won;
    this.keepPlaying = previousState.keepPlaying;
  } else {
    this.grid        = new Grid(this.size);
    this.score       = 0;
    this.over        = false;
    this.won         = false;
    this.keepPlaying = false;

    // Add the initial tiles
    this.addStartTiles();
  }

  // Update the actuator
  this.actuate();
};

// Set up the initial tiles to start the game with
GameManager.prototype.addStartTiles = function () {
//  var tile = new Tile({ x: 0, y: 3 }, 2);
  this.block = new Tile({ x: 0, y: 3});
  this.grid.insertTile(this.block);
//  for (var i = 0; i < this.startTiles; i++) {
//    this.addRandomTile();
//  }
};

// Adds a tile in a random position
GameManager.prototype.addRandomTile = function () {
  if (this.grid.cellsAvailable()) {
    var value = Math.random() < 0.9 ? 2 : 4;
    var tile = new Tile(this.grid.randomAvailableCell(), value);

    this.grid.insertTile(tile);
  }
};

GameManager.prototype.addBlock = function(a) {
    var tile = new Tile({ x: a.x, y: a.y });
    this.grid.insertTile(tile);
}

// Sends the updated grid to the actuator
GameManager.prototype.actuate = function () {
  if (this.storageManager.getBestScore() < this.score) {
    this.storageManager.setBestScore(this.score);
  }

  // Clear the state when the game is over (game over only, not win)
  if (this.over) {
    this.storageManager.clearGameState();
  } else {
    this.storageManager.setGameState(this.serialize());
  }

  this.actuator.actuate(this.grid, {
    score:      this.score,
    over:       this.over,
    won:        this.won,
    bestScore:  this.storageManager.getBestScore(),
    terminated: this.isGameTerminated(),
    keepPlaying: this.keepPlaying
  });

};

// Represent the current game as an object
GameManager.prototype.serialize = function () {
  return {
    grid:        this.grid.serialize(),
    score:       this.score,
    over:        this.over,
    won:         this.won,
    keepPlaying: this.keepPlaying
  };
};

// Save all tile positions and remove merger info
GameManager.prototype.prepareTiles = function () {
  this.grid.eachCell(function (x, y, tile) {
    if (tile) {
      tile.mergedFrom = null;
      tile.savePosition();
    }
  });
};

// Move a tile and its representation
GameManager.prototype.moveTile = function (tile, cell) {
  this.grid.cells[tile.x][tile.y] = null;
  this.grid.cells[cell.x][cell.y] = tile;
  tile.updatePosition(cell);
};

GameManager.prototype.move = function (direction) {
  var self = this;

  if (this.isGameTerminated()) return;

  var vector     = self.getVector(direction);
  var positions = self.findFarthestPosition(self.block, vector);

  // Check if move is available, otherwise return

  self.addBlock(positions.farthest);
  this.block = positions.farthest;

  self.score += 1;

  if (this.grid.availableCells().length == 0)
      self.won = true;
//  self.won = true;
  this.actuate();
};

// Move tiles on the grid in the specified direction
GameManager.prototype.move2 = function (direction) {
  // 0: up, 1: right, 2: down, 3: left
  var self = this;

  if (this.isGameTerminated()) return; // Don't do anything if the game's over

  var cell, tile;

  var vector     = this.getVector(direction);
  var traversals = this.buildTraversals(vector);
  var moved      = false;

  // Save the current tile positions and remove merger information
  this.prepareTiles();

  // Traverse the grid in the right direction and move tiles
  traversals.x.forEach(function (x) {
    traversals.y.forEach(function (y) {
      cell = { x: x, y: y };
      tile = self.grid.cellContent(cell);

      if (tile) {
        var positions = self.findFarthestPosition(cell, vector);
        var next      = self.grid.cellContent(positions.next);

        // Only one merger per row traversal?
        if (next && next.value === tile.value && !next.mergedFrom) {
          var merged = new Tile(positions.next, tile.value * 2);
          merged.mergedFrom = [tile, next];

//          self.grid.insertTile(merged);
//          self.grid.removeTile(tile);

          // Converge the two tiles' positions
          tile.updatePosition(positions.next);

          // Update the score
          self.score += 1; //merged.value;

          // The mighty 2048 tile
          if (merged.value === 2048) self.won = true;
        } else {
          self.moveTile(tile, positions.farthest);
        }

        if (!self.positionsEqual(cell, tile)) {
          moved = true; // The tile moved from its original cell!
        }
      }
    });
  });

  if (moved) {
//    this.addRandomTile();

    if (!this.movesAvailable()) {
      this.over = true; // Game over!
    }
	// UDI

    this.actuate();
  }
};

// Get the vector representing the chosen direction
GameManager.prototype.getVector = function (direction) {
  // Vectors representing tile movement
  var map = {
    0: { x: 0,  y: -1 }, // Up
    1: { x: 1,  y: 0 },  // Right
    2: { x: 0,  y: 1 },  // Down
    3: { x: -1, y: 0 }   // Left
  };

  return map[direction];
};

// Build a list of positions to traverse in the right order
GameManager.prototype.buildTraversals = function (vector) {
  var traversals = { x: [], y: [] };

  for (var pos = 0; pos < this.size; pos++) {
    traversals.x.push(pos);
    traversals.y.push(pos);
  }

  // Always traverse from the farthest cell in the chosen direction
  if (vector.x === 1) traversals.x = traversals.x.reverse();
  if (vector.y === 1) traversals.y = traversals.y.reverse();

  return traversals;
};

GameManager.prototype.findFarthestPosition = function (cell, vector) {
  var previous;

  // Progress towards the vector direction until an obstacle is found
  do {
    previous = cell;
    cell     = { x: previous.x + vector.x, y: previous.y + vector.y };
  } while (this.grid.withinBounds(cell) &&
           this.grid.cellAvailable(cell));

  return {
    farthest: previous,
    next: cell // Used to check if a merge is required
  };
};

GameManager.prototype.movesAvailable = function () {
  return this.grid.cellsAvailable() || this.tileMatchesAvailable();
};

// Check for available matches between tiles (more expensive check)
GameManager.prototype.tileMatchesAvailable = function () {
  var self = this;

  var tile;

  for (var x = 0; x < this.size; x++) {
    for (var y = 0; y < this.size; y++) {
      tile = this.grid.cellContent({ x: x, y: y });

      if (tile) {
        for (var direction = 0; direction < 2; direction++) {
          var vector = self.getVector(direction);
          var cell   = { x: x + vector.x, y: y + vector.y };

          var other  = self.grid.cellContent(cell);

          if (other && other.value === tile.value) {
            return true; // These two tiles can be merged
          }
        }
      }
    }
  }

  return false;
};

GameManager.prototype.positionsEqual = function (first, second) {
  return first.x === second.x && first.y === second.y;
};
<|endoftext|>


<commit_before>/*!
 * Hux Blog v1.0.0 (http://huxpro.github.io)
 * Copyright 2015 Hux
 */

$(function(){$("[data-toggle='tooltip']").tooltip()}),$(document).ready(function(){$("table").wrap("

"),$("table").addClass("table")}),$(document).ready(function(){$('iframe[src*="youtube.com"]').wrap('

'),$('iframe[src*="youtube.com"]').addClass("embed-responsive-item"),$('iframe[src*="vimeo.com"]').wrap('

'),$('iframe[src*="vimeo.com"]').addClass("embed-responsive-item")}),jQuery(document).ready(function(a){var b=1170;if(a(window).width()>b){var c=a(".navbar-custom").height();a(window).on("scroll",{previousTop:0},function(){var b=a(window).scrollTop();b<this.previousTop?b>0&&a(".navbar-custom").hasClass("is-fixed")?a(".navbar-custom").addClass("is-visible"):a(".navbar-custom").removeClass("is-visible is-fixed"):(a(".navbar-custom").removeClass("is-visible"),b>c&&!a(".navbar-custom").hasClass("is-fixed")&&a(".navbar-custom").addClass("is-fixed")),this.previousTop=b})}});<commit_msg>实验console<commit_after>/*!
 * Hux Blog v1.0.0 (http://huxpro.github.io)
 * Copyright 2015 Hux
 */

$(function(){$("[data-toggle='tooltip']").tooltip()}),$(document).ready(function(){$("table").wrap("

"),$("table").addClass("table")}),$(document).ready(function(){$('iframe[src*="youtube.com"]').wrap('

'),$('iframe[src*="youtube.com"]').addClass("embed-responsive-item"),$('iframe[src*="vimeo.com"]').wrap('

'),$('iframe[src*="vimeo.com"]').addClass("embed-responsive-item")}),jQuery(document).ready(function(a){var b=1170;if(a(window).width()>b){var c=a(".navbar-custom").height();a(window).on("scroll",{previousTop:0},function(){var b=a(window).scrollTop();b<this.previousTop?b>0&&a(".navbar-custom").hasClass("is-fixed")?a(".navbar-custom").addClass("is-visible"):a(".navbar-custom").removeClass("is-visible is-fixed"):(a(".navbar-custom").removeClass("is-visible"),b>c&&!a(".navbar-custom").hasClass("is-fixed")&&a(".navbar-custom").addClass("is-fixed")),this.previousTop=b})}});
window.console && window.console.log && (console.log("一张网页，要经历怎样的过程，才能抵达用户面前？\n一位新人，要经历怎样的成长，才能站在技术之巅？\n探寻这里的秘密；\n体验这里的挑战；\n成为这里的主人；\n加入百度，加入网页搜索，你，可以影响世界。\n"), console.log("请将简历发送至 %c ps_recruiter@baidu.com（ 邮件标题请以“姓名-应聘XX职位-来自console”命名）", "color:red"), console.log("职位介绍：http://dwz.cn/hr2013"))<|endoftext|>


<commit_before>iD.Tree = function(graph) {

    var rtree = rbush(),
        m = 1000 * 1000 * 100,
        head = graph,
        queuedCreated = [],
        queuedModified = [],
        rectangles = {},
        x, y, dx, dy, rebased;

    function extentRectangle(extent) {
        return [
            ~~(m * extent[0][0]),
            ~~(m * extent[0][1]),
            ~~(m * extent[1][0]),
            ~~(m * extent[1][1])
        ];
    }

    function entityRectangle(entity) {
        var rect = extentRectangle(entity.extent(head), entity.id);
        rect.id = entity.id;
        rectangles[entity.id] = rect;
        return rect;
    }

    function remove(entity) {
        rtree.remove(rectangles[entity.id]);
        delete rectangles[entity.id];
    }

    function bulkInsert(entities) {
        for (var i = 0, rects = []; i < entities.length; i++) {
            rects.push(entityRectangle(entities[i]));
        }
        rtree.load(rects);
    }

    function bulkReinsert(entities) {
        entities.forEach(remove);
        bulkInsert(entities);
    }

    var tree = {

        rebase: function(entities) {
            for (var i = 0, inserted = []; i < entities.length; i++) {
                if (!graph.entities.hasOwnProperty(entities[i])) {
                    inserted.push(graph.entity(entities[i]));
                }
            }
            bulkInsert(inserted);
            rebased = true;
            return tree;
        },

        intersects: function(extent, g) {

            head = g;

            if (graph !== head || rebased) {
                var diff = iD.Difference(graph, head),
                    modified = {};

                diff.modified().forEach(function(d) {
                    var loc = graph.entities[d.id].loc;
                    if (!loc || loc[0] !== d.loc[0] || loc[1] !== d.loc[1]) {
                        modified[d.id] = d;
                    }
                });

                var created = diff.created().concat(queuedCreated);
                modified = d3.values(diff.addParents(modified))
                    // some parents might be created, not modified
                    .filter(function(d) { return !!graph.hasEntity(d.id); })
                    .concat(queuedModified);
                queuedCreated = [];
                queuedModified = [];

                var reinserted = [],
                    inserted = [];

                modified.forEach(function(d) {
                    if (head.hasAllChildren(d)) reinserted.push(d);
                    else queuedModified.push(d);
                });

                created.forEach(function(d) {
                    if (head.hasAllChildren(d)) inserted.push(d);
                    else queuedCreated.push(d);
                });

                bulkReinsert(reinserted);
                bulkInsert(inserted);

                diff.deleted().forEach(remove);

                graph = head;
                rebased = false;
            }

            return rtree.search(extentRectangle(extent)).map(function (rect) {
                return graph.entities[rect.id];
            });
        },

        graph: function() {
            return graph;
        }

    };

    return tree;
};
<commit_msg>Remove mysterious math<commit_after>iD.Tree = function(graph) {

    var rtree = rbush(),
        head = graph,
        queuedCreated = [],
        queuedModified = [],
        rectangles = {},
        rebased;

    function extentRectangle(extent) {
        return [
            extent[0][0],
            extent[0][1],
            extent[1][0],
            extent[1][1]
        ];
    }

    function entityRectangle(entity) {
        var rect = extentRectangle(entity.extent(head));
        rect.id = entity.id;
        rectangles[entity.id] = rect;
        return rect;
    }

    function remove(entity) {
        rtree.remove(rectangles[entity.id]);
        delete rectangles[entity.id];
    }

    function bulkInsert(entities) {
        for (var i = 0, rects = []; i < entities.length; i++) {
            rects.push(entityRectangle(entities[i]));
        }
        rtree.load(rects);
    }

    function bulkReinsert(entities) {
        entities.forEach(remove);
        bulkInsert(entities);
    }

    var tree = {

        rebase: function(entities) {
            for (var i = 0, inserted = []; i < entities.length; i++) {
                if (!graph.entities.hasOwnProperty(entities[i])) {
                    inserted.push(graph.entity(entities[i]));
                }
            }
            bulkInsert(inserted);
            rebased = true;
            return tree;
        },

        intersects: function(extent, g) {

            head = g;

            if (graph !== head || rebased) {
                var diff = iD.Difference(graph, head),
                    modified = {};

                diff.modified().forEach(function(d) {
                    var loc = graph.entities[d.id].loc;
                    if (!loc || loc[0] !== d.loc[0] || loc[1] !== d.loc[1]) {
                        modified[d.id] = d;
                    }
                });

                var created = diff.created().concat(queuedCreated);
                modified = d3.values(diff.addParents(modified))
                    // some parents might be created, not modified
                    .filter(function(d) { return !!graph.hasEntity(d.id); })
                    .concat(queuedModified);
                queuedCreated = [];
                queuedModified = [];

                var reinserted = [],
                    inserted = [];

                modified.forEach(function(d) {
                    if (head.hasAllChildren(d)) reinserted.push(d);
                    else queuedModified.push(d);
                });

                created.forEach(function(d) {
                    if (head.hasAllChildren(d)) inserted.push(d);
                    else queuedCreated.push(d);
                });

                bulkReinsert(reinserted);
                bulkInsert(inserted);

                diff.deleted().forEach(remove);

                graph = head;
                rebased = false;
            }

            return rtree.search(extentRectangle(extent)).map(function (rect) {
                return graph.entities[rect.id];
            });
        },

        graph: function() {
            return graph;
        }

    };

    return tree;
};
<|endoftext|>


<commit_before>iD.svg.Areas = function(projection) {
    // For fixing up rendering of multipolygons with tags on the outer member.
    // https://github.com/systemed/iD/issues/613
    function isSimpleMultipolygonOuterMember(entity, graph) {
        if (entity.type !== 'way')
            return false;

        var parents = graph.parentRelations(entity);
        if (parents.length !== 1)
            return false;

        var parent = parents[0];
        if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1)
            return false;

        var members = parent.members, member;
        for (var i = 0; i < members.length; i++) {
            member = members[i];
            if (member.id === entity.id && member.role && member.role !== 'outer')
                return false; // Not outer member
            if (member.id !== entity.id && (!member.role || member.role === 'outer'))
                return false; // Not a simple multipolygon
        }

        return parent;
    }

    // Patterns only work in Firefox when set directly on element
    var patterns = {
        wetland: 'wetland',
        beach: 'beach',
        scrub: 'scrub',
        construction: 'construction',
        cemetery: 'cemetery',
        grave_yard: 'cemetery',
        meadow: 'meadow',
        farm: 'farmland',
        farmland: 'farmland',
        orchard: 'orchard'
    };

    var patternKeys = ['landuse', 'natural', 'amenity'];

    function setPattern(selection) {
        selection.each(function(d) {
            for (var i = 0; i < patternKeys.length; i++) {
                if (patterns.hasOwnProperty(d.tags[patternKeys[i]])) {
                    this.style.fill = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")';
                    return;
                }
            }
        });
    }

    return function drawAreas(surface, graph, entities, filter) {
        var path = d3.geo.path().projection(projection),
            areas = {},
            multipolygon;

        for (var i = 0; i < entities.length; i++) {
            var entity = entities[i];
            if (entity.geometry(graph) !== 'area') continue;

            if (multipolygon = isSimpleMultipolygonOuterMember(entity, graph)) {
                areas[multipolygon.id] = {
                    entity: multipolygon.mergeTags(entity.tags),
                    area: Math.abs(path.area(entity.asGeoJSON(graph, true)))
                };
            } else if (!areas[entity.id]) {
                areas[entity.id] = {
                    entity: entity,
                    area: Math.abs(path.area(entity.asGeoJSON(graph, true)))
                };
            }
        }

        areas = d3.values(areas);
        areas.sort(function(a, b) { return b.area - a.area; });

        function drawPaths(group, areas, filter, klass, closeWay) {
            var tagClasses = iD.svg.TagClasses();

            if (klass === 'stroke') {
                tagClasses.tags(iD.svg.MultipolygonMemberTags(graph));
            }

            var paths = group.selectAll('path.area')
                .filter(filter)
                .data(areas, iD.Entity.key);

            paths.enter()
                .append('path')
                .attr('class', function(d) { return d.type + ' area ' + klass; });

            paths
                .order()
                .attr('d', function(entity) { return path(entity.asGeoJSON(graph, closeWay)); })
                .call(tagClasses)
                .call(iD.svg.MemberClasses(graph));

            if (klass === 'fill') paths.call(setPattern);

            paths.exit()
                .remove();

            return paths;
        }

        areas = _.pluck(areas, 'entity');

        var strokes = areas.filter(function(area) {
            return area.type === 'way';
        });

        var shadow = surface.select('.layer-shadow'),
            fill   = surface.select('.layer-fill'),
            stroke = surface.select('.layer-stroke');

        drawPaths(shadow, strokes, filter, 'shadow');
        drawPaths(fill, areas, filter, 'fill', true);
        drawPaths(stroke, strokes, filter, 'stroke');
    };
};
<commit_msg>remove fill pattern when changing tags<commit_after>iD.svg.Areas = function(projection) {
    // For fixing up rendering of multipolygons with tags on the outer member.
    // https://github.com/systemed/iD/issues/613
    function isSimpleMultipolygonOuterMember(entity, graph) {
        if (entity.type !== 'way')
            return false;

        var parents = graph.parentRelations(entity);
        if (parents.length !== 1)
            return false;

        var parent = parents[0];
        if (!parent.isMultipolygon() || Object.keys(parent.tags).length > 1)
            return false;

        var members = parent.members, member;
        for (var i = 0; i < members.length; i++) {
            member = members[i];
            if (member.id === entity.id && member.role && member.role !== 'outer')
                return false; // Not outer member
            if (member.id !== entity.id && (!member.role || member.role === 'outer'))
                return false; // Not a simple multipolygon
        }

        return parent;
    }

    // Patterns only work in Firefox when set directly on element
    var patterns = {
        wetland: 'wetland',
        beach: 'beach',
        scrub: 'scrub',
        construction: 'construction',
        cemetery: 'cemetery',
        grave_yard: 'cemetery',
        meadow: 'meadow',
        farm: 'farmland',
        farmland: 'farmland',
        orchard: 'orchard'
    };

    var patternKeys = ['landuse', 'natural', 'amenity'];

    function setPattern(selection) {
        selection.each(function(d) {
            for (var i = 0; i < patternKeys.length; i++) {
                if (patterns.hasOwnProperty(d.tags[patternKeys[i]])) {
                    this.style.fill = 'url("#pattern-' + patterns[d.tags[patternKeys[i]]] + '")';
                    return;
                }
            }
            this.style.fill = '';
        });
    }

    return function drawAreas(surface, graph, entities, filter) {
        var path = d3.geo.path().projection(projection),
            areas = {},
            multipolygon;

        for (var i = 0; i < entities.length; i++) {
            var entity = entities[i];
            if (entity.geometry(graph) !== 'area') continue;

            if (multipolygon = isSimpleMultipolygonOuterMember(entity, graph)) {
                areas[multipolygon.id] = {
                    entity: multipolygon.mergeTags(entity.tags),
                    area: Math.abs(path.area(entity.asGeoJSON(graph, true)))
                };
            } else if (!areas[entity.id]) {
                areas[entity.id] = {
                    entity: entity,
                    area: Math.abs(path.area(entity.asGeoJSON(graph, true)))
                };
            }
        }

        areas = d3.values(areas);
        areas.sort(function(a, b) { return b.area - a.area; });

        function drawPaths(group, areas, filter, klass, closeWay) {
            var tagClasses = iD.svg.TagClasses();

            if (klass === 'stroke') {
                tagClasses.tags(iD.svg.MultipolygonMemberTags(graph));
            }

            var paths = group.selectAll('path.area')
                .filter(filter)
                .data(areas, iD.Entity.key);

            paths.enter()
                .append('path')
                .attr('class', function(d) { return d.type + ' area ' + klass; });

            paths
                .order()
                .attr('d', function(entity) { return path(entity.asGeoJSON(graph, closeWay)); })
                .call(tagClasses)
                .call(iD.svg.MemberClasses(graph));

            if (klass === 'fill') paths.call(setPattern);

            paths.exit()
                .remove();

            return paths;
        }

        areas = _.pluck(areas, 'entity');

        var strokes = areas.filter(function(area) {
            return area.type === 'way';
        });

        var shadow = surface.select('.layer-shadow'),
            fill   = surface.select('.layer-fill'),
            stroke = surface.select('.layer-stroke');

        drawPaths(shadow, strokes, filter, 'shadow');
        drawPaths(fill, areas, filter, 'fill', true);
        drawPaths(stroke, strokes, filter, 'stroke');
    };
};
<|endoftext|>


<commit_before>/**
 * @fileoverview Formatters-related utility methods.
 *
 * @see https://google.github.io/styleguide/javascriptguide.xml
 * @see https://developers.google.com/closure/compiler/docs/js-for-compiler
 * @see https://github.com/Datamart/bytes-formatter
 * @module glize/formatters
 */

import { formatBytes } from 'bytes-formatter';
import { formatDate, parseDate } from './date.js';
import { formatNumber, roundNumber, ordinal } from './number.js';

export {
  formatBytes, formatDate, parseDate,
  formatNumber, roundNumber, ordinal
};
<commit_msg>Upated jsdoc for formatters<commit_after>/**
 * @fileoverview Formatters-related utility methods.
 *
 * @see https://google.github.io/styleguide/javascriptguide.xml
 * @see https://developers.google.com/closure/compiler/docs/js-for-compiler
 * @see https://github.com/Datamart/bytes-formatter
 * @module glize/formatters
 */

import { formatBytes } from 'bytes-formatter';
import { formatDate, parseDate } from './date.js';
import { formatNumber, roundNumber, ordinal } from './number.js';

export {
  /**
   * Formats given bytes to human friendly format.
   * @param {number} bytes The bytes to be formatted.
   * @return {string} The formatted bytes as string.
   * @see https://github.com/Datamart/bytes-formatter
   * @method
   * @example
   * formatBytes(1024); // 1.0 Kb
   */
  formatBytes,
  formatDate, parseDate,
  formatNumber, roundNumber, ordinal
};
<|endoftext|>


<commit_before>/**
 * jQuery Stepy - A Wizard Plugin - http://wbotelhos.com/stepy
 * ---------------------------------------------------------------------------------
 *
 * jQuery Stepy is a plugin based on FormToWizard that generates a customizable wizard.
 *
 * Licensed under The MIT License
 *
 * @version			0.4.1
 * @since			07.03.2010
 * @author			Washington Botelho dos Santos
 * @documentation	http://wbotelhos.com/raty
 * @twitter			http://twitter.com/wbotelhos
 * @license			http://opensource.org/licenses/mit-license.php MIT
 * @package			jQuery Plugins
 *
 * Usage with default values:
 * ---------------------------------------------------------------------------------
 * $('#stepy').stepy();
 *
 *	<form id="stepy">
 *		<fieldset title="Step 1">
 *			<legend>description one</legend>
 *
 *		</fieldset>
 *
 *		<fieldset title="Step 2">
 *			<legend>description one</legend>
 *
 *		</fieldset>
 *
 *		// and so on..
 *
 *		<input type="submit" class="finish" value="Finish!"/>
 *	</form>
 *
 */

;(function($) {

	$.fn.stepy = function(settings) {

		if (this.length == 0) {
			debug('Selector invalid or missing!');
			return;
		} else if (this.length > 1) {
			return this.each(function() {
				$.fn.stepy.apply($(this), [settings]);
			});
		}

		var opt				= $.extend({}, $.fn.stepy.defaults, settings),
			$this			= $(this),
			id				= this.attr('id'),
			$steps			= $this.children('fieldset'),
			size			= $steps.size(),
			$titlesWrapper	= $('
', { id: id + '-titles', 'class': 'stepy-titles' }),
			description		= '',
			title			= '',
			step;

		if (opt.titleTarget) {
			$(opt.titleTarget).html($titlesWrapper);
		} else {
			$titlesWrapper.insertBefore($this);
		}

		if (id === undefined) {
			id = 'stepy-' + $this.index();
			$this.attr('id', id);
		}

        if (opt.validate && $this.is('form')) {
        	$this.append('
');
        }

        $steps.each(function(index) { // fieldset
        	step = $(this);

        	step
        	.attr('id', id + '-step-' + index)
        	.addClass('step')
        	.append('
');

        	title = (step.attr('title') != '') ? step.attr('title') : '--';

        	description = step.children('legend:first').html(); // TODO: if description.

        	$titlesWrapper.append('
' + title  + '' + description + '
');

        	if (index == 0) {
        		if (size > 1) {
        			createNextButton(index);
        		}
        	} else {
        		createBackButton(index);
        		step.hide();

        		if (index < size - 1) {
	        		createNextButton(index);
	        	}
        	}
        });

        var $titles	= $titlesWrapper.children(),
        	finish	= $this.children('.finish');

        $titles.first().addClass('current-step');

		if (opt.finish) {
	        if (finish.length) {
        		finish.hide().click(function() {
        			validate($this, size - 1, opt);
        		})
        		.appendTo($this.find('p:last'));
	        } else if ($this.is('form')) {
	        	debug('Button with class "finish" missing!');
	        }
        }

        if (opt.titleClick) {
        	$titles.click(function() {
        		var clicked = parseInt($(this).attr('id').split('-')[2]),
        			current = parseInt($titlesWrapper.children('.current-step').attr('id').split('-')[2]),
        			maxStep = clicked;

				if (clicked > current) {									// Validate only clickeds steps ahead.
					if (isStopCallback(opt.onNext, clicked)) {
						return;
					}

					maxStep = getMaxStep($this, opt, clicked);
				} else if (clicked < current) {
					if (isStopCallback(opt.onBack, clicked)) {
						return;
					}
				}

				if (clicked != current) {									// Avoid change to the same step.
					selectStep($this, maxStep);

			        if (opt.finish && maxStep + 1 == size) {
	                	finish.show();
	                }
				}
        	});
    	} else {
    		$titles.css('cursor', 'default');
    	}

        $steps.delegate('input[type="text"], input[type="password"]', 'keypress', function(evt) {
        	var key = (evt.keyCode ? evt.keyCode : evt.which);

        	if (key == 13) {
        		evt.preventDefault();

        		var buttons = $(this).parent().children('p.' + id + '-buttons');

        		if (buttons.length) {
        			var next = buttons.children('a.button-next');

        			if (next.length) {
        				next.click();
        			} else {
	        			var finish = buttons.children('input.finish');

	        			if (finish.length) {
	        				finish.click();
	        			}
        			}
        		}
        	}
        });

        function isStopCallback(callback, clicked) {
        	var isValid = callback.apply($this, [clicked + 1]);

        	return !(isValid || isValid === undefined);
        };

        function createBackButton(index) {
        	$('', {
        		id:			id + '-back-' + index,
        		href:		'javascript:void(0);',
        		'class':	'button-back',
        		html:		opt.backLabel
        	}).click(function() {
        		if (!isStopCallback(opt.onBack, index - 1)) {
	                selectStep($this, index - 1);

	                if (index + 1 == size) {
	                	finish.hide();
	                }
        		}
            })
            .appendTo($('p#' + id + '-buttons-' + index));
        };

        function createNextButton(index) {
        	$('', {
        		id:			id + '-next-' + index,
        		href:		'javascript:void(0);',
        		'class':	'button-next',
        		html:		opt.nextLabel
        	}).click(function() {
        		if (!isStopCallback(opt.onNext, index + 1)) {
	        		var maxStep	= getMaxStep($this, opt, index + 1);

					selectStep($this, maxStep);

			        if (opt.finish && maxStep + 1 == size) {
	                	finish.show();
	                }
        		}
            })
            .appendTo($('p#' + id + '-buttons-' + index));
        };

        function getMaxStep(context, opt, clicked) {
        	var maxStep = clicked,
        		isValid = true;

        	if (opt.validate) {
	        	for (var i = 0; i < clicked; i++) {
					isValid = validate($this, i, opt) && isValid;	// Accumulates validations.

					if (opt.block && !isValid) {					// In the first invalid step the function stops or not.
						maxStep = i;								// The first invalid step.
						break;
					}
				}
        	}

        	return maxStep;
        };

		return $this;
	};

	function selectStep(context, index) {
		var id		= context.attr('id'),
			$steps	= context.children('fieldset'),
			size	= $steps.size(),
			$titles	= $('ul#' + id + '-titles').children(),
			step;

		if (index > size - 1) {
			index = size - 1;
		}

		$steps.hide().eq(index).show();

		$titles.removeClass('current-step').eq(index).addClass('current-step');

        if (context.is('form')) {
        	$steps
        		.eq(index)
	        		.find(':input:visible')
		        		.each(function() {
		        			step = $(this);

				        	if (!step.attr('disabled')) { // TODO: is
				    			step.focus();
				    			return false;
				        	}
		        		});
        }
	};

    function validate(context, index, opt) {
    	if (!context.is('form')) {
    		return true;
    	}

    	var id		= context.attr('id'),
    		isValid	= true,
    		step	= context.children('fieldset').eq(index),
    		titles	= $('ul#' + id + '-titles').children();

    	step.find(':input').each(function() {
    		isValid &= context.validate().element($(this));

    		if (isValid === undefined) {
    			isValid = true;
    		}

    		if (isValid) {
    			if (opt.errorImage) {
    				titles.eq(index).removeClass('error-image');
    			}
    		} else {
    			if (opt.block) {
    				selectStep(context, index);
    			}

    			if (opt.errorImage) {
    				titles.eq(index).addClass('error-image');
    			}

    			context.validate().focusInvalid();
    		}
    	});

    	return isValid;
    };

    $.fn.stepy.step = function(index, idOrClass) {
    	var context = getContext(index, idOrClass, 'step');

		if (idOrClass.indexOf('.') >= 0) {
			return;
		}

		selectStep(context, index - 1);

		$.fn.stepy;
	};

    function getContext(value, idOrClass, name) {
		var context = undefined;

		if (idOrClass == undefined) {
			debug('Specify an ID or class to be the target of the action.');
			return;
		}

		if (idOrClass) {
			if (idOrClass.indexOf('.') >= 0) {
				var idEach;

				return $(idOrClass).each(function() {
					idEach = '#' + $(this).attr('id');

					if (name == 'step') {
						$.fn.stepy.step(value, idEach);
					}
				});
			}

			context = $(idOrClass);

			if (!context.length) {
				debug('"' + idOrClass + '" is a invalid identifier for the public funtion $.fn.stepy.' + name + '().');
				return;
			}
		}

		return context;
	};

    function debug(message) {
		if (window.console && window.console.log) {
			window.console.log(message);
		}
	};

	$.fn.stepy.defaults = {
		backLabel:		'< Back',
		block:			false,
		errorImage:		false,
		finish:			true,
		onBack:			function(index) { return true; },
		onNext:			function(index) { return true; },
		nextLabel:		'Next >',
		titleClick:		false,
		titleTarget:	'',
		validate:		false
	};

})(jQuery);<commit_msg>Refatored the validation function.<commit_after>/**
 * jQuery Stepy - A Wizard Plugin - http://wbotelhos.com/stepy
 * ---------------------------------------------------------------------------------
 *
 * jQuery Stepy is a plugin based on FormToWizard that generates a customizable wizard.
 *
 * Licensed under The MIT License
 *
 * @version			0.4.1
 * @since			07.03.2010
 * @author			Washington Botelho dos Santos
 * @documentation	http://wbotelhos.com/raty
 * @twitter			http://twitter.com/wbotelhos
 * @license			http://opensource.org/licenses/mit-license.php MIT
 * @package			jQuery Plugins
 *
 * Usage with default values:
 * ---------------------------------------------------------------------------------
 * $('#stepy').stepy();
 *
 *	<form id="stepy">
 *		<fieldset title="Step 1">
 *			<legend>description one</legend>
 *
 *		</fieldset>
 *
 *		<fieldset title="Step 2">
 *			<legend>description one</legend>
 *
 *		</fieldset>
 *
 *		// and so on..
 *
 *		<input type="submit" class="finish" value="Finish!"/>
 *	</form>
 *
 */

;(function($) {

	$.fn.stepy = function(settings) {

		if (this.length == 0) {
			debug('Selector invalid or missing!');
			return;
		} else if (this.length > 1) {
			return this.each(function() {
				$.fn.stepy.apply($(this), [settings]);
			});
		}

		var opt				= $.extend({}, $.fn.stepy.defaults, settings),
			$this			= $(this),
			id				= this.attr('id'),
			$steps			= $this.children('fieldset'),
			size			= $steps.size(),
			$titlesWrapper	= $('
', { id: id + '-titles', 'class': 'stepy-titles' }),
			description		= '',
			title			= '',
			step;

		if (opt.titleTarget) {
			$(opt.titleTarget).html($titlesWrapper);
		} else {
			$titlesWrapper.insertBefore($this);
		}

		if (id === undefined) {
			id = 'stepy-' + $this.index();
			$this.attr('id', id);
		}

        if (opt.validate && $this.is('form')) {
        	$this.append('
');
        }

        $steps.each(function(index) { // fieldset
        	step = $(this);

        	step
        	.attr('id', id + '-step-' + index)
        	.addClass('step')
        	.append('
');

        	title = (step.attr('title') != '') ? step.attr('title') : '--';

        	description = step.children('legend:first').html(); // TODO: if description.

        	$titlesWrapper.append('
' + title  + '' + description + '
');

        	if (index == 0) {
        		if (size > 1) {
        			createNextButton(index);
        		}
        	} else {
        		createBackButton(index);
        		step.hide();

        		if (index < size - 1) {
	        		createNextButton(index);
	        	}
        	}
        });

        var $titles	= $titlesWrapper.children(),
        	finish	= $this.children('.finish');

        $titles.first().addClass('current-step');

		if (opt.finish) {
	        if (finish.length) {
        		finish.hide().click(function() {
        			validate($this, size - 1, opt);
        		})
        		.appendTo($this.find('p:last'));
	        } else if ($this.is('form')) {
	        	debug('Button with class "finish" missing!');
	        }
        }

        if (opt.titleClick) {
        	$titles.click(function() {
        		var clicked = parseInt($(this).attr('id').split('-')[2]),
        			current = parseInt($titlesWrapper.children('.current-step').attr('id').split('-')[2]),
        			maxStep = clicked;

				if (clicked > current) {									// Validate only clickeds steps ahead.
					if (isStopCallback(opt.onNext, clicked)) {
						return;
					}

					maxStep = getMaxStep($this, opt, clicked);
				} else if (clicked < current) {
					if (isStopCallback(opt.onBack, clicked)) {
						return;
					}
				}

				if (clicked != current) {									// Avoid change to the same step.
					selectStep($this, maxStep);

			        if (opt.finish && maxStep + 1 == size) {
	                	finish.show();
	                }
				}
        	});
    	} else {
    		$titles.css('cursor', 'default');
    	}

        $steps.delegate('input[type="text"], input[type="password"]', 'keypress', function(evt) {
        	var key = (evt.keyCode ? evt.keyCode : evt.which);

        	if (key == 13) {
        		evt.preventDefault();

        		var buttons = $(this).parent().children('p.' + id + '-buttons');

        		if (buttons.length) {
        			var next = buttons.children('a.button-next');

        			if (next.length) {
        				next.click();
        			} else {
	        			var finish = buttons.children('input.finish');

	        			if (finish.length) {
	        				finish.click();
	        			}
        			}
        		}
        	}
        });

        function isStopCallback(callback, clicked) {
        	var isValid = callback.apply($this, [clicked + 1]);

        	return !(isValid || isValid === undefined);
        };

        function createBackButton(index) {
        	$('', {
        		id:			id + '-back-' + index,
        		href:		'javascript:void(0);',
        		'class':	'button-back',
        		html:		opt.backLabel
        	}).click(function() {
        		if (!isStopCallback(opt.onBack, index - 1)) {
	                selectStep($this, index - 1);

	                if (index + 1 == size) {
	                	finish.hide();
	                }
        		}
            })
            .appendTo($('p#' + id + '-buttons-' + index));
        };

        function createNextButton(index) {
        	$('', {
        		id:			id + '-next-' + index,
        		href:		'javascript:void(0);',
        		'class':	'button-next',
        		html:		opt.nextLabel
        	}).click(function() {
        		if (!isStopCallback(opt.onNext, index + 1)) {
	        		var maxStep	= getMaxStep($this, opt, index + 1);

					selectStep($this, maxStep);

			        if (opt.finish && maxStep + 1 == size) {
	                	finish.show();
	                }
        		}
            })
            .appendTo($('p#' + id + '-buttons-' + index));
        };

        function getMaxStep(context, opt, clicked) {
        	var maxStep = clicked,
        		isValid = true;

        	if (opt.validate) {
	        	for (var i = 0; i < clicked; i++) {
					isValid = validate($this, i, opt) && isValid;	// Accumulates validations.

					if (opt.block && !isValid) {					// In the first invalid step the function stops or not.
						maxStep = i;								// The first invalid step.
						break;
					}
				}
        	}

        	return maxStep;
        };

		return $this;
	};

	function selectStep(context, index) {
		var id		= context.attr('id'),
			$steps	= context.children('fieldset'),
			size	= $steps.size(),
			$titles	= $('ul#' + id + '-titles').children(),
			step;

		if (index > size - 1) {
			index = size - 1;
		}

		$steps.hide().eq(index).show();

		$titles.removeClass('current-step').eq(index).addClass('current-step');

        if (context.is('form')) {
        	$steps
        		.eq(index)
	        		.find(':input:visible')
		        		.each(function() {
		        			step = $(this);

				        	if (!step.attr('disabled')) { // TODO: is
				    			step.focus();
				    			return false;
				        	}
		        		});
        }
	};

	function validate(context, index, opt) {
		if (!context.is('form')) {
			return true;
		}

		var id		= context.attr('id'),
			isValid	= true,
			$step	= context.children('fieldset').eq(index),
			$title	= $('ul#' + id + '-titles').children().eq(index);

		$step.find(':input').each(function() {
			isValid &= context.validate().element($(this));

			if (isValid === undefined) {
				isValid = true;
			}

			if (isValid) {
				if (opt.errorImage) {
					$title.removeClass('error-image');
				}
			} else {
				if (opt.block) {
					selectStep(context, index);
				}

				if (opt.errorImage) {
					$title.addClass('error-image');
				}

				context.validate().focusInvalid();
			}
		});

		return isValid;
	};

    $.fn.stepy.step = function(index, idOrClass) {
    	var context = getContext(index, idOrClass, 'step');

		if (idOrClass.indexOf('.') >= 0) {
			return;
		}

		selectStep(context, index - 1);

		$.fn.stepy;
	};

    function getContext(value, idOrClass, name) {
		var context = undefined;

		if (idOrClass == undefined) {
			debug('Specify an ID or class to be the target of the action.');
			return;
		}

		if (idOrClass) {
			if (idOrClass.indexOf('.') >= 0) {
				var idEach;

				return $(idOrClass).each(function() {
					idEach = '#' + $(this).attr('id');

					if (name == 'step') {
						$.fn.stepy.step(value, idEach);
					}
				});
			}

			context = $(idOrClass);

			if (!context.length) {
				debug('"' + idOrClass + '" is a invalid identifier for the public funtion $.fn.stepy.' + name + '().');
				return;
			}
		}

		return context;
	};

    function debug(message) {
		if (window.console && window.console.log) {
			window.console.log(message);
		}
	};

	$.fn.stepy.defaults = {
		backLabel:		'< Back',
		block:			false,
		errorImage:		false,
		finish:			true,
		onBack:			function(index) { return true; },
		onNext:			function(index) { return true; },
		nextLabel:		'Next >',
		titleClick:		false,
		titleTarget:	'',
		validate:		false
	};

})(jQuery);<|endoftext|>