<commit_before>var pg = require('pg')

var reDup    = /^duplicate key value/
  , reExists = /already exists/

var setup =
  [ "CREATE TABLE $env_metrics_keys ("
  + "  metrics_key varchar(512) NOT NULL CONSTRAINT $env_key_idx UNIQUE,"
  + "  type varchar(255) NOT NULL,"
  + "  create_time timestamp NOT NULL default NOW()"
  + ")"
  , "CREATE TABLE $env_metrics_data ("
  + "  metrics_key varchar(512) NOT NULL,"
  + "  time_start bigint NOT NULL,"
  + "  data text NOT NULL"
  + ")"
  , "CREATE UNIQUE INDEX $env_key_time_idx ON $env_metrics_data (metrics_key, time_start)"

  , "CREATE TABLE $env_metrics_tags ("
  + "  id varchar(255) NOT NULL,"
  + "  ts bigint NOT NULL,"
  + "  data text NOT NULL"
  + ")"
  , makeKVIndex("tags")
  , "CREATE INDEX $env_tag_time_idx ON $env_metrics_tags (ts)"

  , makeKVTable("dashboards"), makeKVIndex("dashboards")
  , makeKVTable("rules"),      makeKVIndex("rules")
  , makeKVTable("tagtypes"),   makeKVIndex("tagtypes")
  ]

function makeKVTable(table) {
  return "CREATE TABLE $env_metrics_" + table + " ("
       + "  id varchar(255) NOT NULL,"
       + "  data text NOT NULL"
       + ")"
}

function makeKVIndex(table) {
  return "CREATE UNIQUE INDEX $env_" + table + "_idx ON $env_metrics_" + table + " (id)"
}

function noop() {}

var fakeAgent = {histogram: noop, counter: noop}

module.exports = PostgresBackend

/// PostgresBackend is the bridge to the backend (in this case, Postgres).
/// It persists metrics points, keys, etc.
///
/// options -
///   env   - String
///   db    - The connection string.
///   agent - MetricsAgent (optional)
///   onError(err)
///
function PostgresBackend(options) {
  this.client  = null
  this.url     = options.db
  this.env     = options.env
  this.agent   = options.agent   || fakeAgent
  this.onError = options.onError || noop
  this.reconnect()

  // Tables
  this.tKeys       = this.env + "_metrics_keys"
  this.tData       = this.env + "_metrics_data"
  this.tTags       = this.env + "_metrics_tags"
  this.tRules      = this.env + "_metrics_rules"
  this.tDashboards = this.env + "_metrics_dashboards"
  this.tTagTypes   = this.env + "_metrics_tagtypes"
  this.reTable     = new RegExp("\\b(" + this.env + "_metrics_\\w+)\\b")

  var _this = this
  this._onKeyInsert   = function(err) { _this.onKeyInsert(err) }
  this._onPointInsert = function(err) { _this.onPointInsert(err) }
}

PostgresBackend.prototype.close = function() { this.client.end() }

PostgresBackend.prototype.setup = function(callback) {
  var _this = this
    , env   = this.env
    , i     = 0

  next()
  function next() {
    var sql = setup[i]
    if (!sql) return callback()
    _this.query(sql.replace(/[$]env/g, env), [], done)
  }

  function done(err) {
    if (err && !reExists.test(err.message)) {
      callback(err)
    } else next(++i)
  }
}

///
/// Metrics
///

// mkey  - String
// start - Integer timestamp (ms)
// end   - Integer timestamp (ms)
// callback(err, [point])
PostgresBackend.prototype.getPoints = function(mkey, start, end, callback) {
  this.query
  ( "SELECT * FROM " + this.tData + " "
  + "WHERE metrics_key=$1 "
  + "AND time_start>=$2 "
  + "AND time_start<=$3 "
  + "ORDER BY time_start",
  [mkey, toHour(start), toHour(end)], function(err, res) {
    if (err) return callback(err)
    var rows   = res.rows
      , points = []
    for (var i = 0, off = 0; i < rows.length; i++) {
      appendPoints(points, parseRow(rows[i].data), start, end)
    }
    callback(null, points)
  })
}

function appendPoints(allPoints, newPoints, start, end) {
  var last
  for (var i = 0; i < newPoints.length; i++) {
    var pt = newPoints[i]
      , ts = pt.ts
    if (ts !== last && start <= ts && ts <= end) {
      allPoints.push(pt)
    }
    last = ts
  }
}

// points - { mkey : point }
PostgresBackend.prototype.savePoints = function(points) {
  var mkeys = Object.keys(points)
    , tKeys = this.tKeys

  for (var i = 0; i < mkeys.length; i++) {
    var mkey = mkeys[i]
      , pt   = points[mkey]
      , type = identify(pt)
    this.query
    ( "INSERT INTO " + tKeys + " "
    + "SELECT $1,$2 "
    + "WHERE NOT EXISTS "
    + "( SELECT 1 FROM " + tKeys + " "
    +   "WHERE metrics_key=$3 )",
    [mkey, type, mkey], this._onKeyInsert)
    this.savePoint(mkey, pt, this._onKeyInsert)
  }
}

function identify(pt) {
  return pt.mean !== undefined ? "histogram"
       : pt.data !== undefined ? "llq"
       : "counter"
}

var msHour = 1000 * 60 * 60

function toHour(ms) { return ms - (ms % msHour) }

PostgresBackend.prototype.savePoint = function(mkey, pt, callback) {
  var _this = this
    , chunk = toHour(pt.ts)
    , done  = callback || this._onPointInsert
  this.query
  ( "SELECT data "
  + "FROM " + this.tData + " "
  + "WHERE metrics_key=$1 AND time_start=$2 "
  + "LIMIT 1",
  [mkey, chunk], function(err, res) {
    if (err) return done(err)
    var rows   = res    && res.rows
      , points = rows   && rows[0] && parseRow(rows[0].data)
      , count  = points && points.length
    if (points) {
      if (count && points[count - 1].ts === pt.ts) {
        return
      }
      points.push(pt)
      _this.query
      ( "UPDATE " + _this.tData + " "
      + "SET data=$1 "
      + "WHERE metrics_key=$2 AND time_start=$3",
      [JSON.stringify(points), mkey, chunk], done)
    } else {
      _this.query
      ( "INSERT INTO " + _this.tData + " VALUES($1, $2, $3)",
      [mkey, chunk, JSON.stringify([pt])], done)
    }
  })
  //this.query("INSERT INTO " + this.tData + " VALUES($1, $2, $3)", [mkey, pt.ts, JSON.stringify(pt)], callback || this._onPointInsert)
}

PostgresBackend.prototype.onKeyInsert = function(err) {
  if (err && !reDup.test(err.message)) this.onError(err)
}

PostgresBackend.prototype.onPointInsert = function(err) {
  if (err) this.onError(err)
}

// callback(err, [{key, type}])
PostgresBackend.prototype.getMetricsKeys = function(callback) {
  this.query("SELECT * FROM " + this.tKeys, [], function(err, res) {
    callback(err, res && res.rows && res.rows.map(toMetricsKey))
  })
}

// Remove the key from the metrics_keys set, but dont actually delete
// all of the historical points in metrics_data.
PostgresBackend.prototype.deleteMetricsKey = function(mkey, callback) {
  this.query("DELETE FROM " + this.tKeys + " WHERE metrics_key=$1", [mkey], callback)
}

function toMetricsKey(data) {
  var row = parseRow(data)
  return { key:  row.metrics_key
         , type: row.type
         }
}

///
/// Tags
///

// begin - Integer timestamp (ms)
// end   - Integer timestamp (ms)
PostgresBackend.prototype.getTagRange = function(begin, end, callback) {
  this.query
  ( "SELECT data FROM " + this.tTags + " "
  + "WHERE ts>=$1 "
  + "AND   ts<=$2 "
  + "ORDER BY ts",
  [begin, end], function(err, res) {
    if (err) return callback(err)
    callback(err, res && res.rows && res.rows.map(getData))
  })
}

// tag - {ts, label, color[, id]}
PostgresBackend.prototype.setTag = function(tag, callback) {
  var id    = tag.id = tag.id || (tag.ts + "_" + digits(3))
    , json  = JSON.stringify(tag)
    , _this = this
  this.query("INSERT INTO " + this.tTags + " VALUES($1, $2, $3)", [id, tag.ts, json], function(err) {
    if (!err) return callback()
    _this.query("UPDATE " + this.tTags + " SET json=$1 WHERE id=$2", [json, id], callback)
  })
}

PostgresBackend.prototype.deleteTag = function(tagID, callback) {
  this._del(this.tTags, tagID, callback)
}

function digits(n) {
  return Math.floor(Math.random() * Math.pow(10, n))
}

///
/// Tag types
///

// Fetch all tag types at once. Not a big deal since there shouldn't be more
// than a dozen or so.
//
// callback(err, {id, color, name})
//
PostgresBackend.prototype.getTagTypes = function(callback) {
  this.query("SELECT * FROM " + this.tTagTypes, [], function(err, res) {
    var rows = res && res.rows
    callback(err, rows && rows.map(rowToTagType))
  })
}

function rowToTagType(row) {
  var data = parseRow(row.data)
  data.id = row.id
  return data
}

// typeOpts - {color, name}
// callback(err)
PostgresBackend.prototype.createTagType = function(typeOpts, callback) {
  var id = Date.now() + "_" + digits(3)
  this._set(this.tTagTypes, id, typeOpts, callback)
}

// typeID - String
// callback(err)
PostgresBackend.prototype.deleteTagType = function(typeID, callback) {
  this._del(this.tTagTypes, typeID, callback)
}

///
/// Rules
///

PostgresBackend.prototype.getRule = function(mkey, callback) {
  this._get(this.tRules, mkey, callback)
}

PostgresBackend.prototype.setRule = function(mkey, rule, callback) {
  this._set(this.tRules, mkey, rule, callback)
}

///
/// Dashboards
///

PostgresBackend.prototype.getDashboard = function(id, callback) {
  this._get(this.tDashboards, id, callback)
}

PostgresBackend.prototype.setDashboard = function(id, dashboard, callback) {
  this._set(this.tDashboards, id, dashboard, callback)
}

PostgresBackend.prototype.deleteDashboard = function(id, callback) {
  this._del(this.tDashboards, id, callback)
}

// List all dashboard IDs.
PostgresBackend.prototype.listDashboards = function(callback) {
  this.query("SELECT id FROM " + this.tDashboards, [], function(err, res) {
    callback(err, res && res.rows && res.rows.map(getID))
  })
}

