<commit_before>import Random from 'random-seed'
import {ZERO_POINT, INFINITY_POINT, traverse, traversalDistance, min as minPoint, isZero as isZeroPoint, compare as comparePoints} from './point-helpers'
import {getExtent} from './text-helpers'
import Iterator from './iterator'

export default class Patch {
  constructor (params = {}) {
    this.combineChanges = (params.combineChanges != null) ? Boolean(params.combineChanges) : true
    this.batchMode = (params.batchMode != null) ? Boolean(params.batchMode) : false
    if (params.seed) {
      let randomGenerator = new Random(params.seed)
      this.generateRandom = randomGenerator.random.bind(randomGenerator)
    }

    this.root = null
    this.nodesCount = 0
    this.iterator = this.buildIterator()
  }

  buildIterator () {
    return new Iterator(this)
  }

  rebalance () {
    this.createBackbone()
    this.createBalancedTree()
  }

  createBackbone () {
    let pseudoRoot = this.root
    while (pseudoRoot != null) {
      let leftChild = pseudoRoot.left
      let rightChild = pseudoRoot.right
      if (leftChild != null) {
        this.rotateNodeRight(leftChild)
        pseudoRoot = leftChild
      } else {
        pseudoRoot = rightChild
      }
    }
  }

  createBalancedTree() {
    let n = this.nodesCount
    let m = Math.pow(2, Math.floor(Math.log2(n + 1))) - 1
    this.performLeftRotations(n - m)
    while (m > 1) {
      m /= 2
      this.performLeftRotations(m - 1)
    }
  }

  performLeftRotations (count) {
    let root = this.root
    for (var i = 0; i < count; i++) {
      if (root == null) return
      let rightChild = root.right
      if (rightChild == null) return
      root = rightChild.right
      this.rotateNodeLeft(rightChild)
    }
  }

  spliceWithText (start, oldExtent, newText, options) {
    let metadata = options ? options.metadata : null
    this.splice(start, oldExtent, getExtent(newText), {text: newText, metadata})
  }

  splice (outputStart, oldExtent, newExtent, options) {
    if (isZeroPoint(oldExtent) && isZeroPoint(newExtent)) return

    let outputOldEnd = traverse(outputStart, oldExtent)
    let outputNewEnd = traverse(outputStart, newExtent)

    let startNode = this.iterator.insertSpliceBoundary(outputStart)
    startNode.isChangeStart = true
    if (this.batchMode) this.splayNode(startNode)

    let endNode = this.iterator.insertSpliceBoundary(outputOldEnd, startNode)
    endNode.isChangeEnd = true
    if (this.batchMode) this.splayNode(endNode)
    if (options && options.metadata) endNode.metadata = options.metadata

    if (this.batchMode) {
      if (startNode !== endNode && endNode.left !== startNode) this.rotateNodeRight(startNode)
    } else {
      startNode.priority = -1
      this.bubbleNodeUp(startNode)
      endNode.priority = -2
      this.bubbleNodeUp(endNode)
    }

    startNode.right = null
    startNode.inputExtent = startNode.inputLeftExtent
    startNode.outputExtent = startNode.outputLeftExtent

    endNode.outputExtent = traverse(outputNewEnd, traversalDistance(endNode.outputExtent, endNode.outputLeftExtent))
    endNode.outputLeftExtent = outputNewEnd
    endNode.newText = options && options.text

    if (endNode.isChangeStart && this.combineChanges) {
      endNode.priority = Infinity
      let rightAncestor = this.bubbleNodeDown(endNode)
      if (endNode.newText != null) {
        rightAncestor.newText = endNode.newText + rightAncestor.newText
      }
      this.deleteNode(endNode)
    } else if (comparePoints(endNode.outputLeftExtent, startNode.outputLeftExtent) === 0
        && comparePoints(endNode.inputLeftExtent, startNode.inputLeftExtent) === 0) {
      startNode.isChangeStart = endNode.isChangeStart
      this.deleteNode(endNode)
    } else if (!this.batchMode) {
      endNode.priority = this.generateRandom()
      this.bubbleNodeDown(endNode)
    }

    if (startNode.isChangeStart && startNode.isChangeEnd && this.combineChanges) {
      startNode.priority = Infinity
      let rightAncestor = this.bubbleNodeDown(startNode) || endNode
      if (startNode.newText != null) {
        rightAncestor.newText = startNode.newText + rightAncestor.newText
      }
      this.deleteNode(startNode)
    } else if (!this.batchMode) {
      startNode.priority = this.generateRandom()
      this.bubbleNodeDown(startNode)
    }
  }

  spliceInput (inputStart, oldExtent, newExtent) {
    let oldExtentIsZero = isZeroPoint(oldExtent)

    if (oldExtentIsZero && isZeroPoint(newExtent)) {
      let outputStart = this.translateInputPosition(inputStart)
      return {start: outputStart, oldExtent, newExtent}
    }

    let inputOldEnd = traverse(inputStart, oldExtent)
    let inputNewEnd = traverse(inputStart, newExtent)

    let startNode = this.iterator.insertSpliceInputBoundary(inputStart, true, oldExtentIsZero)
    if (this.batchMode) this.splayNode(startNode)
    let endNode = this.iterator.insertSpliceInputBoundary(inputOldEnd, false, oldExtentIsZero)
    if (this.batchMode) this.splayNode(endNode)

    if (this.batchMode) {
      if (startNode !== endNode && endNode.left !== startNode) this.rotateNodeRight(startNode)
    } else {
      startNode.priority = -1
      this.bubbleNodeUp(startNode)
      endNode.priority = -2
      this.bubbleNodeUp(endNode)
    }

    startNode.right = null
    startNode.inputExtent = startNode.inputLeftExtent
    startNode.outputExtent = startNode.outputLeftExtent
    startNode.isChangeStart = false

    let outputStart = startNode.outputLeftExtent
    let outputOldExtent = traversalDistance(endNode.outputLeftExtent, startNode.outputLeftExtent)

    let endNodeInputRightExtent = traversalDistance(endNode.inputExtent, endNode.inputLeftExtent)
    let endNodeOutputRightExtent = traversalDistance(endNode.outputExtent, endNode.outputLeftExtent)
    endNode.inputLeftExtent = inputNewEnd
    endNode.inputExtent = traverse(endNode.inputLeftExtent, endNodeInputRightExtent)
    endNode.outputLeftExtent = traverse(startNode.outputLeftExtent, newExtent)
    endNode.outputExtent = traverse(endNode.outputLeftExtent, endNodeOutputRightExtent)
    endNode.isChangeEnd = false
    endNode.newText = null

    let outputNewExtent = traversalDistance(endNode.outputLeftExtent, startNode.outputLeftExtent)

    if (startNode.isChangeEnd) {
      if (!this.batchMode) {
        startNode.priority = this.generateRandom()
        this.bubbleNodeDown(startNode)
      }
    } else {
      this.deleteNode(startNode)
    }

    if (endNode.isChangeStart) {
      if (!this.batchMode) {
        endNode.priority = this.generateRandom()
        this.bubbleNodeDown(endNode)
      }
    } else {
      this.deleteNode(endNode)
    }

    return {
      start: outputStart,
      oldExtent: outputOldExtent,
      newExtent: outputNewExtent
    }
  }

  isChangedAtInputPosition (inputPosition) {
    this.iterator.seekToInputPosition(inputPosition)
    return this.iterator.inChange()
  }

  isChangedAtOutputPosition (outputPosition) {
    this.iterator.seekToOutputPosition(outputPosition)
    return this.iterator.inChange()
  }

  translateInputPosition (inputPosition) {
    this.iterator.seekToInputPosition(inputPosition)
    let overshoot = traversalDistance(inputPosition, this.iterator.getInputStart())
    let outputPosition = minPoint(traverse(this.iterator.getOutputStart(), overshoot), this.iterator.getOutputEnd())
    this.splayNode(this.iterator.getCurrentNode())
    return outputPosition
  }

  translateOutputPosition (outputPosition) {
    this.iterator.seekToOutputPosition(outputPosition)
    let overshoot = traversalDistance(outputPosition, this.iterator.getOutputStart())
    let inputPosition = minPoint(traverse(this.iterator.getInputStart(), overshoot), this.iterator.getInputEnd())
    this.splayNode(this.iterator.getCurrentNode())
    return inputPosition
  }

  getChanges () {
    return this.iterator.getChanges()
  }

  deleteNode (node) {
    node.priority = Infinity
    this.bubbleNodeDown(node)
    if (node.parent) {
      if (node.parent.left === node) {
        node.parent.left = null
      } else {
        node.parent.right = null
        node.parent.inputExtent = node.parent.inputLeftExtent
        node.parent.outputExtent = node.parent.outputLeftExtent
        let ancestor = node.parent
        while (ancestor.parent && ancestor.parent.right === ancestor) {
          ancestor.parent.inputExtent = traverse(ancestor.parent.inputLeftExtent, ancestor.inputExtent)
          ancestor.parent.outputExtent = traverse(ancestor.parent.outputLeftExtent, ancestor.outputExtent)
          ancestor = ancestor.parent
        }
      }

      if (this.batchMode) this.splayNode(node.parent)
    } else {
      this.root = null
    }

    this.nodesCount--
  }

  bubbleNodeUp (node) {
    while (node.parent && node.priority < node.parent.priority) {
      if (node === node.parent.left) {
        this.rotateNodeRight(node)
      } else {
        this.rotateNodeLeft(node)
      }
    }
  }

  bubbleNodeDown (node) {
    let rightAncestor

    while (true) {
      let leftChildPriority = node.left ? node.left.priority : Infinity
      let rightChildPriority = node.right ? node.right.priority : Infinity

      if (leftChildPriority < rightChildPriority && leftChildPriority < node.priority) {
        this.rotateNodeRight(node.left)
      } else if (rightChildPriority < node.priority) {
        rightAncestor = node.right
        this.rotateNodeLeft(node.right)
      } else {
        break
      }
    }

    return rightAncestor
  }

  splayNode (node) {
    if (node == null) return

    while (true) {
      if (this.isNodeLeftChild(node.parent) && this.isNodeRightChild(node)) { // zig-zag
        this.rotateNodeLeft(node)
        this.rotateNodeRight(node)
      } else if (this.isNodeRightChild(node.parent) && this.isNodeLeftChild(node)) { // zig-zag
        this.rotateNodeRight(node)
        this.rotateNodeLeft(node)
      } else if (this.isNodeLeftChild(node.parent) && this.isNodeLeftChild(node)) { // zig-zig
        this.rotateNodeRight(node.parent)
        this.rotateNodeRight(node)
      } else if (this.isNodeRightChild(node.parent) && this.isNodeRightChild(node)) { // zig-zig
        this.rotateNodeLeft(node.parent)
        this.rotateNodeLeft(node)
      } else { // zig
        if (this.isNodeLeftChild(node)) {
          this.rotateNodeRight(node)
        } else if (this.isNodeRightChild(node)) {
          this.rotateNodeLeft(node)
        }

        return
      }
    }
  }

  isNodeLeftChild (node) {
    return node != null && node.parent != null && node.parent.left === node
  }

  isNodeRightChild (node) {
    return node != null && node.parent != null && node.parent.right === node
  }