// Like `client.query`, but record latency and errors.
PostgresBackend.prototype.query = function(sql, params, callback) {
  var agent = this.agent
    , start = Date.now()
    , index = sql.indexOf(" ")
    , cmd   = index === -1 ? sql : sql.slice(0, index)
    , table = this.reTable.exec(sql)

  table = table ? table[1] : "unknown"
  this.client.query(sql, params, function(err, res) {
    agent.histogram("timing|" + cmd + "|" + table, Date.now() - start)
    if (err) {
      agent.counter("error|" + cmd + "|" + table)
    }

    if (callback) callback(err, res)
  })
}

PostgresBackend.prototype.reconnect = function() {
  var isReconnect = !!this.client
  if (this.client) this.client.end()
  this.client = new pg.Client(this.url)
  this.client.on("error", this.onConnectionError.bind(this))

  var _this = this
  this.client.connect(function(err) {
    if (err) {
      if (!isReconnect) throw err
      setTimeout(_this.reconnect.bind(_this), 1000)
    }
  })
}

PostgresBackend.prototype.onConnectionError = function(err) {
  this.onError(err)
  this.reconnect()
}

///
/// KV
///

PostgresBackend.prototype._get = function(table, id, callback) {
  this.query("SELECT data FROM " + table + " WHERE id=$1 LIMIT 1", [id], pluckFirstData(id, callback))
}

// Upsert
PostgresBackend.prototype._set = function(table, id, data, callback) {
  var json  = JSON.stringify(data)
    , _this = this
  this.query("INSERT INTO " + table + " VALUES($1, $2)", [id, json], function(err) {
    if (!err) return callback()
    _this.query("UPDATE " + table + " SET data=$1 WHERE id=$2", [json, id], callback)
  })
}

PostgresBackend.prototype._del = function(table, id, callback) {
  this.query("DELETE FROM " + table + " WHERE id=$1", [id], callback)
}

function getID(row) { return row.id }

function getData(row) { return parseRow(row.data) }

function pluckFirstData(id, callback) {
  return function(err, res) {
    var rows = res && res.rows
      , val  = rows && rows[0] && parseRow(rows[0].data)
    if (val) val.id = id
    callback(err, val)
  }
}

function parseRow(row) {
  return typeof row === "string" ? JSON.parse(row) : row
}
<commit_msg>fix (apparent) data loss due to V8 clock bug<commit_after>var pg = require('pg')

var reDup    = /^duplicate key value/
  , reExists = /already exists/

var setup =
  [ "CREATE TABLE $env_metrics_keys ("
  + "  metrics_key varchar(512) NOT NULL CONSTRAINT $env_key_idx UNIQUE,"
  + "  type varchar(255) NOT NULL,"
  + "  create_time timestamp NOT NULL default NOW()"
  + ")"
  , "CREATE TABLE $env_metrics_data ("
  + "  metrics_key varchar(512) NOT NULL,"
  + "  time_start bigint NOT NULL,"
  + "  data text NOT NULL"
  + ")"
  , "CREATE UNIQUE INDEX $env_key_time_idx ON $env_metrics_data (metrics_key, time_start)"

  , "CREATE TABLE $env_metrics_tags ("
  + "  id varchar(255) NOT NULL,"
  + "  ts bigint NOT NULL,"
  + "  data text NOT NULL"
  + ")"
  , makeKVIndex("tags")
  , "CREATE INDEX $env_tag_time_idx ON $env_metrics_tags (ts)"

  , makeKVTable("dashboards"), makeKVIndex("dashboards")
  , makeKVTable("rules"),      makeKVIndex("rules")
  , makeKVTable("tagtypes"),   makeKVIndex("tagtypes")
  ]

function makeKVTable(table) {
  return "CREATE TABLE $env_metrics_" + table + " ("
       + "  id varchar(255) NOT NULL,"
       + "  data text NOT NULL"
       + ")"
}

function makeKVIndex(table) {
  return "CREATE UNIQUE INDEX $env_" + table + "_idx ON $env_metrics_" + table + " (id)"
}

function noop() {}

var fakeAgent = {histogram: noop, counter: noop}

module.exports = PostgresBackend

/// PostgresBackend is the bridge to the backend (in this case, Postgres).
/// It persists metrics points, keys, etc.
///
/// options -
///   env   - String
///   db    - The connection string.
///   agent - MetricsAgent (optional)
///   onError(err)
///
function PostgresBackend(options) {
  this.client  = null
  this.url     = options.db
  this.env     = options.env
  this.agent   = options.agent   || fakeAgent
  this.onError = options.onError || noop
  this.reconnect()

  // Tables
  this.tKeys       = this.env + "_metrics_keys"
  this.tData       = this.env + "_metrics_data"
  this.tTags       = this.env + "_metrics_tags"
  this.tRules      = this.env + "_metrics_rules"
  this.tDashboards = this.env + "_metrics_dashboards"
  this.tTagTypes   = this.env + "_metrics_tagtypes"
  this.reTable     = new RegExp("\\b(" + this.env + "_metrics_\\w+)\\b")

  var _this = this
  this._onKeyInsert   = function(err) { _this.onKeyInsert(err) }
  this._onPointInsert = function(err) { _this.onPointInsert(err) }
}

PostgresBackend.prototype.close = function() { this.client.end() }

PostgresBackend.prototype.setup = function(callback) {
  var _this = this
    , env   = this.env
    , i     = 0

  next()
  function next() {
    var sql = setup[i]
    if (!sql) return callback()
    _this.query(sql.replace(/[$]env/g, env), [], done)
  }

  function done(err) {
    if (err && !reExists.test(err.message)) {
      callback(err)
    } else next(++i)
  }
}

///
/// Metrics
///

// mkey  - String
// start - Integer timestamp (ms)
// end   - Integer timestamp (ms)
// callback(err, [point])
PostgresBackend.prototype.getPoints = function(mkey, start, end, callback) {
  this.query
  ( "SELECT * FROM " + this.tData + " "
  + "WHERE metrics_key=$1 "
  + "AND time_start>=$2 "
  + "AND time_start<=$3 "
  + "ORDER BY time_start",
  [mkey, toHour(start), toHour(end)], function(err, res) {
    if (err) return callback(err)
    var rows   = res.rows
      , points = []
    for (var i = 0, off = 0; i < rows.length; i++) {
      appendPoints(points, parseRow(rows[i].data), start, end)
    }
    callback(null, points)
  })
}

function appendPoints(allPoints, newPoints, start, end) {
  var last = 0
  for (var i = 0; i < newPoints.length; i++) {
    var pt = newPoints[i]
      , ts = pt.ts
    if (ts > last && start <= ts && ts <= end) {
      allPoints.push(pt)
    }
    last = ts
  }
}

// points - { mkey : point }
PostgresBackend.prototype.savePoints = function(points) {
  var mkeys = Object.keys(points)
    , tKeys = this.tKeys

  for (var i = 0; i < mkeys.length; i++) {
    var mkey = mkeys[i]
      , pt   = points[mkey]
      , type = identify(pt)
    this.query
    ( "INSERT INTO " + tKeys + " "
    + "SELECT $1,$2 "
    + "WHERE NOT EXISTS "
    + "( SELECT 1 FROM " + tKeys + " "
    +   "WHERE metrics_key=$3 )",
    [mkey, type, mkey], this._onKeyInsert)
    this.savePoint(mkey, pt, this._onKeyInsert)
  }
}

function identify(pt) {
  return pt.mean !== undefined ? "histogram"
       : pt.data !== undefined ? "llq"
       : "counter"
}

var msHour = 1000 * 60 * 60

function toHour(ms) { return ms - (ms % msHour) }

PostgresBackend.prototype.savePoint = function(mkey, pt, callback) {
  var _this = this
    , chunk = toHour(pt.ts)
    , done  = callback || this._onPointInsert
  this.query
  ( "SELECT data "
  + "FROM " + this.tData + " "
  + "WHERE metrics_key=$1 AND time_start=$2 "
  + "LIMIT 1",
  [mkey, chunk], function(err, res) {
    if (err) return done(err)
    var rows   = res    && res.rows
      , points = rows   && rows[0] && parseRow(rows[0].data)
      , count  = points && points.length
    if (points) {
      if (count && points[count - 1].ts === pt.ts) {
        return
      }
      points.push(pt)
      _this.query
      ( "UPDATE " + _this.tData + " "
      + "SET data=$1 "
      + "WHERE metrics_key=$2 AND time_start=$3",
      [JSON.stringify(points), mkey, chunk], done)
    } else {
      _this.query
      ( "INSERT INTO " + _this.tData + " VALUES($1, $2, $3)",
      [mkey, chunk, JSON.stringify([pt])], done)
    }
  })
  //this.query("INSERT INTO " + this.tData + " VALUES($1, $2, $3)", [mkey, pt.ts, JSON.stringify(pt)], callback || this._onPointInsert)
}

PostgresBackend.prototype.onKeyInsert = function(err) {
  if (err && !reDup.test(err.message)) this.onError(err)
}

PostgresBackend.prototype.onPointInsert = function(err) {
  if (err) this.onError(err)
}

// callback(err, [{key, type}])
PostgresBackend.prototype.getMetricsKeys = function(callback) {
  this.query("SELECT * FROM " + this.tKeys, [], function(err, res) {
    callback(err, res && res.rows && res.rows.map(toMetricsKey))
  })
}

// Remove the key from the metrics_keys set, but dont actually delete
// all of the historical points in metrics_data.
PostgresBackend.prototype.deleteMetricsKey = function(mkey, callback) {
  this.query("DELETE FROM " + this.tKeys + " WHERE metrics_key=$1", [mkey], callback)
}

function toMetricsKey(data) {
  var row = parseRow(data)
  return { key:  row.metrics_key
         , type: row.type
         }
}

///
/// Tags
///

// begin - Integer timestamp (ms)
// end   - Integer timestamp (ms)
PostgresBackend.prototype.getTagRange = function(begin, end, callback) {
  this.query
  ( "SELECT data FROM " + this.tTags + " "
  + "WHERE ts>=$1 "
  + "AND   ts<=$2 "
  + "ORDER BY ts",
  [begin, end], function(err, res) {
    if (err) return callback(err)
    callback(err, res && res.rows && res.rows.map(getData))
  })
}

// tag - {ts, label, color[, id]}
PostgresBackend.prototype.setTag = function(tag, callback) {
  var id    = tag.id = tag.id || (tag.ts + "_" + digits(3))
    , json  = JSON.stringify(tag)
    , _this = this
  this.query("INSERT INTO " + this.tTags + " VALUES($1, $2, $3)", [id, tag.ts, json], function(err) {
    if (!err) return callback()
    _this.query("UPDATE " + this.tTags + " SET json=$1 WHERE id=$2", [json, id], callback)
  })
}

PostgresBackend.prototype.deleteTag = function(tagID, callback) {
  this._del(this.tTags, tagID, callback)
}

function digits(n) {
  return Math.floor(Math.random() * Math.pow(10, n))
}

///
/// Tag types
///

// Fetch all tag types at once. Not a big deal since there shouldn't be more
// than a dozen or so.
//
// callback(err, {id, color, name})
//
PostgresBackend.prototype.getTagTypes = function(callback) {
  this.query("SELECT * FROM " + this.tTagTypes, [], function(err, res) {
    var rows = res && res.rows
    callback(err, rows && rows.map(rowToTagType))
  })
}

function rowToTagType(row) {
  var data = parseRow(row.data)
  data.id = row.id
  return data
}

// typeOpts - {color, name}
// callback(err)
PostgresBackend.prototype.createTagType = function(typeOpts, callback) {
  var id = Date.now() + "_" + digits(3)
  this._set(this.tTagTypes, id, typeOpts, callback)
}

// typeID - String
// callback(err)
PostgresBackend.prototype.deleteTagType = function(typeID, callback) {
  this._del(this.tTagTypes, typeID, callback)
}

///
/// Rules
///

PostgresBackend.prototype.getRule = function(mkey, callback) {
  this._get(this.tRules, mkey, callback)
}

PostgresBackend.prototype.setRule = function(mkey, rule, callback) {
  this._set(this.tRules, mkey, rule, callback)
}

///
/// Dashboards
///

PostgresBackend.prototype.getDashboard = function(id, callback) {
  this._get(this.tDashboards, id, callback)
}

PostgresBackend.prototype.setDashboard = function(id, dashboard, callback) {
  this._set(this.tDashboards, id, dashboard, callback)
}

PostgresBackend.prototype.deleteDashboard = function(id, callback) {
  this._del(this.tDashboards, id, callback)
}

// List all dashboard IDs.
PostgresBackend.prototype.listDashboards = function(callback) {
  this.query("SELECT id FROM " + this.tDashboards, [], function(err, res) {
    callback(err, res && res.rows && res.rows.map(getID))
  })
}

// Like `client.query`, but record latency and errors.
PostgresBackend.prototype.query = function(sql, params, callback) {
  var agent = this.agent
    , start = Date.now()
    , index = sql.indexOf(" ")
    , cmd   = index === -1 ? sql : sql.slice(0, index)
    , table = this.reTable.exec(sql)

  table = table ? table[1] : "unknown"
  this.client.query(sql, params, function(err, res) {
    agent.histogram("timing|" + cmd + "|" + table, Date.now() - start)
    if (err) {
      agent.counter("error|" + cmd + "|" + table)
    }

    if (callback) callback(err, res)
  })
}

PostgresBackend.prototype.reconnect = function() {
  var isReconnect = !!this.client
  if (this.client) this.client.end()
  this.client = new pg.Client(this.url)
  this.client.on("error", this.onConnectionError.bind(this))

  var _this = this
  this.client.connect(function(err) {
    if (err) {
      if (!isReconnect) throw err
      setTimeout(_this.reconnect.bind(_this), 1000)
    }
  })
}

PostgresBackend.prototype.onConnectionError = function(err) {
  this.onError(err)
  this.reconnect()
}

///
/// KV
///

PostgresBackend.prototype._get = function(table, id, callback) {
  this.query("SELECT data FROM " + table + " WHERE id=$1 LIMIT 1", [id], pluckFirstData(id, callback))
}

// Upsert
PostgresBackend.prototype._set = function(table, id, data, callback) {
  var json  = JSON.stringify(data)
    , _this = this
  this.query("INSERT INTO " + table + " VALUES($1, $2)", [id, json], function(err) {
    if (!err) return callback()
    _this.query("UPDATE " + table + " SET data=$1 WHERE id=$2", [json, id], callback)
  })
}

PostgresBackend.prototype._del = function(table, id, callback) {
  this.query("DELETE FROM " + table + " WHERE id=$1", [id], callback)
}

function getID(row) { return row.id }

function getData(row) { return parseRow(row.data) }

function pluckFirstData(id, callback) {
  return function(err, res) {
    var rows = res && res.rows
      , val  = rows && rows[0] && parseRow(rows[0].data)
    if (val) val.id = id
    callback(err, val)
  }
}

function parseRow(row) {
  return typeof row === "string" ? JSON.parse(row) : row
}
<|endoftext|>


<commit_before>/**
 * @author Vladimir Kozhin
 * @license MIT
 */

'use strict';

const Jii = require('../BaseJii');
const InvalidConfigException = require('../exceptions/InvalidConfigException');
const InvalidParamException = require('../exceptions/InvalidParamException');
const Collection = require('../base/Collection');
const Pagination = require('../data/Pagination');
const FetchEvent = require('../data/FetchEvent');
const DataProviderEvent = require('../data/DataProviderEvent');
const _isNumber = require('lodash/isNumber');
const _isArray = require('lodash/isArray');
const _isObject = require('lodash/isObject');
const _isFunction = require('lodash/isFunction');
const _findKey = require('lodash/findKey');
const _has = require('lodash/has');

class DataProvider extends Collection {

    preInit() {
        /**
         * @type {object}
         */
        this._fetchedKeys = {};

        /**
         * @type {function[]|null}
         */
        this._fetchCallbacks = null;

        /**
         * @type {number|null}
         */
        this._totalCount = 0;

        /**
         * @type {Jii.data.Pagination|boolean}
         */
        this._pagination = null;

        /**
         * @type {Jii.data.Sort}
         */
        this._sort = null;

        /**
         * @type {boolean}
         */
        this.autoFetch = true;

        /**
         * @type {function|Jii.data.Query}
         */
        this.query = null;

        /**
         * @type {string|null} an ID that uniquely identifies the data provider among all data providers.
         * You should set this property if the same page contains two or more different data providers.
         * Otherwise, the [[pagination]] and [[sort]] may not work properly.
         */
        this.id = null;

        super.preInit(...arguments);
    }

    init() {
        if (this.autoFetch) {
            this.fetch();
        }
    }

    /**
     *
     * @param {boolean} [force]
     * @return {*}
     */
    fetch(force) {
        // Queue promises when fetch in process
        if (this._fetchCallbacks !== null) {
            return new Promise(resolve => {
                this._fetchCallbacks.push(resolve);
            });
        }

        if (this.isFetched() && !force) {
            this.refreshFilter();
            return Promise.resolve(false);
        }

        this.trigger(DataProvider.EVENT_BEFORE_FETCH, new FetchEvent({
            isLoading: true
        }));
        this.trigger(DataProvider.EVENT_LOADING, new FetchEvent({
            isLoading: true
        }));

        this._fetchCallbacks = [];
        return Promise.resolve().then(() => {

            // Query as function
            if (_isFunction(this.query)) {
                return this.query(this.getPagination());
            }

            // TODO Query, REST, ...
            throw new InvalidConfigException('Wrong query format in DataProvider.');
        }).then(data => {
            // Validate response
            if (!data) {
                throw new InvalidParamException('Result data is not object in DataProvider.fetch().');
            }
            if (data.totalCount && !_isNumber(data.totalCount)) {
                throw new InvalidParamException('Result param "totalCount" must be number in DataProvider.fetch().');
            }
            if (!_isArray(data.models)) {
                throw new InvalidParamException('Result param "models" must be array in DataProvider.fetch().');
            }

            if (_isNumber(data.totalCount)) {
                this.setTotalCount(data.totalCount);
            }

            // No changes, but fetch
            if (!this.isFetched() && data.models.length === 0 && this.length === 0) {
                this.trigger(DataProvider.EVENT_FETCHED, this._createEvent({
                    isFetch: true
                }));
            } else {
                this.setModels(data.models);
                this.refreshFilter();
            }

            // Resolve queue promises after current
            var callbacks = this._fetchCallbacks;
            this._fetchCallbacks = null;
            setTimeout(() => {
                callbacks.forEach(callback => {
                    callback(data.models);
                });
            });

            this.trigger(DataProvider.EVENT_AFTER_FETCH, new FetchEvent({
                isLoading: false
            }));
            this.trigger(DataProvider.EVENT_LOADING, new FetchEvent({
                isLoading: false
            }));

            return data;
        });
    }

    isFetched() {
        if (!super.isFetched()) {
            return false;
        }

        var pagination = this.getPagination();
        if (pagination) {
            let needFetch = false;
            pagination.getIndexes().forEach(index => {
                if (!_has(this._fetchedKeys, index.toString()) && index < this.getTotalCount() - 1) {
                    needFetch = true;
                }
            });
            return !needFetch;
        }

        return true;
    }

    /**
     * Returns the total number of data models.
     * When [[pagination]] is false, this returns the same value as [[count]].
     * Otherwise, it will call [[prepareTotalCount()]] to get the count.
     * @returns {number} total number of possible data models.
     */
    getTotalCount() {
        if (this._pagination === false) {
            return this.parent ? this.parent.getCount() : this.getCount();
        }
        return this._totalCount;
    }

    /**
     * Sets the total number of data models.
     * @param {number} value the total number of data models.
     */
    setTotalCount(value) {
        this._totalCount = value;

        if (this._pagination) {
            this._pagination.totalCount = value;
        }
    }

    /**
     * Returns the pagination object used by this data provider.
     * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values
     * of [[Pagination.totalCount]] and [[Pagination.pageCount]].
     * @returns {Jii.data.Pagination|boolean} the pagination object. If this is false, it means the pagination is disabled.
     */
    getPagination() {
        if (this._pagination === null) {
            this.setPagination({});
        }

        return this._pagination;
    }

    /**
     * Sets the pagination for this data provider.
     * @param {object|Jii.data.Pagination|boolean} value the pagination to be used by this data provider.
     * @throws InvalidParamException
     */
    setPagination(value) {
        if (_isObject(value)) {
            let config = {
                className: Pagination,
                totalCount: this.getTotalCount()
            };
            if (this.id !== null) {
                config.pageParam = `${ this.id }-page`;
                config.pageSizeParam = `${ this.id }-per-page`;
            }
            this._pagination = Jii.createObject(Jii.mergeConfigs(config, value));
        } else if (value instanceof Pagination || value === false) {
            this._pagination = value;
        } else {
            throw new InvalidParamException('Only Pagination instance, configuration object or false is allowed.');
        }

        this._pagination.on(Pagination.EVENT_CHANGE, this._onPaginationChange.bind(this));
    }

    /**
     * @returns {Jii.data.Sort|boolean} the sorting object. If this is false, it means the sorting is disabled.
     */
    getSort() {
        if (this._sort === null) {
            this.setSort({});
        }

        return this._sort;
    }