  rotateNodeLeft (pivot) {
    let root = pivot.parent

    if (root.parent) {
      if (root === root.parent.left) {
        root.parent.left = pivot
      } else {
        root.parent.right = pivot
      }
    } else {
      this.root = pivot
    }
    pivot.parent = root.parent

    root.right = pivot.left
    if (root.right) {
      root.right.parent = root
    }

    pivot.left = root
    pivot.left.parent = pivot

    pivot.inputLeftExtent = traverse(root.inputLeftExtent, pivot.inputLeftExtent)
    pivot.inputExtent = traverse(pivot.inputLeftExtent, (pivot.right ? pivot.right.inputExtent : ZERO_POINT))
    root.inputExtent = traverse(root.inputLeftExtent, (root.right ? root.right.inputExtent : ZERO_POINT))

    pivot.outputLeftExtent = traverse(root.outputLeftExtent, pivot.outputLeftExtent)
    pivot.outputExtent = traverse(pivot.outputLeftExtent, (pivot.right ? pivot.right.outputExtent : ZERO_POINT))
    root.outputExtent = traverse(root.outputLeftExtent, (root.right ? root.right.outputExtent : ZERO_POINT))
  }

  rotateNodeRight (pivot) {
    let root = pivot.parent

    if (root.parent) {
      if (root === root.parent.left) {
        root.parent.left = pivot
      } else {
        root.parent.right = pivot
      }
    } else {
      this.root = pivot
    }
    pivot.parent = root.parent

    root.left = pivot.right
    if (root.left) {
      root.left.parent = root
    }

    pivot.right = root
    pivot.right.parent = pivot

    root.inputLeftExtent = traversalDistance(root.inputLeftExtent, pivot.inputLeftExtent)
    root.inputExtent = traversalDistance(root.inputExtent, pivot.inputLeftExtent)
    pivot.inputExtent = traverse(pivot.inputLeftExtent, root.inputExtent)

    root.outputLeftExtent = traversalDistance(root.outputLeftExtent, pivot.outputLeftExtent)
    root.outputExtent = traversalDistance(root.outputExtent, pivot.outputLeftExtent)
    pivot.outputExtent = traverse(pivot.outputLeftExtent, root.outputExtent)
  }

  generateRandom () {
    return Math.random()
  }
}
<commit_msg>:art:<commit_after>import Random from 'random-seed'
import {ZERO_POINT, INFINITY_POINT, traverse, traversalDistance, min as minPoint, isZero as isZeroPoint, compare as comparePoints} from './point-helpers'
import {getExtent} from './text-helpers'
import Iterator from './iterator'

export default class Patch {
  constructor (params = {}) {
    this.combineChanges = (params.combineChanges != null) ? Boolean(params.combineChanges) : true
    this.batchMode = (params.batchMode != null) ? Boolean(params.batchMode) : false
    if (params.seed) {
      let randomGenerator = new Random(params.seed)
      this.generateRandom = randomGenerator.random.bind(randomGenerator)
    }

    this.root = null
    this.nodesCount = 0
    this.iterator = this.buildIterator()
  }

  buildIterator () {
    return new Iterator(this)
  }

  rebalance () {
    this.createBackbone()
    this.createBalancedTree()
  }

  createBackbone () {
    let pseudoRoot = this.root
    while (pseudoRoot != null) {
      let leftChild = pseudoRoot.left
      let rightChild = pseudoRoot.right
      if (leftChild != null) {
        this.rotateNodeRight(leftChild)
        pseudoRoot = leftChild
      } else {
        pseudoRoot = rightChild
      }
    }
  }

  createBalancedTree() {
    let n = this.nodesCount
    let m = Math.pow(2, Math.floor(Math.log2(n + 1))) - 1
    this.performRebalancingRotations(n - m)
    while (m > 1) {
      m /= 2
      this.performRebalancingRotations(m - 1)
    }
  }

  performRebalancingRotations (count) {
    let root = this.root
    for (var i = 0; i < count; i++) {
      if (root == null) return
      let rightChild = root.right
      if (rightChild == null) return
      root = rightChild.right
      this.rotateNodeLeft(rightChild)
    }
  }

  spliceWithText (start, oldExtent, newText, options) {
    let metadata = options ? options.metadata : null
    this.splice(start, oldExtent, getExtent(newText), {text: newText, metadata})
  }

  splice (outputStart, oldExtent, newExtent, options) {
    if (isZeroPoint(oldExtent) && isZeroPoint(newExtent)) return

    let outputOldEnd = traverse(outputStart, oldExtent)
    let outputNewEnd = traverse(outputStart, newExtent)

    let startNode = this.iterator.insertSpliceBoundary(outputStart)
    startNode.isChangeStart = true
    if (this.batchMode) this.splayNode(startNode)

    let endNode = this.iterator.insertSpliceBoundary(outputOldEnd, startNode)
    endNode.isChangeEnd = true
    if (this.batchMode) this.splayNode(endNode)
    if (options && options.metadata) endNode.metadata = options.metadata

    if (this.batchMode) {
      if (startNode !== endNode && endNode.left !== startNode) this.rotateNodeRight(startNode)
    } else {
      startNode.priority = -1
      this.bubbleNodeUp(startNode)
      endNode.priority = -2
      this.bubbleNodeUp(endNode)
    }

    startNode.right = null
    startNode.inputExtent = startNode.inputLeftExtent
    startNode.outputExtent = startNode.outputLeftExtent

    endNode.outputExtent = traverse(outputNewEnd, traversalDistance(endNode.outputExtent, endNode.outputLeftExtent))
    endNode.outputLeftExtent = outputNewEnd
    endNode.newText = options && options.text

    if (endNode.isChangeStart && this.combineChanges) {
      endNode.priority = Infinity
      let rightAncestor = this.bubbleNodeDown(endNode)
      if (endNode.newText != null) {
        rightAncestor.newText = endNode.newText + rightAncestor.newText
      }
      this.deleteNode(endNode)
    } else if (comparePoints(endNode.outputLeftExtent, startNode.outputLeftExtent) === 0
        && comparePoints(endNode.inputLeftExtent, startNode.inputLeftExtent) === 0) {
      startNode.isChangeStart = endNode.isChangeStart
      this.deleteNode(endNode)
    } else if (!this.batchMode) {
      endNode.priority = this.generateRandom()
      this.bubbleNodeDown(endNode)
    }

    if (startNode.isChangeStart && startNode.isChangeEnd && this.combineChanges) {
      startNode.priority = Infinity
      let rightAncestor = this.bubbleNodeDown(startNode) || endNode
      if (startNode.newText != null) {
        rightAncestor.newText = startNode.newText + rightAncestor.newText
      }
      this.deleteNode(startNode)
    } else if (!this.batchMode) {
      startNode.priority = this.generateRandom()
      this.bubbleNodeDown(startNode)
    }
  }

  spliceInput (inputStart, oldExtent, newExtent) {
    let oldExtentIsZero = isZeroPoint(oldExtent)

    if (oldExtentIsZero && isZeroPoint(newExtent)) {
      let outputStart = this.translateInputPosition(inputStart)
      return {start: outputStart, oldExtent, newExtent}
    }

    let inputOldEnd = traverse(inputStart, oldExtent)
    let inputNewEnd = traverse(inputStart, newExtent)

    let startNode = this.iterator.insertSpliceInputBoundary(inputStart, true, oldExtentIsZero)
    if (this.batchMode) this.splayNode(startNode)
    let endNode = this.iterator.insertSpliceInputBoundary(inputOldEnd, false, oldExtentIsZero)
    if (this.batchMode) this.splayNode(endNode)

    if (this.batchMode) {
      if (startNode !== endNode && endNode.left !== startNode) this.rotateNodeRight(startNode)
    } else {
      startNode.priority = -1
      this.bubbleNodeUp(startNode)
      endNode.priority = -2
      this.bubbleNodeUp(endNode)
    }

    startNode.right = null
    startNode.inputExtent = startNode.inputLeftExtent
    startNode.outputExtent = startNode.outputLeftExtent
    startNode.isChangeStart = false

    let outputStart = startNode.outputLeftExtent
    let outputOldExtent = traversalDistance(endNode.outputLeftExtent, startNode.outputLeftExtent)

    let endNodeInputRightExtent = traversalDistance(endNode.inputExtent, endNode.inputLeftExtent)
    let endNodeOutputRightExtent = traversalDistance(endNode.outputExtent, endNode.outputLeftExtent)
    endNode.inputLeftExtent = inputNewEnd
    endNode.inputExtent = traverse(endNode.inputLeftExtent, endNodeInputRightExtent)
    endNode.outputLeftExtent = traverse(startNode.outputLeftExtent, newExtent)
    endNode.outputExtent = traverse(endNode.outputLeftExtent, endNodeOutputRightExtent)
    endNode.isChangeEnd = false
    endNode.newText = null

    let outputNewExtent = traversalDistance(endNode.outputLeftExtent, startNode.outputLeftExtent)

    if (startNode.isChangeEnd) {
      if (!this.batchMode) {
        startNode.priority = this.generateRandom()
        this.bubbleNodeDown(startNode)
      }
    } else {
      this.deleteNode(startNode)
    }

    if (endNode.isChangeStart) {
      if (!this.batchMode) {
        endNode.priority = this.generateRandom()
        this.bubbleNodeDown(endNode)
      }
    } else {
      this.deleteNode(endNode)
    }

    return {
      start: outputStart,
      oldExtent: outputOldExtent,
      newExtent: outputNewExtent
    }
  }

  isChangedAtInputPosition (inputPosition) {
    this.iterator.seekToInputPosition(inputPosition)
    return this.iterator.inChange()
  }

  isChangedAtOutputPosition (outputPosition) {
    this.iterator.seekToOutputPosition(outputPosition)
    return this.iterator.inChange()
  }

  translateInputPosition (inputPosition) {
    this.iterator.seekToInputPosition(inputPosition)
    let overshoot = traversalDistance(inputPosition, this.iterator.getInputStart())
    let outputPosition = minPoint(traverse(this.iterator.getOutputStart(), overshoot), this.iterator.getOutputEnd())
    this.splayNode(this.iterator.getCurrentNode())
    return outputPosition
  }

  translateOutputPosition (outputPosition) {
    this.iterator.seekToOutputPosition(outputPosition)
    let overshoot = traversalDistance(outputPosition, this.iterator.getOutputStart())
    let inputPosition = minPoint(traverse(this.iterator.getInputStart(), overshoot), this.iterator.getInputEnd())
    this.splayNode(this.iterator.getCurrentNode())
    return inputPosition
  }

  getChanges () {
    return this.iterator.getChanges()
  }

  deleteNode (node) {
    node.priority = Infinity
    this.bubbleNodeDown(node)
    if (node.parent) {
      if (node.parent.left === node) {
        node.parent.left = null
      } else {
        node.parent.right = null
        node.parent.inputExtent = node.parent.inputLeftExtent
        node.parent.outputExtent = node.parent.outputLeftExtent
        let ancestor = node.parent
        while (ancestor.parent && ancestor.parent.right === ancestor) {
          ancestor.parent.inputExtent = traverse(ancestor.parent.inputLeftExtent, ancestor.inputExtent)
          ancestor.parent.outputExtent = traverse(ancestor.parent.outputLeftExtent, ancestor.outputExtent)
          ancestor = ancestor.parent
        }
      }

      if (this.batchMode) this.splayNode(node.parent)
    } else {
      this.root = null
    }

    this.nodesCount--
  }