    /**
     * Sets the sort definition for this data provider.
     * @param {object|Jii.data.Sort|boolean} value the sort definition to be used by this data provider.
     * This can be one of the following:
     *
     * - a configuration array for creating the sort definition object. The "class" element defaults
     *   to 'jii\data\Sort'
     * - an instance of [[Sort]] or its subclass
     * - false, if sorting needs to be disabled.
     *
     * @throws InvalidParamException
     */
    setSort(value) {
        if (_isObject(value)) {
            let config = {};
            // @todo Sort implementation
            if (this.id !== null) {
                config.sortParam = `${ this.id }-sort`;
            }
            this._sort = Jii.createObject(Jii.mergeConfigs(config, value));
        } else if (/*value instanceof Sort ||*/
        value === false) {
            // @todo Sort implementation
            this._sort = value;
        } else {
            throw new InvalidParamException('Only Sort instance, configuration object or false is allowed.');
        }
    }

    _change(startIndex, toAdd, toRemove, unique, parentCallParams) {
        if (parentCallParams) {
            var pagination = this.getPagination();
            if (pagination) {
                toRemove.forEach(model => {
                    let primaryKey = this._getPrimaryKey(model);
                    var finedKey = _findKey(this._fetchedKeys, key => {
                        return key === primaryKey;
                    });
                    if (finedKey) {
                        delete this._fetchedKeys[finedKey];
                    }
                });

                parentCallParams.toAdd.forEach((model, index) => {
                    this._fetchedKeys[index + pagination.getOffset()] = this._getPrimaryKey(model);
                });

                this.refreshFilter();
                return;
            }
        }

        super._change(startIndex, toAdd, toRemove, unique);
    }

    _onPaginationChange() {
        if (this.autoFetch) {
            this.fetch();
        }
    }

    _filterModels() {
        var pagination = this.getPagination();
        if (pagination) {
            if (!this.parent) {
                throw new InvalidConfigException('DataProvider with pagination need parent collection.');
            }

            const modelsPage = super._filterModels(pagination.getIndexes().map(i => {
                return this.parent._byId[this._fetchedKeys[i]] || null;
            }).filter(model => model !== null));

            return super._filterModels(modelsPage);
        }

        return super._filterModels();
    }

    /**
     *
     * @param {object} params
     * @returns {Jii.data.CollectionEvent}
     */
    _createEvent(params) {
        params.totalCount = this.getTotalCount();
        return new DataProviderEvent(params);
    }

}

/**
 * @event Jii.data.DataProvider#loading
 * @property {Jii.data.FetchEvent} event
 */
DataProvider.EVENT_LOADING = 'loading';

/**
 * @event Jii.data.DataProvider#after_fetch
 * @property {Jii.data.FetchEvent} event
 */
DataProvider.EVENT_AFTER_FETCH = 'after_fetch';

/**
 * @event Jii.data.DataProvider#before_fetch
 * @property {Jii.data.FetchEvent} event
 */
DataProvider.EVENT_BEFORE_FETCH = 'before_fetch';
module.exports = DataProvider;<commit_msg>code review<commit_after>/**
 * @author Vladimir Kozhin
 * @license MIT
 */

'use strict';

const Jii = require('../BaseJii');
const InvalidConfigException = require('../exceptions/InvalidConfigException');
const InvalidParamException = require('../exceptions/InvalidParamException');
const Collection = require('../base/Collection');
const Pagination = require('../data/Pagination');
const FetchEvent = require('../data/FetchEvent');
const DataProviderEvent = require('../data/DataProviderEvent');
const _isNumber = require('lodash/isNumber');
const _isArray = require('lodash/isArray');
const _isObject = require('lodash/isObject');
const _isFunction = require('lodash/isFunction');
const _findKey = require('lodash/findKey');
const _has = require('lodash/has');

class DataProvider extends Collection {

    preInit() {
        /**
         * @type {object}
         */
        this._fetchedKeys = {};

        /**
         * @type {function[]|null}
         */
        this._fetchCallbacks = null;

        /**
         * @type {number|null}
         */
        this._totalCount = 0;

        /**
         * @type {Jii.data.Pagination|boolean}
         */
        this._pagination = null;

        /**
         * @type {Jii.data.Sort}
         */
        this._sort = null;

        /**
         * @type {boolean}
         */
        this.autoFetch = true;

        /**
         * @type {function|Jii.data.Query}
         */
        this.query = null;

        /**
         * @type {string|null} an ID that uniquely identifies the data provider among all data providers.
         * You should set this property if the same page contains two or more different data providers.
         * Otherwise, the [[pagination]] and [[sort]] may not work properly.
         */
        this.id = null;

        super.preInit(...arguments);
    }

    init() {
        if (this.autoFetch) {
            this.fetch();
        }
    }

    /**
     *
     * @param {boolean} [force]
     * @return {*}
     */
    fetch(force) {
        // Queue promises when fetch in process
        if (this._fetchCallbacks !== null) {
            return new Promise(resolve => {
                this._fetchCallbacks.push(resolve);
            });
        }

        if (this.isFetched() && !force) {
            this.refreshFilter();
            return Promise.resolve(false);
        }

        this.trigger(DataProvider.EVENT_BEFORE_FETCH, new FetchEvent({
            isLoading: true
        }));
        this.trigger(DataProvider.EVENT_LOADING, new FetchEvent({
            isLoading: true
        }));

        this._fetchCallbacks = [];
        return Promise.resolve().then(() => {

            // Query as function
            if (_isFunction(this.query)) {
                return this.query(this.getPagination());
            }

            // TODO Query, REST, ...
            throw new InvalidConfigException('Wrong query format in DataProvider.');
        }).then(data => {
            // Validate response
            if (!data) {
                throw new InvalidParamException('Result data is not object in DataProvider.fetch().');
            }
            if (data.totalCount && !_isNumber(data.totalCount)) {
                throw new InvalidParamException('Result param "totalCount" must be number in DataProvider.fetch().');
            }
            if (!_isArray(data.models)) {
                throw new InvalidParamException('Result param "models" must be array in DataProvider.fetch().');
            }

            if (_isNumber(data.totalCount)) {
                this.setTotalCount(data.totalCount);
            }

            // No changes, but fetch
            if (!this.isFetched() && data.models.length === 0 && this.length === 0) {
                this.trigger(DataProvider.EVENT_FETCHED, this._createEvent({
                    isFetch: true
                }));
            } else {
                this.setModels(data.models);
                this.refreshFilter();
            }

            // Resolve queue promises after current
            var callbacks = this._fetchCallbacks;
            this._fetchCallbacks = null;
            setTimeout(() => {
                callbacks.forEach(callback => {
                    callback(data.models);
                });
            });

            this.trigger(DataProvider.EVENT_AFTER_FETCH, new FetchEvent({
                isLoading: false
            }));
            this.trigger(DataProvider.EVENT_LOADING, new FetchEvent({
                isLoading: false
            }));

            return data;
        });
    }

    isFetched() {
        if (!super.isFetched()) {
            return false;
        }

        var pagination = this.getPagination();
        if (pagination) {
            let needFetch = false;
            pagination.getIndexes().forEach(index => {
                if (!_has(this._fetchedKeys, index.toString()) && index < this.getTotalCount() - 1) {
                    needFetch = true;
                }
            });
            return !needFetch;
        }

        return true;
    }

    /**
     * Returns the total number of data models.
     * When [[pagination]] is false, this returns the same value as [[count]].
     * Otherwise, it will call [[prepareTotalCount()]] to get the count.
     * @returns {number} total number of possible data models.
     */
    getTotalCount() {
        if (this._pagination === false) {
            return this.parent ? this.parent.getCount() : this.getCount();
        }
        return this._totalCount;
    }

    /**
     * Sets the total number of data models.
     * @param {number} value the total number of data models.
     */
    setTotalCount(value) {
        this._totalCount = value;

        if (this._pagination) {
            this._pagination.totalCount = value;
        }
    }

    /**
     * Returns the pagination object used by this data provider.
     * Note that you should call [[prepare()]] or [[getModels()]] first to get correct values
     * of [[Pagination.totalCount]] and [[Pagination.pageCount]].
     * @returns {Jii.data.Pagination|boolean} the pagination object. If this is false, it means the pagination is disabled.
     */
    getPagination() {
        if (this._pagination === null) {
            this.setPagination({});
        }

        return this._pagination;
    }

    /**
     * Sets the pagination for this data provider.
     * @param {object|Jii.data.Pagination|boolean} value the pagination to be used by this data provider.
     * @throws InvalidParamException
     */
    setPagination(value) {
        if (_isObject(value)) {
            let config = {
                className: Pagination,
                totalCount: this.getTotalCount()
            };
            if (this.id !== null) {
                config.pageParam = `${ this.id }-page`;
                config.pageSizeParam = `${ this.id }-per-page`;
            }
            this._pagination = Jii.createObject(Jii.mergeConfigs(config, value));
        } else if (value instanceof Pagination || value === false) {
            this._pagination = value;
        } else {
            throw new InvalidParamException('Only Pagination instance, configuration object or false is allowed.');
        }

        this._pagination.on(Pagination.EVENT_CHANGE, this._onPaginationChange.bind(this));
    }

    /**
     * @returns {Jii.data.Sort|boolean} the sorting object. If this is false, it means the sorting is disabled.
     */
    getSort() {
        if (this._sort === null) {
            this.setSort({});
        }

        return this._sort;
    }

    /**
     * Sets the sort definition for this data provider.
     * @param {object|Jii.data.Sort|boolean} value the sort definition to be used by this data provider.
     * This can be one of the following:
     *
     * - a configuration array for creating the sort definition object. The "class" element defaults
     *   to 'jii\data\Sort'
     * - an instance of [[Sort]] or its subclass
     * - false, if sorting needs to be disabled.
     *
     * @throws InvalidParamException
     */
    setSort(value) {
        if (_isObject(value)) {
            let config = {};
            // @todo Sort implementation
            if (this.id !== null) {
                config.sortParam = `${ this.id }-sort`;
            }
            this._sort = Jii.createObject(Jii.mergeConfigs(config, value));
        } else if (/*value instanceof Sort ||*/
        value === false) {
            // @todo Sort implementation
            this._sort = value;
        } else {
            throw new InvalidParamException('Only Sort instance, configuration object or false is allowed.');
        }
    }