  bubbleNodeUp (node) {
    while (node.parent && node.priority < node.parent.priority) {
      if (node === node.parent.left) {
        this.rotateNodeRight(node)
      } else {
        this.rotateNodeLeft(node)
      }
    }
  }

  bubbleNodeDown (node) {
    let rightAncestor

    while (true) {
      let leftChildPriority = node.left ? node.left.priority : Infinity
      let rightChildPriority = node.right ? node.right.priority : Infinity

      if (leftChildPriority < rightChildPriority && leftChildPriority < node.priority) {
        this.rotateNodeRight(node.left)
      } else if (rightChildPriority < node.priority) {
        rightAncestor = node.right
        this.rotateNodeLeft(node.right)
      } else {
        break
      }
    }

    return rightAncestor
  }

  splayNode (node) {
    if (node == null) return

    while (true) {
      if (this.isNodeLeftChild(node.parent) && this.isNodeRightChild(node)) { // zig-zag
        this.rotateNodeLeft(node)
        this.rotateNodeRight(node)
      } else if (this.isNodeRightChild(node.parent) && this.isNodeLeftChild(node)) { // zig-zag
        this.rotateNodeRight(node)
        this.rotateNodeLeft(node)
      } else if (this.isNodeLeftChild(node.parent) && this.isNodeLeftChild(node)) { // zig-zig
        this.rotateNodeRight(node.parent)
        this.rotateNodeRight(node)
      } else if (this.isNodeRightChild(node.parent) && this.isNodeRightChild(node)) { // zig-zig
        this.rotateNodeLeft(node.parent)
        this.rotateNodeLeft(node)
      } else { // zig
        if (this.isNodeLeftChild(node)) {
          this.rotateNodeRight(node)
        } else if (this.isNodeRightChild(node)) {
          this.rotateNodeLeft(node)
        }

        return
      }
    }
  }

  isNodeLeftChild (node) {
    return node != null && node.parent != null && node.parent.left === node
  }

  isNodeRightChild (node) {
    return node != null && node.parent != null && node.parent.right === node
  }

  rotateNodeLeft (pivot) {
    let root = pivot.parent

    if (root.parent) {
      if (root === root.parent.left) {
        root.parent.left = pivot
      } else {
        root.parent.right = pivot
      }
    } else {
      this.root = pivot
    }
    pivot.parent = root.parent

    root.right = pivot.left
    if (root.right) {
      root.right.parent = root
    }

    pivot.left = root
    pivot.left.parent = pivot

    pivot.inputLeftExtent = traverse(root.inputLeftExtent, pivot.inputLeftExtent)
    pivot.inputExtent = traverse(pivot.inputLeftExtent, (pivot.right ? pivot.right.inputExtent : ZERO_POINT))
    root.inputExtent = traverse(root.inputLeftExtent, (root.right ? root.right.inputExtent : ZERO_POINT))

    pivot.outputLeftExtent = traverse(root.outputLeftExtent, pivot.outputLeftExtent)
    pivot.outputExtent = traverse(pivot.outputLeftExtent, (pivot.right ? pivot.right.outputExtent : ZERO_POINT))
    root.outputExtent = traverse(root.outputLeftExtent, (root.right ? root.right.outputExtent : ZERO_POINT))
  }

  rotateNodeRight (pivot) {
    let root = pivot.parent

    if (root.parent) {
      if (root === root.parent.left) {
        root.parent.left = pivot
      } else {
        root.parent.right = pivot
      }
    } else {
      this.root = pivot
    }
    pivot.parent = root.parent

    root.left = pivot.right
    if (root.left) {
      root.left.parent = root
    }

    pivot.right = root
    pivot.right.parent = pivot

    root.inputLeftExtent = traversalDistance(root.inputLeftExtent, pivot.inputLeftExtent)
    root.inputExtent = traversalDistance(root.inputExtent, pivot.inputLeftExtent)
    pivot.inputExtent = traverse(pivot.inputLeftExtent, root.inputExtent)

    root.outputLeftExtent = traversalDistance(root.outputLeftExtent, pivot.outputLeftExtent)
    root.outputExtent = traversalDistance(root.outputExtent, pivot.outputLeftExtent)
    pivot.outputExtent = traverse(pivot.outputLeftExtent, root.outputExtent)
  }

  generateRandom () {
    return Math.random()
  }
}
<|endoftext|>


<commit_before>var FEATURE_NAME = 'Photo'

var taker = function(source, onSuccess, onError) {
	var options = {
		correctOrientation: true,
		mediaType: navigator.camera.MediaType.PICTURE,
        encodingType: Camera.EncodingType.JPEG,
        destinationType: Camera.DestinationType.FILE_URI,
        sourceType: source
	};
	navigator.camera.getPicture(
		function(uri) {
			console.log("Resolving file uri: " + uri);
			resolveLocalFileSystemURL(uri,
				function(entry) {
					console.log("Loading file entry: " + entry);
					entry.file(
						function(file) {
							console.log("Reading file: " + file);
							var reader = new FileReader();
							reader.onload = function(event) {
								var array = reader.result;
								console.log("Creating Blob from ArrayBuffer: " + array);
								var blob = new Blob([array], 'image/jpeg')
								onSuccess(blob);
							};
							reader.onerror = function(event) {
								var error = reader.error;
								console.log("Failed to read file: " + error);
								onError(error);
							};
							reader.readAsArrayBuffer(file);
						},
						function(error) {
							console.log("Failed to load file: " + error);
							onError(error);
						}
					);
				},
				function(error) {
					console.log("Failed to resolve uri: " + error);
					onError(error);
				}
			);
		},
		function(error) {
			console.log("Failed to getPicture: " + error);
			onError(error);
		},
		options
	);
    console.log('Result: ' + some);
}

module.exports = {
    select : function(onSuccess, onError) {
        console.log(FEATURE_NAME + '#select');
        taker(Camera.PictureSourceType.PHOTOLIBRARY, onSuccess, onError);
    },
    take : function(onSuccess, onError) {
        console.log(FEATURE_NAME + '#take');
        taker(Camera.PictureSourceType.CAMERA, onSuccess, onError);
    }
};
<commit_msg>onload から onloadend に変更<commit_after>var FEATURE_NAME = 'Photo'

var taker = function(source, onSuccess, onError) {
	navigator.camera.getPicture(
		function(uri) {
			console.log("Resolving file uri: " + uri);
			resolveLocalFileSystemURL(uri,
				function(entry) {
					console.log("Loading file entry: " + entry);
					entry.file(
						function(file) {
							console.log("Reading file: " + file);
							var reader = new FileReader();
							reader.onloadend = function(event) {
								var array = reader.result;
								console.log("Creating Blob from ArrayBuffer: " + array.byteLength + " bytes");
								var blob = new Blob([array], {type: 'image/jpeg'});
								console.log("Photo Blob: " + blob.size + " bytes");
								onSuccess(blob);
							};
							reader.onerror = function(event) {
								var error = reader.error;
								console.log("Failed to read file: " + error);
								onError(error);
							};
							reader.readAsArrayBuffer(file);
						},
						function(error) {
							console.log("Failed to load file: " + error);
							onError(error);
						}
					);
				},
				function(error) {
					console.log("Failed to resolve uri: " + error);
					onError(error);
				}
			);
		},
		function(error) {
			console.log("Failed to getPicture: " + error);
			onError(error);
		},
		{
			correctOrientation: true,
			mediaType: navigator.camera.MediaType.PICTURE,
	        encodingType: Camera.EncodingType.JPEG,
	        destinationType: Camera.DestinationType.FILE_URI,
	        sourceType: source
		}
	);
    console.log('Result: ' + some);
}

module.exports = {
    select : function(onSuccess, onError) {
        console.log(FEATURE_NAME + '#select');
        taker(Camera.PictureSourceType.PHOTOLIBRARY, onSuccess, onError);
    },
    take : function(onSuccess, onError) {
        console.log(FEATURE_NAME + '#take');
        taker(Camera.PictureSourceType.CAMERA, onSuccess, onError);
    }
};
<|endoftext|>