    _change(startIndex, toAdd, toRemove, unique, parentCallParams) {
        if (parentCallParams) {
            var pagination = this.getPagination();
            if (pagination) {
                toRemove.forEach(model => {
                    let primaryKey = this._getPrimaryKey(model);
                    var finedKey = _findKey(this._fetchedKeys, key => {
                        return key === primaryKey;
                    });
                    if (finedKey) {
                        delete this._fetchedKeys[finedKey];
                    }
                });

                parentCallParams.toAdd.forEach((model, index) => {
                    this._fetchedKeys[index + pagination.getOffset()] = this._getPrimaryKey(model);
                });

                this.refreshFilter();
                return;
            }
        }

        super._change(startIndex, toAdd, toRemove, unique);
    }

    _onPaginationChange() {
        if (this.autoFetch) {
            this.fetch();
        }
    }

    _filterModels() {
        var pagination = this.getPagination();
        if (pagination) {
            if (!this.parent) {
                throw new InvalidConfigException('DataProvider with pagination need parent collection.');
            }

            // There were no filtering with super._filterModels when pagination is not null
            // Now we're passing filtered models of the current page to the super._filterModels()
            // @todo probably all the collection indices should be used, not the indices on the current page
            const currentPageModels = pagination.getIndexes().map(i => {
                return this.parent._byId[this._fetchedKeys[i]] || null;
            }).filter(model => model !== null);

            return super._filterModels(currentPageModels);
        }

        return super._filterModels();
    }

    /**
     *
     * @param {object} params
     * @returns {Jii.data.CollectionEvent}
     */
    _createEvent(params) {
        params.totalCount = this.getTotalCount();
        return new DataProviderEvent(params);
    }

}

/**
 * @event Jii.data.DataProvider#loading
 * @property {Jii.data.FetchEvent} event
 */
DataProvider.EVENT_LOADING = 'loading';

/**
 * @event Jii.data.DataProvider#after_fetch
 * @property {Jii.data.FetchEvent} event
 */
DataProvider.EVENT_AFTER_FETCH = 'after_fetch';

/**
 * @event Jii.data.DataProvider#before_fetch
 * @property {Jii.data.FetchEvent} event
 */
DataProvider.EVENT_BEFORE_FETCH = 'before_fetch';
module.exports = DataProvider;<|endoftext|>


<commit_before>// !preview r2d3 data=list(sources = list("FileStreamSource[file]"), sinks = list("FileSink[file]"), stats = list(list(rps = list("in" = 1, out = 2)), list(rps = list("in" = 5, out = 10)), list(rps = list("in" = 1, out = 10)))), options = list(demo = FALSE), container = "div"

function StreamStats() {
  var rpmIn = [0];
  var rpmOut = [0];

  var maxIn = 10;
  var maxOut = 10;

  var maxStats = 100;

  this.add = function(rps) {
    if (maxIn < rps.in) maxIn = rps.in;
    if (maxOut < rps.out) maxOut = rps.out;

    rpmIn.unshift(rps.in);
    rpmOut.unshift(rps.out);

    rpmIn = rpmIn.slice(0, Math.min(rpmIn.length, maxStats - 1));
    rpmOut = rpmOut.slice(0, Math.min(rpmOut.length, maxStats - 1));
  };

  this.rpmIn = function() {
    return rpmIn;
  };

  this.rpmOut = function() {
    return rpmOut;
  };

  this.maxIn = function() {
    return maxIn;
  };

  this.maxOut = function() {
    return maxOut;
  };

  this.latestRpsIn = function() {
    return rpmIn[0];
  };

  this.latestRpsOut = function() {
    return rpmOut[0];
  };
}

var stats = new StreamStats();

function StreamRenderer(stats) {
  var self = this;
  var width = 0;
  var height = 0;

  var svg = div.append("svg");
  var chart = div.append("svg")
    .attr("class", "chart")
    .style("top", 0)
    .style("left", 0);

  var chartIn;
  var chartOut;
  var rowsPerSecondIn;
  var rowsPerSecondOut;

  var barWidth = 10;
  var margin = 15;
  var remotesCircle = 10;
  var remotesMargin = margin;
  var remotesHeight = 0;
  var chartHeight = 0;

  this.formatRps = function(num) {
    return d3.format(",.2r")(num);
  };

  this.setSize = function(newWidth, newHeight) {
    remotesHeight = Math.min(50, newHeight / 3);
    chartHeight = newHeight - 2 * remotesHeight;

    svg.attr("width", newWidth).attr("height", newHeight);

    chart.attr("width", newWidth).attr("height", chartHeight);
    chart.style("top", remotesHeight + "px");

    chart.style("display", newHeight < 120 ? "none" : "block");

    width = newWidth;
    height =  newHeight;
  };

  this.update = function() {
    chart.attr("class", "chart");
    setTimeout(function() {
      chart.attr("class", "chart animate");
    }, 50);

    rowsPerSecondIn.text(self.formatRps(stats.latestRpsIn()) + " rps");
    rowsPerSecondOut.text(self.formatRps(stats.latestRpsOut()) + " rps");

    rowsPerSecondIn.style("display", height < 60 ? "none" : "block");
    rowsPerSecondOut.style("display", height < 60 ? "none" : "block");

    var chartHeight = height - 2 * remotesHeight;

    var dataIn = chartIn.selectAll("rect")
      .data(stats.rpmIn());

    var barsIn = dataIn
      .enter()
        .append("rect")
      .merge(dataIn)
        .attr("width", barWidth - 2)
        .attr("height", function(d, i) { return chartHeight / 2 * d / stats.maxIn();})
        .attr("x", function(d, i) { return margin + i * barWidth - barWidth; })
        .attr("y", function(d, i) { return chartHeight / 2 - chartHeight / 2 * d / stats.maxIn(); })
        .attr("class", "barIn");

    var dataOut = chartOut.selectAll("rect")
      .data(stats.rpmOut());

    var barsOut = dataOut.
      enter()
        .append("rect")
      .merge(dataOut)
        .attr("width", barWidth - 2)
        .attr("height", function(d, i) { return chartHeight / 2 * d / stats.maxOut();})
        .attr("x", function(d, i) { return margin + i * barWidth - barWidth; })
        .attr("y", function(d, i) { return chartHeight / 2; })
        .attr("class", "barOut");
  };

  this.render = function() {
    svg.selectAll("g").remove();
    svg.selectAll("text").remove();
    chart.selectAll("rect").remove();

    chartIn = chart.append("g");
    chartOut = chart.append("g");

    rpsTextSize = Math.min(10 + height / 40, 20);

    rowsPerSecondIn = svg.append("text");
    rowsPerSecondIn.style("font-size", rpsTextSize + "px");

    rowsPerSecondOut = svg.append("text");
    rowsPerSecondOut.style("font-size", rpsTextSize + "px");

    var sourceCircles = svg.selectAll(".source")
      .data(data.sources)
      .enter()
      .append("g")
        .attr("class", "source")
        .attr("transform", function(d, i) {
          return "translate(" + (margin + i * 24) + ", " + remotesMargin + ")";
        })
        .append("circle");

    sourceCircles.attr("cx", "0").attr("cy", "0").attr("r", "10")
      .attr("class", function (d, i) {
        return (i == data.sources.length - 1) ? "source selected" : "source";
      })
      .attr("stroke-width", "2")
      .on("mouseover", function (d, i) {
        sourceCircles.attr("class", "source");
        d3.select(this).attr("class", "source selected");
        sourceText.text(d);
      });

    var sourceText = svg
      .append("text")
        .attr("y", margin + 3).attr("x", margin + data.sources.length * 24)
        .attr("class", "label")
        .text(function(d) { return data.sources[data.sources.length - 1]; });

    rowsPerSecondIn
      .attr("y", margin + rpsTextSize + 5).attr("x", margin + data.sources.length * 24)
      .attr("class", "rps")
      .text(self.formatRps(stats.latestRpsIn()) + " rps");

    rowsPerSecondIn.append("title").text("rows per second");

    var sinkCircles = svg.selectAll(".sink")
      .data(data.sinks)
      .enter()
      .append("g")
        .attr("class", "sink")
        .attr("transform", function(d, i) {
          return "translate(" + (margin + i * 24) + ","  + (height - remotesMargin) + ")";
        })
        .append("circle");

    sinkCircles.attr("cx", "0").attr("cy", "0").attr("r", "10")
      .attr("class", function (d, i) {
        return (i == data.sinks.length - 1) ? "sink selected" : "sink";
      })
      .attr("stroke-width", "2")
      .on("mouseover", function (d, i) {
        sinkCircles.attr("class", "sink");
        d3.select(this).attr("class", "sink selected");
        sinkText.text(d);
      });

    var sinkText = svg
      .append("text")
        .attr("y", height - margin + 3).attr("x", margin + data.sinks.length * 24)
        .attr("class", "label sinkText")
        .text(function(d) { return data.sinks[data.sinks.length - 1]; });

    rowsPerSecondOut
      .attr("y", height - margin - 15).attr("x", margin + data.sinks.length * 24)
      .attr("class", "rps")
      .text(self.formatRps(stats.latestRpsOut()) + " rps");

    rowsPerSecondOut.append("title").text("rows per second");

    this.update();
  };
}

var renderer = new StreamRenderer(stats);

r2d3.onRender(function(data, svg, width, height, options) {
  renderer.setSize(width, height);
  renderer.render();
});

r2d3.onResize(function(width, height) {
  renderer.setSize(width, height);
  renderer.render();
});

function debug(msg) {
  if (options !== null && "debug" in options) {
    console.log(msg);
  }
}

if (options !== null && "demo" in options) {
  setInterval(function() {
    var data = {
      rps: {
        in: Math.floor(Math.random() * 100),
        out: Math.floor(Math.random() * 100)
      }
    };

    debug(JSON.stringify(data));
    stats.add(data.rps);
    renderer.update();
  }, 1000);
}

if (typeof(Shiny) !== "undefined") {
  Shiny.addCustomMessageHandler("sparklyr_stream_view", function(data) {
      debug(JSON.stringify(data));
      stats.add(data.rps);
      renderer.update();
    }
  );
}

if (data !== null && "stats" in data) {
  var statsIdx = 0;
  setInterval(function(data) {
    return function() {
      if (statsIdx >= data.stats.length) {
        renderer.stop();
        return;
      }

      var d = data.stats[statsIdx];

      debug(JSON.stringify(d));
      stats.add(d.rps);
      renderer.update();

      statsIdx += 1;
    };
  }(data), 1000);
}
<commit_msg>minor formatting fix to stream chart<commit_after>// !preview r2d3 data=list(sources = list("FileStreamSource[file]"), sinks = list("FileSink[file]"), stats = list(list(rps = list("in" = 1, out = 2)), list(rps = list("in" = 5, out = 10)), list(rps = list("in" = 1, out = 10)))), options = list(demo = FALSE), container = "div"