<commit_before>export default function(Vue, Rx) {
    return {
        created() {
            const vm = this;
            let pipes = vm.$options.pipes;

            if (pipes instanceof Function) {
                pipes = pipes.call(vm);
            }

            if (pipes && pipes instanceof Object) {
                vm._pipeSubs = [];
                vm._watchSubs = [];
                vm.$pipes = vm.$pipes || {};
                vm.$pipesError = vm.$pipesError || {};
                vm.$pipesOld = vm.$pipesOld || {};
                vm.$inits = vm.$inits || {};

                vm.$refreshAll = () => {
                    Object.keys().forEach(
                        (key) => link(vm.$inits[key], key, vm)
                    )
                };

                const propName = key => key;
                const propOldName = key => `${key}Old`;
                const propPipeName = key => `${key}Pipe`;
                const propApplyName = key => `${key}Apply`;
                const propErrorName = key => `${key}Error`;
                const propProcessName = key => `${key}Process`;
                const propOldPipeName = key => `${key}OldPipe`;
                const propErrorPipeName = key => `${key}ErrorPipe`;

                const link = function(func, key, ...params) {
                    const value = func.call(vm, ...params);

                    if (value instanceof Function) {
                        vm[propApplyName(key)] = (...args) => {
                            link(value, key, ...args);
                        };
                    } else if (value instanceof Object && value.next instanceof Function) {
                        vm[propApplyName(key)] = (arg) => {
                            vm[propPipeName(key)].next(arg);
                        };

                        vm._pipeSubs.push(value.subscribe(
                            v => vm[propPipeName(key)].next(v),
                            v => vm[propErrorPipeName(key)].next(v)
                        ));
                    } else {
                        const observable = value instanceof Object && value.subscribe instanceof Function ?
                            value :
                            Rx.Observable.of(value);

                        vm[propProcessName(key)] = true;
                        vm[propName(key)] = null;
                        vm[propErrorName(key)] = null;

                        if (!vm[propApplyName(key)]) {
                            vm.$inits[key] = func;
                            vm[propApplyName(key)] = (arg) => {
                                vm[propPipeName(key)].next(arg);
                            };
                        }

                        observable.subscribe(
                            v => vm[propPipeName(key)].next(v),
                            v => vm[propErrorPipeName(key)].next(v)
                        );
                    }
                };

                Object.keys(vm.$props).forEach(key => {
                    const subj = new Rx.Subject();
                    const subjOld = new Rx.Subject();

                    Vue.util.defineReactive(vm, propOldName(key), undefined);

                    vm[propPipeName(key)] = subj;
                    vm[propOldPipeName(key)] = subjOld;

                    vm._watchSubs.push(vm.$watch(key, (newValue, oldValue) => {
                        subj.next(newValue);
                        subjOld.next(oldValue);
                    }));

                    vm._pipeSubs.push(subj.subscribe(value => {
                        vm[propName(key)] = value;
                    }));

                    vm._pipeSubs.push(subjOld.subscribe(value => {
                        vm[propOldName(key)] = value;
                    }));
                });

                Object.keys(pipes).forEach(key => {
                    const subj = new Rx.BehaviorSubject();
                    const subjOld = new Rx.BehaviorSubject();
                    const subjError = new Rx.BehaviorSubject();

                    Vue.util.defineReactive(vm, propName(key), undefined);
                    Vue.util.defineReactive(vm, propOldName(key), undefined);
                    Vue.util.defineReactive(vm, propErrorName(key), undefined);
                    Vue.util.defineReactive(vm, propProcessName(key), false);

                    vm[propPipeName(key)] = subj;
                    vm[propOldPipeName(key)] = subjOld;
                    vm[propErrorPipeName(key)] = subjError;
                    vm.$pipes[key] = subj;
                    vm.$pipesOld[key] = subjOld;
                    vm.$pipesError[key] = subjError;

                    vm._pipeSubs.push(subj.subscribe(
                        value => {
                            vm.$emit(propProcessName(key), false);
                            vm.$emit(propName(key), value);
                            vm.$emit(propErrorName(key), null);

                            subjOld.next(vm[propName(key)]);

                            vm[propProcessName(key)] = false;
                            vm[propName(key)] = value;
                            vm[propErrorName(key)] = null;
                        }
                    ));

                    vm._pipeSubs.push(subjOld.subscribe(
                        value => {
                            vm[propOldName(key)] = value;
                        }
                    ));

                    vm._pipeSubs.push(subjError.filter(v => v).subscribe(
                        error => {
                            console.error(error);

                            vm.$emit(propName(key), null);
                            vm.$emit(propErrorName(key), error);
                            vm.$emit(propProcessName(key), false);

                            vm[propName(key)] = null;
                            vm[propErrorName(key)] = error;
                            vm[propProcessName(key)] = false;
                        }
                    ));

                    link(pipes[key], key, vm);
                });
            }
        },

        beforeDestroy() {
            this._pipeSubs.length && this._pipeSubs.forEach(
                sub => sub && (sub.dispose && sub.dispose() || sub.unsubscribe && sub.unsubscribe())
            );
            this._watchSubs.length && this._watchSubs.forEach(sub => sub && sub());
        }
    };
};
<commit_msg>Fixed a typo<commit_after>export default function(Vue, Rx) {
    return {
        created() {
            const vm = this;
            let pipes = vm.$options.pipes;

            if (pipes instanceof Function) {
                pipes = pipes.call(vm);
            }

            if (pipes && pipes instanceof Object) {
                vm._pipeSubs = [];
                vm._watchSubs = [];
                vm.$pipes = vm.$pipes || {};
                vm.$pipesError = vm.$pipesError || {};
                vm.$pipesOld = vm.$pipesOld || {};
                vm.$inits = vm.$inits || {};

                vm.$refreshAll = () => {
                    Object.keys(vm.$inits).forEach(
                        (key) => link(vm.$inits[key], key, vm)
                    )
                };

                const propName = key => key;
                const propOldName = key => `${key}Old`;
                const propPipeName = key => `${key}Pipe`;
                const propApplyName = key => `${key}Apply`;
                const propErrorName = key => `${key}Error`;
                const propProcessName = key => `${key}Process`;
                const propOldPipeName = key => `${key}OldPipe`;
                const propErrorPipeName = key => `${key}ErrorPipe`;

                const link = function(func, key, ...params) {
                    const value = func.call(vm, ...params);

                    if (value instanceof Function) {
                        vm[propApplyName(key)] = (...args) => {
                            link(value, key, ...args);
                        };
                    } else if (value instanceof Object && value.next instanceof Function) {
                        vm[propApplyName(key)] = (arg) => {
                            vm[propPipeName(key)].next(arg);
                        };

                        vm._pipeSubs.push(value.subscribe(
                            v => vm[propPipeName(key)].next(v),
                            v => vm[propErrorPipeName(key)].next(v)
                        ));
                    } else {
                        const observable = value instanceof Object && value.subscribe instanceof Function ?
                            value :
                            Rx.Observable.of(value);

                        vm[propProcessName(key)] = true;
                        vm[propName(key)] = null;
                        vm[propErrorName(key)] = null;

                        if (!vm[propApplyName(key)]) {
                            vm.$inits[key] = func;
                            vm[propApplyName(key)] = (arg) => {
                                vm[propPipeName(key)].next(arg);
                            };
                        }

                        observable.subscribe(
                            v => vm[propPipeName(key)].next(v),
                            v => vm[propErrorPipeName(key)].next(v)
                        );
                    }
                };

                Object.keys(vm.$props).forEach(key => {
                    const subj = new Rx.Subject();
                    const subjOld = new Rx.Subject();

                    Vue.util.defineReactive(vm, propOldName(key), undefined);

                    vm[propPipeName(key)] = subj;
                    vm[propOldPipeName(key)] = subjOld;

                    vm._watchSubs.push(vm.$watch(key, (newValue, oldValue) => {
                        subj.next(newValue);
                        subjOld.next(oldValue);
                    }));

                    vm._pipeSubs.push(subj.subscribe(value => {
                        vm[propName(key)] = value;
                    }));

                    vm._pipeSubs.push(subjOld.subscribe(value => {
                        vm[propOldName(key)] = value;
                    }));
                });

                Object.keys(pipes).forEach(key => {
                    const subj = new Rx.BehaviorSubject();
                    const subjOld = new Rx.BehaviorSubject();
                    const subjError = new Rx.BehaviorSubject();

                    Vue.util.defineReactive(vm, propName(key), undefined);
                    Vue.util.defineReactive(vm, propOldName(key), undefined);
                    Vue.util.defineReactive(vm, propErrorName(key), undefined);
                    Vue.util.defineReactive(vm, propProcessName(key), false);

                    vm[propPipeName(key)] = subj;
                    vm[propOldPipeName(key)] = subjOld;
                    vm[propErrorPipeName(key)] = subjError;
                    vm.$pipes[key] = subj;
                    vm.$pipesOld[key] = subjOld;
                    vm.$pipesError[key] = subjError;

                    vm._pipeSubs.push(subj.subscribe(
                        value => {
                            vm.$emit(propProcessName(key), false);
                            vm.$emit(propName(key), value);
                            vm.$emit(propErrorName(key), null);

                            subjOld.next(vm[propName(key)]);

                            vm[propProcessName(key)] = false;
                            vm[propName(key)] = value;
                            vm[propErrorName(key)] = null;
                        }
                    ));

                    vm._pipeSubs.push(subjOld.subscribe(
                        value => {
                            vm[propOldName(key)] = value;
                        }
                    ));

                    vm._pipeSubs.push(subjError.filter(v => v).subscribe(
                        error => {
                            console.error(error);

                            vm.$emit(propName(key), null);
                            vm.$emit(propErrorName(key), error);
                            vm.$emit(propProcessName(key), false);

                            vm[propName(key)] = null;
                            vm[propErrorName(key)] = error;
                            vm[propProcessName(key)] = false;
                        }
                    ));

                    link(pipes[key], key, vm);
                });
            }
        },

        beforeDestroy() {
            this._pipeSubs.length && this._pipeSubs.forEach(
                sub => sub && (sub.dispose && sub.dispose() || sub.unsubscribe && sub.unsubscribe())
            );
            this._watchSubs.length && this._watchSubs.forEach(sub => sub && sub());
        }
    };
};
<|endoftext|>


<commit_before>const expireJobSuffix = '__expired';

module.exports = {
  create,
  insertVersion,
  getVersion,
  versionTableExists,
  fetchNextJob,
  completeJob,
  cancelJob,
  failJob,
  insertJob,
  expire,
  archive,
  expireJobSuffix
};

function create(schema) {
  return [
    createSchema(schema),
    createVersionTable(schema),
    createJobStateEnum(schema),
    createJobTable(schema),
    createIndexSingletonOn(schema),
    createIndexSingletonKeyOn(schema),
    createIndexSingletonKey(schema)
  ];
}

function createSchema(schema) {
  return `
    CREATE SCHEMA IF NOT EXISTS ${schema}
  `;
}

function createVersionTable(schema) {
  return `
    CREATE TABLE IF NOT EXISTS ${schema}.version (
      version text primary key
    )
  `;
}

function createJobStateEnum(schema) {
  // ENUM definition order is important
  // base type is numeric and first values are less than last values
  return `
    CREATE TYPE ${schema}.job_state AS ENUM (
      'created',
      'retry',
      'active',
      'complete',
      'expired',
      'cancelled',
      'failed'
    )
  `;
}

function createJobTable(schema) {
  return `
    CREATE TABLE IF NOT EXISTS ${schema}.job (
      id uuid primary key not null,
      name text not null,
      data jsonb,
      state ${schema}.job_state not null,
      retryLimit integer not null default(0),
      retryCount integer not null default(0),
      startIn interval,
      startedOn timestamp with time zone,
      singletonKey text,
      singletonOn timestamp without time zone,
      expireIn interval,
      createdOn timestamp with time zone not null default now(),
      completedOn timestamp with time zone
    )
  `;
}

function createIndexSingletonKey(schema){
  // anything with singletonKey means "only 1 job can be queued or active at a time"
  return `
    CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' AND singletonOn IS NULL
  `;
}

function createIndexSingletonOn(schema){
  // anything with singletonOn means "only 1 job within this time period, queued, active or completed"
  return `
    CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL
  `;
}

function createIndexSingletonKeyOn(schema){
  // anything with both singletonOn and singletonKey means "only 1 job within this time period with this key, queued, active or completed"
  return `
    CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'
  `;
}

function getVersion(schema) {
  return `
    SELECT version from ${schema}.version
  `;
}

function versionTableExists(schema) {
  return `
    SELECT to_regclass('${schema}.version') as name
  `;
}

function insertVersion(schema) {
  return `
    INSERT INTO ${schema}.version(version) VALUES ($1)
  `;
}

function fetchNextJob(schema) {
  return `
    WITH nextJob as (
      SELECT id
      FROM ${schema}.job
      WHERE state < 'active'
        AND name = $1
        AND (createdOn + startIn) < now()
      ORDER BY createdOn, id
      LIMIT 1
      FOR UPDATE SKIP LOCKED
    )
    UPDATE ${schema}.job SET
      state = 'active',
      startedOn = now(),
      retryCount = CASE WHEN state = 'retry' THEN retryCount + 1 ELSE retryCount END
    FROM nextJob
    WHERE ${schema}.job.id = nextJob.id
    RETURNING ${schema}.job.id, ${schema}.job.data
  `;
}

function completeJob(schema){
  return `
    UPDATE ${schema}.job
    SET completedOn = now(),
      state = 'complete'
    WHERE id = $1
      AND state = 'active'
    `;
}

function cancelJob(schema){
  return `
    UPDATE ${schema}.job
    SET completedOn = now(),
      state = 'cancelled'
    WHERE id = $1
      AND state < 'complete'
  `;
}

function failJob(schema){
  return `
    UPDATE ${schema}.job
    SET completedOn = now(),
      state = 'failed'
    WHERE id = $1
      AND state < 'complete'
  `;
}

function insertJob(schema) {
  return `
    INSERT INTO ${schema}.job (id, name, state, retryLimit, startIn, expireIn, data, singletonKey, singletonOn)
    VALUES (
      $1, $2, 'created', $3, CAST($4 as interval), CAST($5 as interval), $6, $7,
      CASE WHEN $8::integer IS NOT NULL THEN 'epoch'::timestamp + '1 second'::interval * ($8 * floor((date_part('epoch', now()) + $9) / $8)) ELSE NULL END
    )
    ON CONFLICT DO NOTHING
  `;
}