function StreamStats() {
  var rpmIn = [0];
  var rpmOut = [0];

  var maxIn = 10;
  var maxOut = 10;

  var maxStats = 100;

  this.add = function(rps) {
    if (maxIn < rps.in) maxIn = rps.in;
    if (maxOut < rps.out) maxOut = rps.out;

    rpmIn.unshift(rps.in);
    rpmOut.unshift(rps.out);

    rpmIn = rpmIn.slice(0, Math.min(rpmIn.length, maxStats - 1));
    rpmOut = rpmOut.slice(0, Math.min(rpmOut.length, maxStats - 1));
  };

  this.rpmIn = function() {
    return rpmIn;
  };

  this.rpmOut = function() {
    return rpmOut;
  };

  this.maxIn = function() {
    return maxIn;
  };

  this.maxOut = function() {
    return maxOut;
  };

  this.latestRpsIn = function() {
    return rpmIn[0];
  };

  this.latestRpsOut = function() {
    return rpmOut[0];
  };
}

var stats = new StreamStats();

function StreamRenderer(stats) {
  var self = this;
  var width = 0;
  var height = 0;

  var svg = div.append("svg");
  var chart = div.append("svg")
    .attr("class", "chart")
    .style("top", 0)
    .style("left", 0);

  var chartIn;
  var chartOut;
  var rowsPerSecondIn;
  var rowsPerSecondOut;

  var barWidth = 10;
  var margin = 15;
  var remotesCircle = 10;
  var remotesMargin = margin;
  var remotesHeight = 0;
  var chartHeight = 0;

  this.formatRps = function(num) {
    return d3.format(",.0f")(num);
  };

  this.setSize = function(newWidth, newHeight) {
    remotesHeight = Math.min(50, newHeight / 3);
    chartHeight = newHeight - 2 * remotesHeight;

    svg.attr("width", newWidth).attr("height", newHeight);

    chart.attr("width", newWidth).attr("height", chartHeight);
    chart.style("top", remotesHeight + "px");

    chart.style("display", newHeight < 120 ? "none" : "block");

    width = newWidth;
    height =  newHeight;
  };

  this.update = function() {
    chart.attr("class", "chart");
    setTimeout(function() {
      chart.attr("class", "chart animate");
    }, 50);

    rowsPerSecondIn.text(self.formatRps(stats.latestRpsIn()) + " rps");
    rowsPerSecondOut.text(self.formatRps(stats.latestRpsOut()) + " rps");

    rowsPerSecondIn.style("display", height < 60 ? "none" : "block");
    rowsPerSecondOut.style("display", height < 60 ? "none" : "block");

    var chartHeight = height - 2 * remotesHeight;

    var dataIn = chartIn.selectAll("rect")
      .data(stats.rpmIn());

    var barsIn = dataIn
      .enter()
        .append("rect")
      .merge(dataIn)
        .attr("width", barWidth - 2)
        .attr("height", function(d, i) { return chartHeight / 2 * d / stats.maxIn();})
        .attr("x", function(d, i) { return margin + i * barWidth - barWidth; })
        .attr("y", function(d, i) { return chartHeight / 2 - chartHeight / 2 * d / stats.maxIn(); })
        .attr("class", "barIn");

    var dataOut = chartOut.selectAll("rect")
      .data(stats.rpmOut());

    var barsOut = dataOut.
      enter()
        .append("rect")
      .merge(dataOut)
        .attr("width", barWidth - 2)
        .attr("height", function(d, i) { return chartHeight / 2 * d / stats.maxOut();})
        .attr("x", function(d, i) { return margin + i * barWidth - barWidth; })
        .attr("y", function(d, i) { return chartHeight / 2; })
        .attr("class", "barOut");
  };

  this.render = function() {
    svg.selectAll("g").remove();
    svg.selectAll("text").remove();
    chart.selectAll("rect").remove();

    chartIn = chart.append("g");
    chartOut = chart.append("g");

    rpsTextSize = Math.min(10 + height / 40, 20);

    rowsPerSecondIn = svg.append("text");
    rowsPerSecondIn.style("font-size", rpsTextSize + "px");

    rowsPerSecondOut = svg.append("text");
    rowsPerSecondOut.style("font-size", rpsTextSize + "px");

    var sourceCircles = svg.selectAll(".source")
      .data(data.sources)
      .enter()
      .append("g")
        .attr("class", "source")
        .attr("transform", function(d, i) {
          return "translate(" + (margin + i * 24) + ", " + remotesMargin + ")";
        })
        .append("circle");

    sourceCircles.attr("cx", "0").attr("cy", "0").attr("r", "10")
      .attr("class", function (d, i) {
        return (i == data.sources.length - 1) ? "source selected" : "source";
      })
      .attr("stroke-width", "2")
      .on("mouseover", function (d, i) {
        sourceCircles.attr("class", "source");
        d3.select(this).attr("class", "source selected");
        sourceText.text(d);
      });

    var sourceText = svg
      .append("text")
        .attr("y", margin + 3).attr("x", margin + data.sources.length * 24)
        .attr("class", "label")
        .text(function(d) { return data.sources[data.sources.length - 1]; });

    rowsPerSecondIn
      .attr("y", margin + rpsTextSize + 5).attr("x", margin + data.sources.length * 24)
      .attr("class", "rps")
      .text(self.formatRps(stats.latestRpsIn()) + " rps");

    rowsPerSecondIn.append("title").text("rows per second");

    var sinkCircles = svg.selectAll(".sink")
      .data(data.sinks)
      .enter()
      .append("g")
        .attr("class", "sink")
        .attr("transform", function(d, i) {
          return "translate(" + (margin + i * 24) + ","  + (height - remotesMargin) + ")";
        })
        .append("circle");

    sinkCircles.attr("cx", "0").attr("cy", "0").attr("r", "10")
      .attr("class", function (d, i) {
        return (i == data.sinks.length - 1) ? "sink selected" : "sink";
      })
      .attr("stroke-width", "2")
      .on("mouseover", function (d, i) {
        sinkCircles.attr("class", "sink");
        d3.select(this).attr("class", "sink selected");
        sinkText.text(d);
      });

    var sinkText = svg
      .append("text")
        .attr("y", height - margin + 3).attr("x", margin + data.sinks.length * 24)
        .attr("class", "label sinkText")
        .text(function(d) { return data.sinks[data.sinks.length - 1]; });

    rowsPerSecondOut
      .attr("y", height - margin - 15).attr("x", margin + data.sinks.length * 24)
      .attr("class", "rps")
      .text(self.formatRps(stats.latestRpsOut()) + " rps");

    rowsPerSecondOut.append("title").text("rows per second");

    this.update();
  };
}

var renderer = new StreamRenderer(stats);

r2d3.onRender(function(data, svg, width, height, options) {
  renderer.setSize(width, height);
  renderer.render();
});

r2d3.onResize(function(width, height) {
  renderer.setSize(width, height);
  renderer.render();
});

function debug(msg) {
  if (options !== null && "debug" in options) {
    console.log(msg);
  }
}

if (options !== null && "demo" in options) {
  setInterval(function() {
    var data = {
      rps: {
        in: Math.floor(Math.random() * 100),
        out: Math.floor(Math.random() * 100)
      }
    };

    debug(JSON.stringify(data));
    stats.add(data.rps);
    renderer.update();
  }, 1000);
}

if (typeof(Shiny) !== "undefined") {
  Shiny.addCustomMessageHandler("sparklyr_stream_view", function(data) {
      debug(JSON.stringify(data));
      stats.add(data.rps);
      renderer.update();
    }
  );
}

if (data !== null && "stats" in data) {
  var statsIdx = 0;
  setInterval(function(data) {
    return function() {
      if (statsIdx >= data.stats.length) {
        renderer.stop();
        return;
      }

      var d = data.stats[statsIdx];

      debug(JSON.stringify(d));
      stats.add(d.rps);
      renderer.update();

      statsIdx += 1;
    };
  }(data), 1000);
}
<|endoftext|>


<commit_before>/*jslint browser:true*/

var vlille = (function () {
    'use strict';

    var API_PROXY_BASE = 'http://localhost:8001/';

    /**
     *
     * @param  {Document} xml [description]
     * @return {Object}       [description]
     */
    function xmlStationsToJson(xml) {
        var i,
            j,
            len,
            len2,
            markers = xml.childNodes[0].children || [],
            attributes,
            jsonMarker,
            jsonArray = [];

        // imperative way
        for (i = 0, len = markers.length; i < len; i += 1) {
            jsonMarker = {};
            attributes = markers[i].attributes;

            for (j = 0, len2 = attributes.length; j < len2; j += 1) {
                jsonMarker[attributes[j].name] = attributes[j].value;
            }

            jsonArray.push(jsonMarker);
        }

        return jsonArray;
    }

    /**
     *
     * @param  {Document} xmlNode [description]
     * @return {Object}           [description]
     */
    function xmlStationToJson(xml) {
        var i,
            len,
            stationData = xml.childNodes[0].children || [],
            jsonStation = {};

        for (i = 0, len = stationData.length; i < len; i += 1) {
            jsonStation[stationData[i].nodeName] = stationData[i].innerHTML;
        }

        return jsonStation;
    }

    /**
     * Format params object to url query args string.
     * @param  {Object} params [description]
     * @return {String}        [description]
     */
    function formatParams(params) {
        var key,
            query = [];

        for (key in params) {
            if (params.hasOwnProperty(key)) {
                query.push(key + '=' + params[key]);
            }
        }

        return query.join('&');
    }

    /**
     * Basic XHR request implementation.
     * @param  {String}   url     [description]
     * @param  {Object}   params  [description]
     * @param  {Function} resolve [description]
     * @param  {Function} reject  [description]
     */
    function requestXML(url, params, resolve, reject) {
        var requestObj = new XMLHttpRequest(),
            urlWithParams = url;

        if (params) {
            urlWithParams += '?' + formatParams(params);
        }

        requestObj.open('GET', urlWithParams);

        requestObj.addEventListener('load', function (event) {
            var target = event.target;

            if (target.status === 200) {
                resolve(target.responseXML);
            } else {
                reject(target);
            }
        });

        requestObj.addEventListener('error', function (event) {
            reject(new Error(event));
        });

        requestObj.send();
    }

    /**
     * @return {Number} Radians value of the number.
     */
    Number.prototype.toRadians = function () {
        return this * Math.PI / 180;
    };

    /**
     * @see http://www.movable-type.co.uk/scripts/latlong.html
     * @param  {Object} coord1 [description]
     * @param  {Object} coord2 [description]
     * @return {Number}        [description]
     */
    function haversineDistance(coord1, coord2) {
        var R = 6371000, // metres
            phi1,
            phi2,
            deltaPhi,
            deltaLambda,
            a,
            c;

        phi1 = coord1.lat.toRadians();
        phi2 = coord2.lat.toRadians();
        deltaPhi = (coord2.lat - coord1.lat).toRadians();
        deltaLambda = (coord2.lon - coord1.lon).toRadians();

        a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2);
        c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        return R * c;
    }

    /**
     * Publics
     */

    /**
     * Gets full stations list.
     * @return {Function} [description]
     */
    function getAllStations() {
        return {
            then: function (resolve, reject) {
                var xmlResolve = function (xml) {
                    resolve(xmlStationsToJson(xml));
                };

                requestXML(API_PROXY_BASE + 'xml-stations.aspx', null, xmlResolve, reject);
            }
        };
    }

    /**
     * Gets informations about the station whit the given `id`.
     * @param  {String} id [description]
     * @return {Function}  [description]
     */
    function getStation(id) {
        var params = {
            borne: id
        };

        return {
            then: function (resolve, reject) {
                var xmlResolve = function (xml) {
                    resolve(xmlStationToJson(xml));
                };

                requestXML(API_PROXY_BASE + 'xml-station.aspx', params, xmlResolve, reject);
            }
        };
    }

    /**
     * Gets closest station using Haversine formula.
     * @param  {[type]} coord [description]
     * @return {[type]}       [description]
     */
    function getClosestStation(coords) {

        function then(resolve, reject) {
            getAllStations().then(function (stations) {
                var i,
                    len,
                    station,
                    stationCoords,
                    stationDistance,
                    distance = Infinity,
                    closetStation;

                for (i = 0, len = stations.length; i < len; i += 1) {
                    station = stations[i];
                    stationCoords = {
                        lat: parseFloat(station.lat),
                        lon: parseFloat(station.lng)
                    };

                    stationDistance = haversineDistance(coords, stationCoords);
                    if (stationDistance < distance) {
                        closetStation = station;
                        distance = stationDistance;
                    }
                }

                resolve(closetStation);
            }, reject);
        }

        return {
            then: then
        };
    }

    return {
        stations: getAllStations,
        station: getStation,
        closestStation: getClosestStation
    };
}());<commit_msg>Refactor closestStation() onto closestStations() with a new  parameter to allow number of results configuration<commit_after>/*jslint browser:true*/