function expire(schema) {
  return `
    WITH expired AS (
      UPDATE ${schema}.job
      SET state = CASE WHEN retryCount < retryLimit THEN 'retry'::${schema}.job_state ELSE 'expired'::${schema}.job_state END,
        completedOn = CASE WHEN retryCount < retryLimit THEN NULL ELSE now() END
      WHERE state = 'active'
        AND (startedOn + expireIn) < now()
      RETURNING id, name, state, data
    )
    SELECT id, name, data FROM expired WHERE state = 'expired';
  `;
}

function archive(schema) {
  return `
    DELETE FROM ${schema}.job
    WHERE (completedOn + CAST($1 as interval) < now())
      OR (state = 'created' and name like '%${expireJobSuffix}' and createdOn + CAST($1 as interval) < now())
  `;
}
<commit_msg>added state count query<commit_after>const expireJobSuffix = '__expired';

module.exports = {
  create,
  insertVersion,
  getVersion,
  versionTableExists,
  fetchNextJob,
  completeJob,
  cancelJob,
  failJob,
  insertJob,
  expire,
  archive,
  countStates,
  expireJobSuffix
};

function create(schema) {
  return [
    createSchema(schema),
    createVersionTable(schema),
    createJobStateEnum(schema),
    createJobTable(schema),
    createIndexSingletonOn(schema),
    createIndexSingletonKeyOn(schema),
    createIndexSingletonKey(schema)
  ];
}

function createSchema(schema) {
  return `
    CREATE SCHEMA IF NOT EXISTS ${schema}
  `;
}

function createVersionTable(schema) {
  return `
    CREATE TABLE IF NOT EXISTS ${schema}.version (
      version text primary key
    )
  `;
}

function createJobStateEnum(schema) {
  // ENUM definition order is important
  // base type is numeric and first values are less than last values
  return `
    CREATE TYPE ${schema}.job_state AS ENUM (
      'created',
      'retry',
      'active',
      'complete',
      'expired',
      'cancelled',
      'failed'
    )
  `;
}

function createJobTable(schema) {
  return `
    CREATE TABLE IF NOT EXISTS ${schema}.job (
      id uuid primary key not null,
      name text not null,
      data jsonb,
      state ${schema}.job_state not null,
      retryLimit integer not null default(0),
      retryCount integer not null default(0),
      startIn interval,
      startedOn timestamp with time zone,
      singletonKey text,
      singletonOn timestamp without time zone,
      expireIn interval,
      createdOn timestamp with time zone not null default now(),
      completedOn timestamp with time zone
    )
  `;
}

function createIndexSingletonKey(schema){
  // anything with singletonKey means "only 1 job can be queued or active at a time"
  return `
    CREATE UNIQUE INDEX job_singletonKey ON ${schema}.job (name, singletonKey) WHERE state < 'complete' AND singletonOn IS NULL
  `;
}

function createIndexSingletonOn(schema){
  // anything with singletonOn means "only 1 job within this time period, queued, active or completed"
  return `
    CREATE UNIQUE INDEX job_singletonOn ON ${schema}.job (name, singletonOn) WHERE state < 'expired' AND singletonKey IS NULL
  `;
}

function createIndexSingletonKeyOn(schema){
  // anything with both singletonOn and singletonKey means "only 1 job within this time period with this key, queued, active or completed"
  return `
    CREATE UNIQUE INDEX job_singletonKeyOn ON ${schema}.job (name, singletonOn, singletonKey) WHERE state < 'expired'
  `;
}

function getVersion(schema) {
  return `
    SELECT version from ${schema}.version
  `;
}

function versionTableExists(schema) {
  return `
    SELECT to_regclass('${schema}.version') as name
  `;
}

function insertVersion(schema) {
  return `
    INSERT INTO ${schema}.version(version) VALUES ($1)
  `;
}

function fetchNextJob(schema) {
  return `
    WITH nextJob as (
      SELECT id
      FROM ${schema}.job
      WHERE state < 'active'
        AND name = $1
        AND (createdOn + startIn) < now()
      ORDER BY createdOn, id
      LIMIT 1
      FOR UPDATE SKIP LOCKED
    )
    UPDATE ${schema}.job SET
      state = 'active',
      startedOn = now(),
      retryCount = CASE WHEN state = 'retry' THEN retryCount + 1 ELSE retryCount END
    FROM nextJob
    WHERE ${schema}.job.id = nextJob.id
    RETURNING ${schema}.job.id, ${schema}.job.data
  `;
}

function completeJob(schema){
  return `
    UPDATE ${schema}.job
    SET completedOn = now(),
      state = 'complete'
    WHERE id = $1
      AND state = 'active'
    `;
}

function cancelJob(schema){
  return `
    UPDATE ${schema}.job
    SET completedOn = now(),
      state = 'cancelled'
    WHERE id = $1
      AND state < 'complete'
  `;
}

function failJob(schema){
  return `
    UPDATE ${schema}.job
    SET completedOn = now(),
      state = 'failed'
    WHERE id = $1
      AND state < 'complete'
  `;
}

function insertJob(schema) {
  return `
    INSERT INTO ${schema}.job (id, name, state, retryLimit, startIn, expireIn, data, singletonKey, singletonOn)
    VALUES (
      $1, $2, 'created', $3, CAST($4 as interval), CAST($5 as interval), $6, $7,
      CASE WHEN $8::integer IS NOT NULL THEN 'epoch'::timestamp + '1 second'::interval * ($8 * floor((date_part('epoch', now()) + $9) / $8)) ELSE NULL END
    )
    ON CONFLICT DO NOTHING
  `;
}

function expire(schema) {
  return `
    WITH expired AS (
      UPDATE ${schema}.job
      SET state = CASE WHEN retryCount < retryLimit THEN 'retry'::${schema}.job_state ELSE 'expired'::${schema}.job_state END,
        completedOn = CASE WHEN retryCount < retryLimit THEN NULL ELSE now() END
      WHERE state = 'active'
        AND (startedOn + expireIn) < now()
      RETURNING id, name, state, data
    )
    SELECT id, name, data FROM expired WHERE state = 'expired';
  `;
}

function archive(schema) {
  return `
    DELETE FROM ${schema}.job
    WHERE (completedOn + CAST($1 as interval) < now())
      OR (state = 'created' and name like '%${expireJobSuffix}' and createdOn + CAST($1 as interval) < now())
  `;
}

function countStates(schema){
  return `
    SELECT
      COUNT(*) FILTER (where state = 'created') as created,
      COUNT(*) FILTER (where state = 'retry') as retry,
      COUNT(*) FILTER (where state = 'active') as active,
      COUNT(*) FILTER (where state = 'complete') as complete,
      COUNT(*) FILTER (where state = 'expired') as expired,
      COUNT(*) FILTER (where state = 'cancelled') as cancelled,
      COUNT(*) FILTER (where state = 'failed') as failed
    FROM ${schema}.job
  `;
}
<|endoftext|>