var vlille = (function () {
    'use strict';

    var API_PROXY_BASE = 'http://localhost:8001/';

    /**
     *
     * @param  {Document} xml [description]
     * @return {Object}       [description]
     */
    function xmlStationsToJson(xml) {
        var i,
            j,
            len,
            len2,
            markers = xml.childNodes[0].children || [],
            attributes,
            jsonMarker,
            jsonArray = [];

        // imperative way
        for (i = 0, len = markers.length; i < len; i += 1) {
            jsonMarker = {};
            attributes = markers[i].attributes;

            for (j = 0, len2 = attributes.length; j < len2; j += 1) {
                jsonMarker[attributes[j].name] = attributes[j].value;
            }

            jsonArray.push(jsonMarker);
        }

        return jsonArray;
    }

    /**
     *
     * @param  {Document} xmlNode [description]
     * @return {Object}           [description]
     */
    function xmlStationToJson(xml) {
        var i,
            len,
            stationData = xml.childNodes[0].children || [],
            jsonStation = {};

        for (i = 0, len = stationData.length; i < len; i += 1) {
            jsonStation[stationData[i].nodeName] = stationData[i].innerHTML;
        }

        return jsonStation;
    }

    /**
     * Format params object to url query args string.
     * @param  {Object} params [description]
     * @return {String}        [description]
     */
    function formatParams(params) {
        var key,
            query = [];

        for (key in params) {
            if (params.hasOwnProperty(key)) {
                query.push(key + '=' + params[key]);
            }
        }

        return query.join('&');
    }

    /**
     * Basic XHR request implementation.
     * @param  {String}   url     [description]
     * @param  {Object}   params  [description]
     * @param  {Function} resolve [description]
     * @param  {Function} reject  [description]
     */
    function requestXML(url, params, resolve, reject) {
        var requestObj = new XMLHttpRequest(),
            urlWithParams = url;

        if (params) {
            urlWithParams += '?' + formatParams(params);
        }

        requestObj.open('GET', urlWithParams);

        requestObj.addEventListener('load', function (event) {
            var target = event.target;

            if (target.status === 200) {
                resolve(target.responseXML);
            } else {
                reject(target);
            }
        });

        requestObj.addEventListener('error', function (event) {
            reject(new Error(event));
        });

        requestObj.send();
    }

    /**
     * @return {Number} Radians value of the number.
     */
    Number.prototype.toRadians = function () {
        return this * Math.PI / 180;
    };

    /**
     * @see http://www.movable-type.co.uk/scripts/latlong.html
     * @param  {Object} coord1 [description]
     * @param  {Object} coord2 [description]
     * @return {Number}        [description]
     */
    function haversineDistance(coord1, coord2) {
        var R = 6371000, // metres
            phi1,
            phi2,
            deltaPhi,
            deltaLambda,
            a,
            c;

        phi1 = coord1.lat.toRadians();
        phi2 = coord2.lat.toRadians();
        deltaPhi = (coord2.lat - coord1.lat).toRadians();
        deltaLambda = (coord2.lon - coord1.lon).toRadians();

        a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) + Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2);
        c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

        return R * c;
    }

    /**
     * Publics
     */

    /**
     * Gets full stations list.
     * @return {Function} [description]
     */
    function getAllStations() {
        return {
            then: function (resolve, reject) {
                var xmlResolve = function (xml) {
                    resolve(xmlStationsToJson(xml));
                };

                requestXML(API_PROXY_BASE + 'xml-stations.aspx', null, xmlResolve, reject);
            }
        };
    }

    /**
     * Gets informations about the station whit the given `id`.
     * @param  {String} id [description]
     * @return {Function}  [description]
     */
    function getStation(id) {
        var params = {
            borne: id
        };

        return {
            then: function (resolve, reject) {
                var xmlResolve = function (xml) {
                    resolve(xmlStationToJson(xml));
                };

                requestXML(API_PROXY_BASE + 'xml-station.aspx', params, xmlResolve, reject);
            }
        };
    }

    /**
     * Gets closest stations using Haversine formula.
     * The second parameter `max` (default value = 3) allow one to configure the maximum number of results.
     * @param  {Object} coord [description]
     * @param  {Int} max      [description]
     * @return {Function}     [description]
     */
    function getClosestStations(coords, max) {
        if (max === undefined) {
            max = 3;
        }

        function then(resolve, reject) {
            getAllStations().then(function (stations) {
                var closetStations = stations
                    .map(function (station) {
                        var stationCoords = {
                            lat: parseFloat(station.lat),
                            lon: parseFloat(station.lng)
                        };

                        station.distance = haversineDistance(coords, stationCoords);

                        return station;
                    })
                    .sort(function (a, b) {
                        return a.distance - b.distance;
                    });

                if (closetStations.length > max) {
                    closetStations.length = max;
                }

                resolve(closetStations);
            }, reject);
        }

        return {
            then: then
        };
    }

    return {
        stations: getAllStations,
        station: getStation,
        closestStations: getClosestStations
    };
}());<|endoftext|>


<commit_before>/*
 Copyright (c) 2015, GEM Foundation.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses/agpl.html>.
 */

(function($, Backbone, _) {
    var progressHandlingFunction = function(progress) {
        var percent = progress.loaded / progress.total * 100;
        $('.bar').css('width', percent + '%');
        if (percent == 100) {
            dialog.hidePleaseWait();
        }
    };

    var dialog = (function ()
                  {
                      var pleaseWaitDiv = $('

Processing...

');
                      return {
                          showPleaseWait: function(msg, progress) {
                          $('h1', pleaseWaitDiv).text(msg);
                          if (progress) {
                          progressHandlingFunction({loaded: 0, total: 1});
                          } else {
                          progressHandlingFunction({loaded: 1, total: 1});
                      }
                              pleaseWaitDiv.modal();
                          },
                          hidePleaseWait: function () {
                              pleaseWaitDiv.modal('hide');
                          }
                      };
                  })();

    var diaerror = (function ()
                  {
                      var errorDiv = $('
\

\

\

\

Calculation not accepted: traceback
\

\

\

\

\
                      <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>\

\

\

\

');

//

Error

Close
');
//                      errorDiv.on("click", function(e) {
//                          this.hideDiaError();
//                      })
                      return {
                          showDiaError: function(title, msg) {
                              if (title != null) {
                                  $('.modal-title', errorDiv).html(title);
                              }
                              if (msg != null) {
                                  $('.modal-body-pre', errorDiv).html(msg);
                              }
                              errorDiv.modal();
                          },
                          hideDiaError: function () {
                              errorDiv.modal('hide');
                          }
                      };
                  })();

    var CalculationTable = Backbone.View.extend(
        {
            /* the html element where the table is rendered */
            el: $('#my-calculations'),

            initialize: function(options) {

                /* whatever happens to any calculation, re-render the table */
                _.bindAll(this, 'render');
                this.calculations = options.calculations;
                this.calculations.bind('reset', this.render);
                this.calculations.bind('add', this.render);
                this.calculations.bind('remove', this.render);

                /* if false, it prevents the table to be refreshed */
                this.can_be_rendered = true;

                this.render();
            },

            events: {
                "click .btn-danger": "remove_calculation",
                "click .btn-traceback": "show_traceback",
                "click .btn-file": "on_run_risk_clicked",
                "change .btn-file input": "on_run_risk_queued"
            },

            /* When an input dialog is opened, it is very important to not re-render the table */
            on_run_risk_clicked: function(e) {
                /* if a file input dialog has been opened do not refresh the calc table */
                this.can_be_rendered = false;
            },

            on_run_risk_queued: function(e) {
                this.can_be_rendered = true;
            },

            remove_calculation: function(e) {
                e.preventDefault();
                var calc_id = $(e.target).attr('data-calc-id');
                var view = this;
                diaerror.showDiaError("Removing calculation " + calc_id, "...");
                $.post(gem_oq_server_url + "/v1/calc/" + calc_id + "/remove"
                     ).success(
                         function(data, textStatus, jqXHR)
                         {
                             diaerror.showDiaError("Removing calculation " + calc_id, "Calculation " + calc_id + " removed.");
                             view.calculations.remove([view.calculations.get(calc_id)]);
                         }
                     ).error(
                         function(jqXHR, textStatus, errorThrown)
                         {
                             if (jqXHR.status == 404) {
                                 diaerror.showDiaError("Removing calculation " + calc_id + ".", "Failed: calculation " + calc_id + " not found.");
                             }
                             else {
                                 diaerror.showDiaError("Removing calculation " + calc_id + ".", "Failed: " + textStatus);
                             }
                         }
                     );
            },

            show_traceback: function(e) {
                var calc_id = $(e.target).attr('data-calc-id');

                var myXhr = $.ajax({url: gem_oq_server_url + "/v1/calc/" + calc_id + "/traceback",
                                    error: function (jqXHR, textStatus, errorThrown) {
                                        if (jqXHR.status == 404) {
                                            diaerror.showDiaError("Calculation " + calc_id + " not found.");
                                        }
                                        else {
                                            diaerror.showDiaError("Error retrieving traceback for calculation " + calc_id + ".", textStatus);
                                        }
                                        // alert("Error: " + textStatus);
                                    },
                                    success: function (data, textStatus, jqXHR) {
                                        if (data.length == 0) {
                                            diaerror.showDiaError("Traceback not found for calculation " + calc_id + ".", []);
                                        }
                                        else {
                                            out = ""
                                            for (s in data) {
                                                if (data[s] == "")
                                                    continue;
                                                out += data[s] + '\n';
                                            }
                                            diaerror.showDiaError("Traceback of calculation " + calc_id + ".", out);
                                        }
                                        // alert("Success: " + textStatus);
                                    }});
            },

            render: function() {
                if (!this.can_be_rendered) {
                    return;
                };
                this.$el.html(_.template($('#calculation-table-template').html(),
                                         { calculations: this.calculations.models }));
            }
        });

    var Calculation = Backbone.Model.extend({
        defaults: {
            map: null,
            layers: []
        }
    });

    var Calculations = Backbone.Collection.extend(
        {
            model: Calculation,
            url: gem_oq_server_url + "/v1/calc/list?relevant=true"
        });
    var calculations = new Calculations();

    var refresh_calcs;

    function setTimer() {
        refresh_calcs = setInterval(function() { calculations.fetch({reset: true}) }, 1000);
    }

    /* classic event management */
    $(document).ready(
        function() {
            var calculation_table = new CalculationTable({ calculations: calculations });
            calculations.fetch({reset: true});
            setTimer();

            /* XXX. Reset the input file value to ensure the change event
               will be always triggered */
            $(document).on("click", 'input[name=archive]',
                           function(e) { this.value = null; });
            $(document).on("change", 'input[name=archive]',
                           function(e) {
                               dialog.showPleaseWait('Uploading calculation', true);
                               var input = $(e.target);
                               var form = input.parents('form')[0];

                               $(form).ajaxSubmit(
                                   {
                                    xhr: function() {  // custom xhr to add progress bar management
                                        var myXhr = $.ajaxSettings.xhr();
                                        if(myXhr.upload){ // if upload property exists
                                            myXhr.upload.addEventListener('progress', progressHandlingFunction, false);
                                        }
                                        return myXhr;
                                    },
                                    success: function(data) {
                                        calculations.add(new Calculation(data));
                                    },
                                    error: function(xhr) {
                                        dialog.hidePleaseWait();
                                        var s, out, ret = $.parseJSON(xhr.responseText);
                                        out = ""
                                        for (s in ret) {
                                            if (ret[s] == "")
                                                continue;
                                            out += ret[s] + '\n';
                                        }
                                        diaerror.showDiaError("Calculation not accepted: traceback", out);
                                    }});
                           });

            $(document).on('hidden.bs.modal', 'div[id^=traceback-]',
                           function(e) {
                               setTimer();
                           });

        });
})($, Backbone, _, gem_oq_server_url);
<commit_msg>webui: add slow down to 3 secs list polling time<commit_after>/*
 Copyright (c) 2015, GEM Foundation.

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 License, or (at your option) any later version.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <https://www.gnu.org/licenses/agpl.html>.
 */

(function($, Backbone, _) {
    var progressHandlingFunction = function(progress) {
        var percent = progress.loaded / progress.total * 100;
        $('.bar').css('width', percent + '%');
        if (percent == 100) {
            dialog.hidePleaseWait();
        }
    };

    var dialog = (function ()
                  {
                      var pleaseWaitDiv = $('

Processing...

');
                      return {
                          showPleaseWait: function(msg, progress) {
                          $('h1', pleaseWaitDiv).text(msg);
                          if (progress) {
                          progressHandlingFunction({loaded: 0, total: 1});
                          } else {
                          progressHandlingFunction({loaded: 1, total: 1});
                      }
                              pleaseWaitDiv.modal();
                          },
                          hidePleaseWait: function () {
                              pleaseWaitDiv.modal('hide');
                          }
                      };
                  })();

    var diaerror = (function ()
                  {
                      var errorDiv = $('
\

\

\

\

Calculation not accepted: traceback
\

\

\

\

\
                      <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>\

\

\

\

');

//

Error

Close
');
//                      errorDiv.on("click", function(e) {
//                          this.hideDiaError();
//                      })
                      return {
                          showDiaError: function(title, msg) {
                              if (title != null) {
                                  $('.modal-title', errorDiv).html(title);
                              }
                              if (msg != null) {
                                  $('.modal-body-pre', errorDiv).html(msg);
                              }
                              errorDiv.modal();
                          },
                          hideDiaError: function () {
                              errorDiv.modal('hide');
                          }
                      };
                  })();

    var CalculationTable = Backbone.View.extend(
        {
            /* the html element where the table is rendered */
            el: $('#my-calculations'),

            initialize: function(options) {

                /* whatever happens to any calculation, re-render the table */
                _.bindAll(this, 'render');
                this.calculations = options.calculations;
                this.calculations.bind('reset', this.render);
                this.calculations.bind('add', this.render);
                this.calculations.bind('remove', this.render);

                /* if false, it prevents the table to be refreshed */
                this.can_be_rendered = true;

                this.render();
            },

            events: {
                "click .btn-danger": "remove_calculation",
                "click .btn-traceback": "show_traceback",
                "click .btn-file": "on_run_risk_clicked",
                "change .btn-file input": "on_run_risk_queued"
            },

            /* When an input dialog is opened, it is very important to not re-render the table */
            on_run_risk_clicked: function(e) {
                /* if a file input dialog has been opened do not refresh the calc table */
                this.can_be_rendered = false;
            },

            on_run_risk_queued: function(e) {
                this.can_be_rendered = true;
            },

            remove_calculation: function(e) {
                e.preventDefault();
                var calc_id = $(e.target).attr('data-calc-id');
                var view = this;
                diaerror.showDiaError("Removing calculation " + calc_id, "...");
                $.post(gem_oq_server_url + "/v1/calc/" + calc_id + "/remove"
                     ).success(
                         function(data, textStatus, jqXHR)
                         {
                             diaerror.showDiaError("Removing calculation " + calc_id, "Calculation " + calc_id + " removed.");
                             view.calculations.remove([view.calculations.get(calc_id)]);
                         }
                     ).error(
                         function(jqXHR, textStatus, errorThrown)
                         {
                             if (jqXHR.status == 404) {
                                 diaerror.showDiaError("Removing calculation " + calc_id + ".", "Failed: calculation " + calc_id + " not found.");
                             }
                             else {
                                 diaerror.showDiaError("Removing calculation " + calc_id + ".", "Failed: " + textStatus);
                             }
                         }
                     );
            },

            show_traceback: function(e) {
                var calc_id = $(e.target).attr('data-calc-id');

                var myXhr = $.ajax({url: gem_oq_server_url + "/v1/calc/" + calc_id + "/traceback",
                                    error: function (jqXHR, textStatus, errorThrown) {
                                        if (jqXHR.status == 404) {
                                            diaerror.showDiaError("Calculation " + calc_id + " not found.");
                                        }
                                        else {
                                            diaerror.showDiaError("Error retrieving traceback for calculation " + calc_id + ".", textStatus);
                                        }
                                        // alert("Error: " + textStatus);
                                    },
                                    success: function (data, textStatus, jqXHR) {
                                        if (data.length == 0) {
                                            diaerror.showDiaError("Traceback not found for calculation " + calc_id + ".", []);
                                        }
                                        else {
                                            out = ""
                                            for (s in data) {
                                                if (data[s] == "")
                                                    continue;
                                                out += data[s] + '\n';
                                            }
                                            diaerror.showDiaError("Traceback of calculation " + calc_id + ".", out);
                                        }
                                        // alert("Success: " + textStatus);
                                    }});
            },

            render: function() {
                if (!this.can_be_rendered) {
                    return;
                };
                this.$el.html(_.template($('#calculation-table-template').html(),
                                         { calculations: this.calculations.models }));
            }
        });

    var Calculation = Backbone.Model.extend({
        defaults: {
            map: null,
            layers: []
        }
    });

    var Calculations = Backbone.Collection.extend(
        {
            model: Calculation,
            url: gem_oq_server_url + "/v1/calc/list?relevant=true"
        });
    var calculations = new Calculations();

    var refresh_calcs;

    function setTimer() {
        refresh_calcs = setInterval(function() { calculations.fetch({reset: true}) }, 3000);
    }

    /* classic event management */
    $(document).ready(
        function() {
            var calculation_table = new CalculationTable({ calculations: calculations });
            calculations.fetch({reset: true});
            setTimer();

            /* XXX. Reset the input file value to ensure the change event
               will be always triggered */
            $(document).on("click", 'input[name=archive]',
                           function(e) { this.value = null; });
            $(document).on("change", 'input[name=archive]',
                           function(e) {
                               dialog.showPleaseWait('Uploading calculation', true);
                               var input = $(e.target);
                               var form = input.parents('form')[0];

                               $(form).ajaxSubmit(
                                   {
                                    xhr: function() {  // custom xhr to add progress bar management
                                        var myXhr = $.ajaxSettings.xhr();
                                        if(myXhr.upload){ // if upload property exists
                                            myXhr.upload.addEventListener('progress', progressHandlingFunction, false);
                                        }
                                        return myXhr;
                                    },
                                    success: function(data) {
                                        calculations.add(new Calculation(data));
                                    },
                                    error: function(xhr) {
                                        dialog.hidePleaseWait();
                                        var s, out, ret = $.parseJSON(xhr.responseText);
                                        out = ""
                                        for (s in ret) {
                                            if (ret[s] == "")
                                                continue;
                                            out += ret[s] + '\n';
                                        }
                                        diaerror.showDiaError("Calculation not accepted: traceback", out);
                                    }});
                           });

            $(document).on('hidden.bs.modal', 'div[id^=traceback-]',
                           function(e) {
                               setTimer();
                           });

        });
})($, Backbone, _, gem_oq_server_url);
<|endoftext|>