<commit_before>angular.module('rt.popup', [])
    .factory('Popup', function ($window, $document, $timeout, $compile, $parse) {
        var openedPopup = null;
        var mouseDownInsidePopup = null;
        var template = '

';

        // Padding towards edges of screen.
        var padding = 10;

        function loseFocus(e) {
            if ( (mouseDownInsidePopup === null || mouseDownInsidePopup === false) && openedPopup && !$.contains(openedPopup.el[0], e.target)) {
                mouseDownInsidePopup = null;
                hidePopup();
            }
        }

        function checkMouseDown(e) {
            if (openedPopup) {
                mouseDownInsidePopup = $.contains(openedPopup.el[0], e.target);
            }
        }

        function hidePopup() {
            if (!openedPopup) {
                return;
            }

            var popup = openedPopup;
            openedPopup = null;

            $timeout(function () {
                $parse(popup.options.popupHidden)(popup.scope);

                popup.el.hide().remove();
                $document.off('click', loseFocus);
                $document.off('mousedown', checkMouseDown);
            });
        }

        function extend(obj, values) {
            for (var key in values) {
                if (!obj[key]) {
                    obj[key] = values[key];
                }
            }
        }

        function showPopup(anchor, scope, attrs) {
            extend(attrs, {
                popupPlacement: 'right',
                popupClass: '',
                popupShown: '',
                popupHidden: '',
                popupOverlap: 5 // Overlap with anchor element
            });

            scope.popupView = attrs.popupShow;
            scope.hidePopover = hidePopup;

            $timeout(function () {
                makePopup(anchor, scope, attrs);
            });
        }

        function offset(el) {
            var rect = el[0].getBoundingClientRect();
            return {
                width: rect.width || el.prop('offsetWidth'),
                height: rect.height || el.prop('offsetHeight'),
                top: rect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
                left: rect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
            };
        }

        function fixPosition(scope, anchor, element, arrow, options) {
            var popupPosition = null;
            var arrowPosition = null;
            var anchorPoint = null;
            var anchorGeom = offset(anchor);

            var placement = options.popupPlacement;

            var maxHeight = $window.innerHeight - 2 * padding;

            var overlap = options.popupOverlap;

            // Calculate popup position
            if (placement === 'right') {
                anchorPoint = {
                    top: anchorGeom.top + anchorGeom.height / 2,
                    left: anchorGeom.left + anchorGeom.width - overlap
                };

                popupPosition = {
                    top: anchorPoint.top - element.height() / 2,
                    left: anchorPoint.left
                };

                // Clamp for edge of screen
                popupPosition.top = Math.max(padding, popupPosition.top);

                arrowPosition = {
                    top: anchorPoint.top - popupPosition.top
                };
            } else if (placement === 'left') {
                anchorPoint = {
                    top: anchorGeom.top + anchorGeom.height / 2,
                    left: anchorGeom.left + overlap - 2
                };

                popupPosition = {
                    top: anchorPoint.top - element.height() / 2,
                    left: anchorPoint.left - element.width()
                };

                // Clamp for edge of screen
                popupPosition.top = Math.max(padding, popupPosition.top);

                arrowPosition = {
                    top: anchorPoint.top - popupPosition.top
                };
            } else if (placement === 'bottom') {
                anchorPoint = {
                    top: anchorGeom.top + anchorGeom.height,
                    left: anchorGeom.left + anchorGeom.width / 2
                };

                popupPosition = {
                    top: anchorPoint.top - overlap,
                    left: anchorPoint.left - element.width() / 2
                };

                // Clamp for edge of screen
                popupPosition.left = Math.max(padding, popupPosition.left);
                maxHeight -= popupPosition.top;

                arrowPosition = {
                    left: anchorPoint.left - popupPosition.left
                };
            } else if (placement === 'top') {
                anchorPoint = {
                    top: anchorGeom.top - element.outerHeight(),
                    left: anchorGeom.left + anchorGeom.width / 2
                };

                popupPosition = {
                    top: anchorPoint.top + overlap,
                    left: anchorPoint.left - element.width() / 2
                };

                // Clamp for edge of screen
                popupPosition.left = Math.max(padding, popupPosition.left);
                maxHeight -= popupPosition.top;

                arrowPosition = {
                    left: anchorPoint.left - popupPosition.left
                };
            } else {
                throw new Error('Unsupported placement ' + placement);
            }

            element.removeClass('left right bottom top');
            element.addClass(placement);
            element.css({
                top: popupPosition.top + 'px',
                left: popupPosition.left + 'px',
                display: 'block',
                maxHeight: maxHeight
            });

            var header = element.find('.popover-title');
            var content = element.find('.popover-content');
            var footer = element.find('.popover-footer');
            content.css({
                // Need to figure out where this 4 comes from.
                maxHeight: maxHeight - footer.outerHeight() - header.outerHeight() - 4,
                overflow: 'auto'
            });

            if (arrowPosition) {
                arrow.css(arrowPosition);
            }

            element.removeClass('hide');

            $document.on('mousedown', checkMouseDown);
            $document.on('click', loseFocus);

            $parse(options.popupShown)(scope);
        }

        function makePopup(anchor, scope, options) {
            var element = $compile(template)(scope);
            openedPopup = {
                el: element,
                options: options,
                scope: scope
            };

            var body = $document.find('body');
            body.append(element);

            // Add arrow
            var arrow = $('
', { 'class': 'arrow' });
            element.children('.arrow').remove();
            element.append(arrow);

            scope.$reposition = function () {
                // add extra class here, to ensure that the width calculation takes this into account
                var extra_class = options.popupClass;
                if (extra_class) {
                    element.addClass(extra_class);
                }

                $timeout(function () {
                    fixPosition(scope, anchor, element, arrow, options);
                });
            };
        }

        return {
            show: showPopup,
            close: hidePopup
        };
    })

    .directive('popupShow', function (Popup, $parse, $timeout) {
        return {
            restrict: 'A',
            scope: true,
            link: function (scope, element, attrs) {
                element.click(function () {
                    $timeout(function () {
                        Popup.close();

                        var shouldShow = $parse(attrs.popupIf || 'true');
                        if (shouldShow(scope)) {
                            Popup.show(element, scope, attrs);
                        }
                    });
                });
            }
        };
    })

    .directive('popupAutoShow', function (Popup, $parse) {
        return {
            restrict: 'A',
            link: function (scope, element, attrs) {
                scope.$watch(attrs.popupAutoShow, function (val) {
                    if (val) {
                        Popup.close();
                        var shouldShow = $parse(attrs.popupIf || 'true');
                        if (shouldShow(scope)) {
                            Popup.show(element, scope, attrs);
                        }
                    }
                });
            }
        };
    });
<commit_msg>Added fix to ensure that left/right placement do not overlap bottom edge of screen.<commit_after>angular.module('rt.popup', [])
    .factory('Popup', function ($window, $document, $timeout, $compile, $parse) {
        var openedPopup = null;
        var mouseDownInsidePopup = null;
        var template = '

';

        // Padding towards edges of screen.
        var padding = 10;

        function loseFocus(e) {
            if ( (mouseDownInsidePopup === null || mouseDownInsidePopup === false) && openedPopup && !$.contains(openedPopup.el[0], e.target)) {
                mouseDownInsidePopup = null;
                hidePopup();
            }
        }

        function checkMouseDown(e) {
            if (openedPopup) {
                mouseDownInsidePopup = $.contains(openedPopup.el[0], e.target);
            }
        }

        function hidePopup() {
            if (!openedPopup) {
                return;
            }

            var popup = openedPopup;
            openedPopup = null;

            $timeout(function () {
                $parse(popup.options.popupHidden)(popup.scope);

                popup.el.hide().remove();
                $document.off('click', loseFocus);
                $document.off('mousedown', checkMouseDown);
            });
        }

        function extend(obj, values) {
            for (var key in values) {
                if (!obj[key]) {
                    obj[key] = values[key];
                }
            }
        }

        function showPopup(anchor, scope, attrs) {
            extend(attrs, {
                popupPlacement: 'right',
                popupClass: '',
                popupShown: '',
                popupHidden: '',
                popupOverlap: 5 // Overlap with anchor element
            });

            scope.popupView = attrs.popupShow;
            scope.hidePopover = hidePopup;

            $timeout(function () {
                makePopup(anchor, scope, attrs);
            });
        }

        function offset(el) {
            var rect = el[0].getBoundingClientRect();
            return {
                width: rect.width || el.prop('offsetWidth'),
                height: rect.height || el.prop('offsetHeight'),
                top: rect.top + ($window.pageYOffset || $document[0].documentElement.scrollTop),
                left: rect.left + ($window.pageXOffset || $document[0].documentElement.scrollLeft)
            };
        }

        function fixPosition(scope, anchor, element, arrow, options) {
            var popupPosition = null;
            var arrowPosition = null;
            var anchorPoint = null;
            var anchorGeom = offset(anchor);

            var placement = options.popupPlacement;

            var maxHeight = $window.innerHeight - 2 * padding;

            var overlap = options.popupOverlap;

            // Calculate popup position
            if (placement === 'right') {
                anchorPoint = {
                    top: anchorGeom.top + anchorGeom.height / 2,
                    left: anchorGeom.left + anchorGeom.width - overlap
                };

                popupPosition = {
                    top: anchorPoint.top - element.height() / 2,
                    left: anchorPoint.left
                };

                // Clamp for edge of screen
                popupPosition.top = Math.max(padding, popupPosition.top);
                maxHeight -= popupPosition.top;

                arrowPosition = {
                    top: anchorPoint.top - popupPosition.top
                };
            } else if (placement === 'left') {
                anchorPoint = {
                    top: anchorGeom.top + anchorGeom.height / 2,
                    left: anchorGeom.left + overlap - 2
                };

                popupPosition = {
                    top: anchorPoint.top - element.height() / 2,
                    left: anchorPoint.left - element.width()
                };

                // Clamp for edge of screen
                popupPosition.top = Math.max(padding, popupPosition.top);
                maxHeight -= popupPosition.top;

                arrowPosition = {
                    top: anchorPoint.top - popupPosition.top
                };
            } else if (placement === 'bottom') {
                anchorPoint = {
                    top: anchorGeom.top + anchorGeom.height,
                    left: anchorGeom.left + anchorGeom.width / 2
                };

                popupPosition = {
                    top: anchorPoint.top - overlap,
                    left: anchorPoint.left - element.width() / 2
                };

                // Clamp for edge of screen
                popupPosition.left = Math.max(padding, popupPosition.left);
                maxHeight -= popupPosition.top;

                arrowPosition = {
                    left: anchorPoint.left - popupPosition.left
                };
            } else if (placement === 'top') {
                anchorPoint = {
                    top: anchorGeom.top - element.outerHeight(),
                    left: anchorGeom.left + anchorGeom.width / 2
                };

                popupPosition = {
                    top: anchorPoint.top + overlap,
                    left: anchorPoint.left - element.width() / 2
                };

                // Clamp for edge of screen
                popupPosition.left = Math.max(padding, popupPosition.left);
                maxHeight -= popupPosition.top;

                arrowPosition = {
                    left: anchorPoint.left - popupPosition.left
                };
            } else {
                throw new Error('Unsupported placement ' + placement);
            }

            element.removeClass('left right bottom top');
            element.addClass(placement);
            element.css({
                top: popupPosition.top + 'px',
                left: popupPosition.left + 'px',
                display: 'block',
                maxHeight: maxHeight
            });

            var header = element.find('.popover-title');
            var content = element.find('.popover-content');
            var footer = element.find('.popover-footer');
            content.css({
                // Need to figure out where this 4 comes from.
                maxHeight: maxHeight - footer.outerHeight() - header.outerHeight() - 4,
                overflow: 'auto'
            });

            if (arrowPosition) {
                arrow.css(arrowPosition);
            }

            element.removeClass('hide');

            $document.on('mousedown', checkMouseDown);
            $document.on('click', loseFocus);

            $parse(options.popupShown)(scope);
        }

        function makePopup(anchor, scope, options) {
            var element = $compile(template)(scope);
            openedPopup = {
                el: element,
                options: options,
                scope: scope
            };

            var body = $document.find('body');
            body.append(element);

            // Add arrow
            var arrow = $('
', { 'class': 'arrow' });
            element.children('.arrow').remove();
            element.append(arrow);

            scope.$reposition = function () {
                // add extra class here, to ensure that the width calculation takes this into account
                var extra_class = options.popupClass;
                if (extra_class) {
                    element.addClass(extra_class);
                }

                $timeout(function () {
                    fixPosition(scope, anchor, element, arrow, options);
                });
            };
        }

        return {
            show: showPopup,
            close: hidePopup
        };
    })

    .directive('popupShow', function (Popup, $parse, $timeout) {
        return {
            restrict: 'A',
            scope: true,
            link: function (scope, element, attrs) {
                element.click(function () {
                    $timeout(function () {
                        Popup.close();

                        var shouldShow = $parse(attrs.popupIf || 'true');
                        if (shouldShow(scope)) {
                            Popup.show(element, scope, attrs);
                        }
                    });
                });
            }
        };
    })

    .directive('popupAutoShow', function (Popup, $parse) {
        return {
            restrict: 'A',
            link: function (scope, element, attrs) {
                scope.$watch(attrs.popupAutoShow, function (val) {
                    if (val) {
                        Popup.close();
                        var shouldShow = $parse(attrs.popupIf || 'true');
                        if (shouldShow(scope)) {
                            Popup.show(element, scope, attrs);
                        }
                    }
                });
            }
        };
    });
<|endoftext|>


<commit_before>// vi: sw=2 sts=2 et

const Lang = imports.lang;

const Gtk = imports.gi.Gtk;
const GObject = imports.gi.GObject;
const Signals = imports.signals;

const Gettext = imports.gettext.domain('bitcoin-markets');
const _ = Gettext.gettext;
const N_ = function(e) { return e; };

const Local = imports.misc.extensionUtils.getCurrentExtension();
const Convenience = Local.imports.convenience;
const ApiProvider = Local.imports.ApiProvider;
const ExchangeData = Local.imports.ExchangeData.ExchangeData;

const IndicatorCollectionModel =
  Local.imports.IndicatorCollectionModel.IndicatorCollectionModel;

const makeConfigRow = function (description, widget) {
  let box = new Gtk.Box({
    orientation: Gtk.Orientation.HORIZONTAL,
    margin_bottom: 5,
    hexpand: true,
    vexpand: false
  });

  let label = new Gtk.Label({
    label: description,
    xalign: 0,
    expand: true
  });

  box.add(label);
  box.add(widget);

  return box;
};

const ComboBoxView = new Lang.Class({
  Name: "ComboBoxView",

  Columns: { LABEL: 0, VALUE: 1 },

  _init: function (options) {

    let model = new Gtk.ListStore();
    model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]);

    let comboBox = new Gtk.ComboBox({model: model});
    let renderer = new Gtk.CellRendererText();

    comboBox.pack_start(renderer, true);
    comboBox.add_attribute(renderer, 'text', 0);

    comboBox.connect('changed', function (entry) {
      let [success, iter] = comboBox.get_active_iter();

      if (success) {
        this.emit('changed', model.get_value(iter, this.Columns.VALUE));
      }
    }.bind(this));

    this.widget = comboBox;
    this.model = model;

    this.setOptions(options);
  },

  setOptions: function (options) {
    for each (let o in options) {
      let iter;

      this.model.set(
        iter = this.model.append(),
        [this.Columns.LABEL, this.Columns.VALUE],
        [o.label, o.value]
      );

      if (o.active) {
        this.widget.set_active_iter(iter);
      }
    }
  }
});

Signals.addSignalMethods(ComboBoxView.prototype);

const makeComboBoxCurrency = function (currencies, selected) {
  let options = currencies.map(
    function (c) ({label: c, value: c, active: (c === selected)})
  )

  return new ComboBoxView(options);
}

const ProviderConfigView = new Lang.Class({
  Name: "ProviderConfigView",

  _init: function (configWidget, indicatorConfig) {
    this._configWidget = configWidget;
    this._indicatorConfig = indicatorConfig;
    this._widgets = [];
    this._setDefaults(indicatorConfig);
  },

  _addRow: function (label, widget) {
    let rowWidget = makeConfigRow(label, widget);
    this._configWidget.add(rowWidget);
    this._widgets.push(rowWidget);

    return rowWidget;
  },

  _addSelectCurrency: function (currencies) {
    let comboBoxCurrency = makeComboBoxCurrency(
      currencies, this._indicatorConfig.get('currency')
    );

    comboBoxCurrency.connect('changed', function(view, value) {
      this._indicatorConfig.set('currency', value);
    }.bind(this));

    let rowWidget = this._addRow(_("Currency"), comboBoxCurrency.widget);

    return {
      rowWidget: rowWidget,
      comboBoxView: comboBoxCurrency
    };
  },

  _setDefaults: function (config) {},

  destroy: function () {
    for each (let widget in this._widgets) {
      this._configWidget.remove(widget);
    }

    this._configWidget.show_all();
  }
});

const BitStampConfigView = new Lang.Class({
  Name: "BitStampConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);
    this._addSelectCurrency((new ApiProvider.BitstampApi()).currencies);
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'bitstamp') {
      config.attributes = {
        api: 'bitstamp',
        currency: 'USD',
        attribute: 'last'
      }

      config.emit('update');
    }
  },
});

const MtGoxConfigView = new Lang.Class({
  Name: "MtGoxConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);
    this._addSelectCurrency((new ApiProvider.MtGoxApi()).currencies);
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'mtgox') {
      config.attributes = {
        api: 'mtgox',
        currency: 'USD',
        attribute: 'last_local'
      }

      config.emit('update');
    }
  },
});

const BitcoinAverageConfigView = new Lang.Class({
  Name: "BitcoinAverageConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);

    let api = new ApiProvider.BitcoinAverageApi();

    let currencySelect = this._addSelectCurrency(api.currencies);
    let exchangeSelect = this._addSelectExchange();

    let updateExchangeSelect = function (currency) {
      exchangeSelect.comboBoxView.setOptions(
        this._makeExchangeOptions(currency)
      );
    }.bind(this);

    currencySelect.comboBoxView.connect(
      'changed', function (view, currency) updateExchangeSelect(currency)
    );

    updateExchangeSelect(this._indicatorConfig.get('currency'));
  },

  _addSelectExchange: function () {
    let comboBoxExchange = new ComboBoxView();

    comboBoxExchange.connect("changed", function (view, value) {
      this._indicatorConfig.set('exchange', value);
    }.bind(this));

    let rowWidget = this._addRow(_("Exchange"), comboBoxExchange.widget);

    return {
      rowWidget: rowWidget,
      comboBoxView: comboBoxExchange
    }
  },

  _makeExchangeOptions: function (currency) {
    let currentExchange = this._indicatorConfig.get('exchange') || 'average';
    let exchanges = ApiProvider.getCurrencyToExchange()[currency];

    let options = [
      {label: 'Average', value: 'average', active: (currentExchange === 'average')}
    ];

    exchanges.forEach(function (e) {
      options.push({label: e, value: e, active: e === currentExchange});
    });

    return options;
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'bitcoinaverage') {
      config.attributes = {
        api: 'bitcoinaverage',
        exchange: 'average',
        currency: 'USD',
        attribute: 'last'
      };

      config.emit('update');
    }
  },
});

const BitcoinChartsConfigView = new Lang.Class({
  Name: "BitcoinChartsConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);
    let api = new ApiProvider.BitcoinCharts();
    this._addSelectCurrency((new ApiProvider.BitcoinChartsApi()).currencies);
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'bitstamp') {
      config.attributes = {
        api: 'bitcoincharts',
        exchange: 'mtgox',
        currency: 'USD',
        attribute: 'last'
      }

      config.emit('update');
    }
  }
});

const IndicatorConfigView = new Lang.Class({
  Name: "BitcoinMarkets.IndicatorConfigView",

  _init: function (indicatorConfig) {
    this._indicatorConfig = indicatorConfig;

    this.widget = new Gtk.Box({
      orientation: Gtk.Orientation.VERTICAL,
    });

    let options = [
        {label: 'BitcoinAverage', value: 'bitcoinaverage'},
        {label: 'MtGox',    value: 'mtgox'},
        {label: 'BitStamp', value: 'bitstamp'}
    ];

    for each (let o in options) {
      if (o.value === indicatorConfig.get('api')) {
        o.active = true;
      }
    }

    this._selectApiView = new ComboBoxView(options);

    this._selectApiView.connect("changed", function (view, api) {
      this._selectApi(api);
    }.bind(this));

    this.widget.add(
      makeConfigRow(_("Provider"), this._selectApiView.widget)
    );

    this._selectApi(indicatorConfig.get('api'));

    this.widget.show_all();
  },

  _selectApi: function (api) {
    let apiConfigViews = {
      mtgox: function () {
        return new MtGoxConfigView(this.widget, this._indicatorConfig);
      }.bind(this),

      bitstamp: function () {
        return new BitStampConfigView(this.widget, this._indicatorConfig);
      }.bind(this),

      bitcoinaverage: function () {
        return new BitcoinAverageConfigView(
          this.widget, this._indicatorConfig
        );
      }.bind(this)
    }

    if (this._apiConfigView) {
      this._apiConfigView.destroy();
      this._apiConfigView = null;
    }

    if (api in apiConfigViews) {
      this._apiConfigView = apiConfigViews[api]();
    } else {
      throw new Error("no config view for " + api);
    }

    this.widget.show_all();
  },

  destroy: function () {
    this.disconnectAll();
    this.widget.destroy();
  }
});

Signals.addSignalMethods(IndicatorConfigView.prototype);

const BitcoinMarketsSettingsWidget = new GObject.Class({
  Name: "BitcoinMarkets.BitcoinMarketsSettingsWidget",
  GTypeName: "BitcoinMarketsSettingsWidget",
  Extends: Gtk.Box,

  _init: function () {
    this.parent({
      orientation: Gtk.Orientation.HORIZONTAL
    });

    this._apiProvider = new ApiProvider.ApiProvider();
    this._store = new IndicatorCollectionModel();

    /* sidebar (left) */

    let sidebar = new Gtk.Box({
      margin: 10,
      orientation: Gtk.Orientation.VERTICAL,
      width_request: 200
    });

    sidebar.add(this._getTreeView());
    sidebar.add(this._getToolbar());

    this.add(sidebar);

    /* config (right) */

    this._configLayout = new Gtk.Box({
      margin: 10,
      orientation: Gtk.Orientation.HORIZONTAL,
      expand: true,
    });

    this.add(this._configLayout);

    /* behavior */

    this._selection = this._treeView.get_selection();
    this._selection.connect(
      'changed',
      Lang.bind(this, this._onSelectionChanged)
    );
  },

  _getTreeView: function () {
    this._treeView = new Gtk.TreeView({
      model: this._store,
      headers_visible: false,
      reorderable: true,
      hexpand: false,
      vexpand: true
    });

    let label = new Gtk.TreeViewColumn({title: "Label"});
    let renderer = new Gtk.CellRendererText()
    label.pack_start(renderer, true);
    label.add_attribute(renderer, "text", 0);
    this._treeView.insert_column(label, 0);

    return this._treeView;
  },

  _getToolbar: function () {
    let toolbar = this._toolbar = new Gtk.Toolbar({
      icon_size: 1
    });

    toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);

    let getAddIndicatorAction = function ({label, value}) {
      let action = new Gtk.Action({
        name: label,
        label: label
      });
    };

    /* new widget button with menu */
    let newButton = new Gtk.ToolButton({icon_name: "list-add-symbolic"});
    newButton.connect('clicked', Lang.bind(this, this._addClicked));
    toolbar.add(newButton);

    /* delete button */
    let delButton = this._delButton =
      new Gtk.ToolButton({icon_name: "list-remove-symbolic"});
    delButton.connect('clicked', Lang.bind(this, this._delClicked));

    toolbar.add(delButton);

    this._updateToolbar();

    return toolbar;
  },

  _onSelectionChanged: function () {
    let [isSelected, model, iter] = this._selection.get_selected();

    if (isSelected) {
      this._showIndicatorConfig(this._store.getConfig(iter));
    } else {
      this._showIndicatorConfig(null);
    }
  },

  _showIndicatorConfig: function (indicatorConfig) {
    if (this._indicatorConfigView) {
      this._configLayout.remove(this._indicatorConfigView.widget);
      this._indicatorConfigView.destroy();
      this._indicatorConfigView = null;
    }

    if (indicatorConfig === null) {
      return;
    }

    this._indicatorConfigView = new IndicatorConfigView(indicatorConfig);
    this._configLayout.add(this._indicatorConfigView.widget);
  },

  _updateToolbar: function () {
    this._delButton.set_sensitive(this._store.size() > 0);
  },

  _addClicked: function () {
    this._store.append();
    this._updateToolbar();
  },

  _delClicked: function () {
    let [isSelected, model, iter] = this._selection.get_selected();

    if (isSelected) {
      this._store.remove(iter);
    }

    this._updateToolbar();
  }
});

function init() {
  Convenience.initTranslations();
}

function buildPrefsWidget() {
  let widget = new BitcoinMarketsSettingsWidget();

  widget.show_all();

  return widget;
}
<commit_msg>ux: exchange selector sensitivity<commit_after>// vi: sw=2 sts=2 et

const Lang = imports.lang;

const Gtk = imports.gi.Gtk;
const GObject = imports.gi.GObject;
const Signals = imports.signals;

const Gettext = imports.gettext.domain('bitcoin-markets');
const _ = Gettext.gettext;
const N_ = function(e) { return e; };

const Local = imports.misc.extensionUtils.getCurrentExtension();
const Convenience = Local.imports.convenience;
const ApiProvider = Local.imports.ApiProvider;
const ExchangeData = Local.imports.ExchangeData.ExchangeData;

const IndicatorCollectionModel =
  Local.imports.IndicatorCollectionModel.IndicatorCollectionModel;

const makeConfigRow = function (description, widget) {
  let box = new Gtk.Box({
    orientation: Gtk.Orientation.HORIZONTAL,
    margin_bottom: 5,
    hexpand: true,
    vexpand: false
  });

  let label = new Gtk.Label({
    label: description,
    xalign: 0,
    expand: true
  });

  box.add(label);
  box.add(widget);

  return box;
};

const ComboBoxView = new Lang.Class({
  Name: "ComboBoxView",

  Columns: { LABEL: 0, VALUE: 1 },

  _init: function (options) {

    let model = new Gtk.ListStore();
    model.set_column_types([GObject.TYPE_STRING, GObject.TYPE_STRING]);

    let comboBox = new Gtk.ComboBox({model: model});
    let renderer = new Gtk.CellRendererText();

    comboBox.pack_start(renderer, true);
    comboBox.add_attribute(renderer, 'text', 0);

    comboBox.connect('changed', function (entry) {
      let [success, iter] = comboBox.get_active_iter();

      if (success) {
        this.emit('changed', model.get_value(iter, this.Columns.VALUE));
      }
    }.bind(this));

    this.widget = comboBox;
    this.model = model;

    this.setOptions(options);
  },

  setOptions: function (options) {
    this.model.clear();

    for each (let o in options) {
      let iter;

      this.model.set(
        iter = this.model.append(),
        [this.Columns.LABEL, this.Columns.VALUE],
        [o.label, o.value]
      );

      if (o.active) {
        this.widget.set_active_iter(iter);
      }
    }
  }
});

Signals.addSignalMethods(ComboBoxView.prototype);

const makeComboBoxCurrency = function (currencies, selected) {
  let options = currencies.map(
    function (c) ({label: c, value: c, active: (c === selected)})
  )

  return new ComboBoxView(options);
}

const ProviderConfigView = new Lang.Class({
  Name: "ProviderConfigView",

  _init: function (configWidget, indicatorConfig) {
    this._configWidget = configWidget;
    this._indicatorConfig = indicatorConfig;
    this._widgets = [];
    this._setDefaults(indicatorConfig);
  },

  _addRow: function (label, widget) {
    let rowWidget = makeConfigRow(label, widget);
    this._configWidget.add(rowWidget);
    this._widgets.push(rowWidget);

    return rowWidget;
  },

  _addSelectCurrency: function (currencies) {
    let comboBoxCurrency = makeComboBoxCurrency(
      currencies, this._indicatorConfig.get('currency')
    );

    comboBoxCurrency.connect('changed', function(view, value) {
      this._indicatorConfig.set('currency', value);
    }.bind(this));

    let rowWidget = this._addRow(_("Currency"), comboBoxCurrency.widget);

    return {
      rowWidget: rowWidget,
      comboBoxView: comboBoxCurrency
    };
  },

  _setDefaults: function (config) {},

  destroy: function () {
    for each (let widget in this._widgets) {
      this._configWidget.remove(widget);
    }

    this._configWidget.show_all();
  }
});

const BitStampConfigView = new Lang.Class({
  Name: "BitStampConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);
    this._addSelectCurrency((new ApiProvider.BitstampApi()).currencies);
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'bitstamp') {
      config.attributes = {
        api: 'bitstamp',
        currency: 'USD',
        attribute: 'last'
      }

      config.emit('update');
    }
  },
});

const MtGoxConfigView = new Lang.Class({
  Name: "MtGoxConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);
    this._addSelectCurrency((new ApiProvider.MtGoxApi()).currencies);
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'mtgox') {
      config.attributes = {
        api: 'mtgox',
        currency: 'USD',
        attribute: 'last_local'
      }

      config.emit('update');
    }
  },
});

const BitcoinAverageConfigView = new Lang.Class({
  Name: "BitcoinAverageConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);

    let api = new ApiProvider.BitcoinAverageApi();

    let currencySelect = this._addSelectCurrency(api.currencies);
    let averageSwitch = this._addAverageSwitch();
    let exchangeSelect = this._addSelectExchange();

    let updateExchangeSelect = function () {
      let useAverage = this._indicatorConfig.get('use_average');
      let currency = this._indicatorConfig.get('currency');
      exchangeSelect.rowWidget.sensitive = useAverage === false;
      exchangeSelect.comboBoxView.setOptions(
        this._makeExchangeOptions(currency)
      );
    }.bind(this);

    currencySelect.comboBoxView.connect('changed', updateExchangeSelect);

    // TODO use proper view method: connect("changed")
    averageSwitch.switchView.connect('notify::active', function (obj) {
      this._indicatorConfig.set('use_average', obj.active);
      updateExchangeSelect();
    }.bind(this));

    updateExchangeSelect();
  },

  _addAverageSwitch: function () {
    let switchView = new Gtk.Switch({
      active: this._indicatorConfig.get('use_average')
    });

    let rowWidget = this._addRow(_("Average"), switchView);

    return {
      rowWidget: rowWidget,
      switchView: switchView
    };
  },

  _addSelectExchange: function () {
    let comboBoxExchange = new ComboBoxView();

    comboBoxExchange.connect("changed", function (view, value) {
      this._indicatorConfig.set('exchange', value);
    }.bind(this));

    let rowWidget = this._addRow(_("Exchange"), comboBoxExchange.widget);

    return {
      rowWidget: rowWidget,
      comboBoxView: comboBoxExchange
    }
  },

  _makeExchangeOptions: function (currency) {
    let currentExchange = this._indicatorConfig.get('exchange');
    let exchanges = ApiProvider.getCurrencyToExchange()[currency];

    let options = exchanges.map(function (e) {
      return {label: e, value: e, active: e === currentExchange}
    });

    return options;
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'bitcoinaverage') {
      config.attributes = {
        api: 'bitcoinaverage',
        exchange: 'average',
        currency: 'USD',
        attribute: 'last'
      };

      config.emit('update');
    }
  },
});

const BitcoinChartsConfigView = new Lang.Class({
  Name: "BitcoinChartsConfigView",
  Extends: ProviderConfigView,

  _init: function (configWidget, indicatorConfig) {
    this.parent(configWidget, indicatorConfig);
    let api = new ApiProvider.BitcoinCharts();
    this._addSelectCurrency((new ApiProvider.BitcoinChartsApi()).currencies);
  },

  _setDefaults: function (config) {
    if (config.get('api') !== 'bitstamp') {
      config.attributes = {
        api: 'bitcoincharts',
        exchange: 'mtgox',
        currency: 'USD',
        attribute: 'last'
      }

      config.emit('update');
    }
  }
});

const IndicatorConfigView = new Lang.Class({
  Name: "BitcoinMarkets.IndicatorConfigView",

  _init: function (indicatorConfig) {
    this._indicatorConfig = indicatorConfig;

    this.widget = new Gtk.Box({
      orientation: Gtk.Orientation.VERTICAL,
    });

    let options = [
        {label: 'BitcoinAverage', value: 'bitcoinaverage'},
        {label: 'MtGox',    value: 'mtgox'},
        {label: 'BitStamp', value: 'bitstamp'}
    ];

    for each (let o in options) {
      if (o.value === indicatorConfig.get('api')) {
        o.active = true;
      }
    }

    this._selectApiView = new ComboBoxView(options);

    this._selectApiView.connect("changed", function (view, api) {
      this._selectApi(api);
    }.bind(this));

    this.widget.add(
      makeConfigRow(_("Provider"), this._selectApiView.widget)
    );

    this._selectApi(indicatorConfig.get('api'));

    this.widget.show_all();
  },

  _selectApi: function (api) {
    let apiConfigViews = {
      mtgox: function () {
        return new MtGoxConfigView(this.widget, this._indicatorConfig);
      }.bind(this),

      bitstamp: function () {
        return new BitStampConfigView(this.widget, this._indicatorConfig);
      }.bind(this),

      bitcoinaverage: function () {
        return new BitcoinAverageConfigView(
          this.widget, this._indicatorConfig
        );
      }.bind(this)
    }

    if (this._apiConfigView) {
      this._apiConfigView.destroy();
      this._apiConfigView = null;
    }

    if (api in apiConfigViews) {
      this._apiConfigView = apiConfigViews[api]();
    } else {
      throw new Error("no config view for " + api);
    }

    this.widget.show_all();
  },

  destroy: function () {
    this.disconnectAll();
    this.widget.destroy();
  }
});

Signals.addSignalMethods(IndicatorConfigView.prototype);

const BitcoinMarketsSettingsWidget = new GObject.Class({
  Name: "BitcoinMarkets.BitcoinMarketsSettingsWidget",
  GTypeName: "BitcoinMarketsSettingsWidget",
  Extends: Gtk.Box,

  _init: function () {
    this.parent({
      orientation: Gtk.Orientation.HORIZONTAL
    });

    this._apiProvider = new ApiProvider.ApiProvider();
    this._store = new IndicatorCollectionModel();

    /* sidebar (left) */

    let sidebar = new Gtk.Box({
      margin: 10,
      orientation: Gtk.Orientation.VERTICAL,
      width_request: 200
    });

    sidebar.add(this._getTreeView());
    sidebar.add(this._getToolbar());

    this.add(sidebar);

    /* config (right) */

    this._configLayout = new Gtk.Box({
      margin: 10,
      orientation: Gtk.Orientation.HORIZONTAL,
      expand: true,
    });

    this.add(this._configLayout);

    /* behavior */

    this._selection = this._treeView.get_selection();
    this._selection.connect(
      'changed',
      Lang.bind(this, this._onSelectionChanged)
    );
  },

  _getTreeView: function () {
    this._treeView = new Gtk.TreeView({
      model: this._store,
      headers_visible: false,
      reorderable: true,
      hexpand: false,
      vexpand: true
    });

    let label = new Gtk.TreeViewColumn({title: "Label"});
    let renderer = new Gtk.CellRendererText()
    label.pack_start(renderer, true);
    label.add_attribute(renderer, "text", 0);
    this._treeView.insert_column(label, 0);

    return this._treeView;
  },

  _getToolbar: function () {
    let toolbar = this._toolbar = new Gtk.Toolbar({
      icon_size: 1
    });

    toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_INLINE_TOOLBAR);

    let getAddIndicatorAction = function ({label, value}) {
      let action = new Gtk.Action({
        name: label,
        label: label
      });
    };

    /* new widget button with menu */
    let newButton = new Gtk.ToolButton({icon_name: "list-add-symbolic"});
    newButton.connect('clicked', Lang.bind(this, this._addClicked));
    toolbar.add(newButton);

    /* delete button */
    let delButton = this._delButton =
      new Gtk.ToolButton({icon_name: "list-remove-symbolic"});
    delButton.connect('clicked', Lang.bind(this, this._delClicked));

    toolbar.add(delButton);

    this._updateToolbar();

    return toolbar;
  },

  _onSelectionChanged: function () {
    let [isSelected, model, iter] = this._selection.get_selected();

    if (isSelected) {
      this._showIndicatorConfig(this._store.getConfig(iter));
    } else {
      this._showIndicatorConfig(null);
    }
  },

  _showIndicatorConfig: function (indicatorConfig) {
    if (this._indicatorConfigView) {
      this._configLayout.remove(this._indicatorConfigView.widget);
      this._indicatorConfigView.destroy();
      this._indicatorConfigView = null;
    }

    if (indicatorConfig === null) {
      return;
    }

    this._indicatorConfigView = new IndicatorConfigView(indicatorConfig);
    this._configLayout.add(this._indicatorConfigView.widget);
  },

  _updateToolbar: function () {
    this._delButton.set_sensitive(this._store.size() > 0);
  },

  _addClicked: function () {
    this._store.append();
    this._updateToolbar();
  },

  _delClicked: function () {
    let [isSelected, model, iter] = this._selection.get_selected();

    if (isSelected) {
      this._store.remove(iter);
    }

    this._updateToolbar();
  }
});

function init() {
  Convenience.initTranslations();
}

function buildPrefsWidget() {
  let widget = new BitcoinMarketsSettingsWidget();

  widget.show_all();

  return widget;
}
<|endoftext|>