<commit_before>import { createCellDragBehaviour } from './cell-drag-behaviour'
import { dispatch as createDispatch } from 'd3-dispatch'
import { modulo, someResult as some } from '@zambezi/fun'
import { rebind, keyCodeHandler, appendIfMissing } from '@zambezi/d3-utils'
import { reduce, indexBy, find, findIndex, debounce, uniqueId } from 'underscore'
import { select, event } from 'd3-selection'
import { unwrap } from '@zambezi/grid'

import './cell-selection.css'

export function createCellSelection () {
  const dispatch = createDispatch(
    'cell-selected-change',
    'cell-selected-update',
    'cell-active-action',
    'cell-active-change',
    'cell-active-update',
    'cell-active-paste'
  )

  const clipboardKeydown = uniqueId('keydown.clipboard.')
  const clipboardKeyup = uniqueId('keyup.clipboard.')
  const cellDragBehaviour = createCellDragBehaviour()
  const api = rebind()
            .from(dispatch, 'on')
            .from(cellDragBehaviour, 'ignoreSelector')
            .from(cellDragBehaviour, 'debugIgnoreSelector')
  const defaultSelectionKey = d => d
  const clipboardProxy = appendIfMissing('textarea.zambezi-grid-clipboard-proxy')

  let selected = []
  let selectedCandidates
  let activeCandidate
  let selectedRowsByColumnId = {}
  let acceptPasteFrom = []
  let active
  let selectable = true
  let trackPaste = true
  let typeToActivate = true
  let lastOverCell
  let rowSelectionKey = defaultSelectionKey
  let rowUpdateNeeded = true
  let clip
  let isIEPasting

  function cellSelection (s) {
    s.each(cellSelectionEach)
  }

  cellSelection.selected = function (value) {
    if (!arguments.length) return selected
    selected = value
    selectedCandidates = value
    return cellSelection
  }

  cellSelection.trackPaste = function (value) {
    if (!arguments.length) return trackPaste
    trackPaste = value
    return cellSelection
  }

  cellSelection.acceptPasteFrom = function (value) {
    if (!arguments.length) return acceptPasteFrom
    acceptPasteFrom = value
    return cellSelection
  }

  cellSelection.typeToActivate = function (value) {
    if (!arguments.length) return typeToActivate
    typeToActivate = value
    return cellSelection
  }

  cellSelection.selectable = function (value) {
    if (!arguments.length) return selectable
    selectable = value
    return cellSelection
  }

  cellSelection.active = function (value) {
    if (!arguments.length) return active
    active = value
    activeCandidate = value
    return cellSelection
  }

  cellSelection.rowChangedKey = function (targetRow) {
    const unwrappedRow = unwrap(targetRow)
    const selectedColumnIds = []

    let key = ''

    selected.forEach(addSelected)

    function addSelected ({row, column}) {
      if (unwrappedRow !== row) return
      selectedColumnIds.push(column.id)
    }

    key = selectedColumnIds.join('↑')

    if (active && rowSelectionKey(unwrappedRow) === rowSelectionKey(active.row)) {
      key += ('☆' + active.column.id)
    }
    return key
  }

  cellSelection.rowSelectionKey = function (value) {
    if (!arguments.length) return rowSelectionKey
    rowSelectionKey = value
    return cellSelection
  }

  return api(cellSelection)

  function cellSelectionEach (bundle) {
    const target = select(this)
            .on('data-dirty.cell-selection', () => (rowUpdateNeeded = true))
    const { columns } = bundle
    const columnById = indexBy(columns, 'id')
    const newRowByOldRow = new Map()

    setupDragEvents()
    setupPasteEvents()
    setupKeyboardNavEvents()

    if (rowUpdateNeeded) updateRowsFromKeys()
    if (selectedCandidates) updateSelectedFromCandidates()
    if (activeCandidate) updateActiveFromCandidate()

    bundle.dispatcher
        .on('cell-update.cell-selection', onCellUpdate)

    function moveHorizontal (step) {
      if (!active) return

      const { column, row } = active
      const currentColumnIndex = columns.indexOf(column)
      const newColumn = columns[modulo(currentColumnIndex + step, columns.length)]

      setActive({ row, column: newColumn })
      target.dispatch('redraw', { bubbles: true })
    }

    function moveVertical (step, rows) {
      if (!active) return

      const { column, row } = active
      const currentRowIndex = findIndex(rows, r => rowSelectionKey(unwrap(r)) === rowSelectionKey(row))
      const newRow = rows[modulo(currentRowIndex + step, rows.length)]

      setActive({ row: newRow, column })
      target.dispatch('redraw', { bubbles: true })
    }

    function updateRowsFromKeys () {
      rowUpdateNeeded = false
      if (rowSelectionKey === defaultSelectionKey) return

      selectedCandidates = (selected || [])
          .reduce(updateRowFromSelectionKey, [])
          .concat(selectedCandidates || [])

      if (active) activeCandidate = active

      function updateRowFromSelectionKey (acc, {column, row}) {
        const newRow = findNewRowForOld(row)
        if (newRow) acc.push({column, row: newRow})
        return acc
      }
    }

    function findNewRowForOld (row) {
      const rowSeen = newRowByOldRow.has(row)
      let newRow = newRowByOldRow.get(row)
      if (rowSeen && !newRow) return null
      if (!newRow) {
        newRow = find(bundle, r => rowSelectionKey(r) === rowSelectionKey(row))
        newRowByOldRow.set(row, newRow)
      }
      return newRow
    }

    function setActiveIfNone () {
      if (active) return
      if (!bundle.length) return
      if (!columns.length) return
      setActive({ row: bundle[0], column: columns[0] })
      select(this).dispatch('redraw', { bubbles: true })
    }

    function updateActiveFromCandidate () {
      active = candidateToSelection(activeCandidate)
      activeCandidate = null
      dispatch.call('cell-active-update', this, active)
    }

    function updateSelectedFromCandidates () {
      selectedRowsByColumnId = selectedCandidates.reduce(toRealSelection, {})
      selected = compileSelected()
      selectedCandidates = null
      dispatch.call('cell-selected-update', this, selected, active)
    }

    function toRealSelection (acc, candidate) {
      const selection = candidateToSelection(candidate)

      if (!selection) {
        console.warn("Couldn't find cell for", candidate)
        return acc
      }

      const columnId = selection.column.id
      const set = acc[columnId] || new Set()

      set.add(selection.row)
      acc[columnId] = set
      return acc
    }

    function candidateToSelection ({ row, column }) {
      const columnFound = typeof column === 'string' ? columnById[column] : column
      const rowFound = typeof row === 'number' ? bundle[row] : findNewRowForOld(row)

      if (!columnFound || !rowFound) return undefined
      return { row: rowFound, column: columnFound }
    }

    function compileSelected () {
      return reduce(selectedRowsByColumnId, toCells, [])
    }

    function setupKeyboardNavEvents () {
      target.attr('tabindex', '0')
          .on('focus', debounce(setActiveIfNone, 200))
          .on(
            'keydown.keyboard-cell-selection'
          , some(
              keyCodeHandler(() => dispatch.call('cell-active-action', this, active), 13) // enter
            , keyCodeHandler(d => moveVertical(-1, d.rows), 38) // up
            , keyCodeHandler(d => moveVertical(1, d.rows), 40)  // down
            , keyCodeHandler(() => moveHorizontal(-1), 37)      // left
            , keyCodeHandler(() => moveHorizontal(1), 39)       // right
            , keyCodeHandler(onTab, 9)
            , activateFromInput
            )
          )
    }

    function setupPasteEvents () {
      select(document)
          .on(clipboardKeydown, trackPaste ? onClipMaybe : null)
          .on(clipboardKeyup, trackPaste ? onClipAfter : null)
    }

    function setupDragEvents () {
      target.call(
        cellDragBehaviour
            .on('dragstart.select', d => mouseSelection(d))
            .on('dragstart.active', setActive)
            .on('dragstart.redraw', () => target.dispatch('redraw', { bubbles: true }))

            .on('dragend.set-active', activateLastCell)
            .on('dragend.redraw', () => target.dispatch('redraw', { bubbles: true }))

            .on('dragover.select', d => mouseSelection(d, true))
            .on('dragover.cache', d => (lastOverCell = d))
            .on('dragover.redraw', () => target.dispatch('redraw', { bubbles: true }))
      )
    }

    function activateLastCell () {
      if (!lastOverCell) return
      setActive(lastOverCell)
      lastOverCell = null
    }

    function onClipMaybe () {
      const targetNode = target.node()
      const allAcceptedNodes = (acceptPasteFrom || []).concat(targetNode)
      const { ctrlKey } = event

      if (!allAcceptedNodes.some(n => n.contains(document.activeElement))) return
      if (!ctrlKey) return

      clip = select(document.body)
            .select(clipboardProxy)
              .on('paste.cell-selection', onPaste)

      clip.node().focus()
      clip.node().select()

      function onPaste () {
        const clipboardData = event.clipboardData

        if (!clipboardData) {
          isIEPasting = true
          return
        }

        const text = clipboardData.getData('Text')
        dispatch.call('cell-active-paste', targetNode, active, text)
      }
    }

    function onClipAfter () {
      const targetNode = target.node()
      const { key } = event

      if (key !== 'Control') return

      if (isIEPasting) {
        dispatch.call('cell-active-paste', targetNode, active, clip.property('value'))
        isIEPasting = false
      }

      if (clip) {
        clip.remove()
        clip = null
        targetNode.focus()
      }
    }

    function activateFromInput () {
      const { key, ctrlKey, altKey, metaKey } = event

      if (!typeToActivate) return
      if (ctrlKey || altKey || metaKey) return
      if (!active) return
      if (isASpecialKey(key)) return

      dispatch.call('cell-active-action', target.node(), active, key)
    }

    function onTab () {
      moveHorizontal(event.shiftKey ? -1 : 1)
      event.preventDefault()
    }

    function mouseSelection (d, forceAppend) {
      const column = d.column
      const columnId = column.id
      const set = selectedRowsByColumnId[columnId]
      const row = unwrap(d.row)
      const { shiftKey, ctrlKey } = event.sourceEvent ? event.sourceEvent : event
      const cell = { row, column }
      const isAlreadySelected = set && set.has(row)
      const hasActive = !!active

      if (selectable) {
        switch (true) {
          case ctrlKey && shiftKey && hasActive:
            addRangeToSelection(active, cell)
            break

          case (forceAppend || shiftKey) && hasActive:
            setSelectionToRange(active, cell)
            break

          case ctrlKey && isAlreadySelected:
            removeFromSelected(cell)
            break

          case ctrlKey:
            addToSelected(cell)
            break

          default:
            selectOnly(cell)
        }

        selected = compileSelected()
        dispatch.call('cell-selected-change', this, selected, active)
      }
    }

    function setActive (cell) {
      if (!cell) {
        active = null
        return
      }

      const { row, column } = cell
      active = { row: unwrap(row), column }
      dispatch.call('cell-active-change', this, active)
    }

    function setSelectionToRange (a, b) {
      selectedRowsByColumnId = {}
      rangeFrom(a, b).forEach(addToSelected)
    }

    function addRangeToSelection (a, b) {
      rangeFrom(a, b).forEach(addToSelected)
    }

    function rangeFrom (a, b) {
      const columnRange = rangeFromUnorderedIndices(
              columns,
              columns.indexOf(a.column),
              columns.indexOf(b.column)
            )
      const rowRange = rangeFromUnorderedIndices(
              bundle.rows,
              findIndex(bundle.rows, r => unwrap(r) === a.row),
              findIndex(bundle.rows, r => unwrap(r) === b.row)
            ).map(unwrap)

      return rowRange.reduce(allCellsInColumns, [])

      function allCellsInColumns (acc, row) {
        return acc.concat(columnRange.map(column => ({ column, row })))
      }
    }

    function selectOnly (target) {
      selectedRowsByColumnId = {}
      addToSelected(target)
    }

    function rangeFromUnorderedIndices (array, i1, i2) {
      return array.slice(
        Math.min(i1, i2)
      , Math.max(i1, i2) + 1
      )
    }

    function removeFromSelected ({ row, column }) {
      const columnId = column.id
      const set = selectedRowsByColumnId[columnId]

      if (set) set.delete(row)
    }

    function addToSelected ({ row, column }) {
      const columnId = column.id
      const set = selectedRowsByColumnId[columnId] || new Set()

      set.add(row)
      selectedRowsByColumnId[columnId] = set
    }

    function toCells (acc, set, columnId) {
      const column = columnById[columnId]
      return acc.concat(Array.from(set.values()).map(row => ({ row, column })))
    }
  }

  function onCellUpdate () {
    select(this)
        .classed('is-selected', isCellSelected)
        .classed('is-active', isCellActive)
  }

  function isCellSelected (d) {
    const rowSetForColumn = selectedRowsByColumnId[d.column.id]
    return rowSetForColumn && rowSetForColumn.has(unwrap(d.row))
  }

  function isCellActive (d) {
    if (!active) return false
    return areSameCell(d, active)
  }
}

function isASpecialKey (key) {
  return key && key.length > 1
}

function areSameCell (a, b) {
  return a && b && unwrap(a.row) === unwrap(b.row) && a.column === b.column
}
<commit_msg>Compare cells using rowSelectionKey<commit_after>import { createCellDragBehaviour } from './cell-drag-behaviour'
import { dispatch as createDispatch } from 'd3-dispatch'
import { modulo, someResult as some } from '@zambezi/fun'
import { rebind, keyCodeHandler, appendIfMissing } from '@zambezi/d3-utils'
import { reduce, indexBy, find, findIndex, debounce, uniqueId } from 'underscore'
import { select, event } from 'd3-selection'
import { unwrap } from '@zambezi/grid'

import './cell-selection.css'

export function createCellSelection () {
  const dispatch = createDispatch(
    'cell-selected-change',
    'cell-selected-update',
    'cell-active-action',
    'cell-active-change',
    'cell-active-update',
    'cell-active-paste'
  )

  const clipboardKeydown = uniqueId('keydown.clipboard.')
  const clipboardKeyup = uniqueId('keyup.clipboard.')
  const cellDragBehaviour = createCellDragBehaviour()
  const api = rebind()
            .from(dispatch, 'on')
            .from(cellDragBehaviour, 'ignoreSelector')
            .from(cellDragBehaviour, 'debugIgnoreSelector')
  const defaultSelectionKey = d => d
  const clipboardProxy = appendIfMissing('textarea.zambezi-grid-clipboard-proxy')

  let selected = []
  let selectedCandidates
  let activeCandidate
  let selectedRowsByColumnId = {}
  let acceptPasteFrom = []
  let active
  let selectable = true
  let trackPaste = true
  let typeToActivate = true
  let lastOverCell
  let rowSelectionKey = defaultSelectionKey
  let rowUpdateNeeded = true
  let clip
  let isIEPasting

  function cellSelection (s) {
    s.each(cellSelectionEach)
  }

  cellSelection.selected = function (value) {
    if (!arguments.length) return selected
    selected = value
    selectedCandidates = value
    return cellSelection
  }

  cellSelection.trackPaste = function (value) {
    if (!arguments.length) return trackPaste
    trackPaste = value
    return cellSelection
  }

  cellSelection.acceptPasteFrom = function (value) {
    if (!arguments.length) return acceptPasteFrom
    acceptPasteFrom = value
    return cellSelection
  }

  cellSelection.typeToActivate = function (value) {
    if (!arguments.length) return typeToActivate
    typeToActivate = value
    return cellSelection
  }

  cellSelection.selectable = function (value) {
    if (!arguments.length) return selectable
    selectable = value
    return cellSelection
  }

  cellSelection.active = function (value) {
    if (!arguments.length) return active
    active = value
    activeCandidate = value
    return cellSelection
  }

  cellSelection.rowChangedKey = function (targetRow) {
    const unwrappedRow = unwrap(targetRow)
    const selectedColumnIds = []

    let key = ''

    selected.forEach(addSelected)

    function addSelected ({row, column}) {
      if (unwrappedRow !== row) return
      selectedColumnIds.push(column.id)
    }

    key = selectedColumnIds.join('↑')

    if (active && rowSelectionKey(unwrappedRow) === rowSelectionKey(active.row)) {
      key += ('☆' + active.column.id)
    }
    return key
  }

  cellSelection.rowSelectionKey = function (value) {
    if (!arguments.length) return rowSelectionKey
    rowSelectionKey = value
    return cellSelection
  }

  return api(cellSelection)

  function cellSelectionEach (bundle) {
    const target = select(this)
            .on('data-dirty.cell-selection', () => (rowUpdateNeeded = true))
    const { columns } = bundle
    const columnById = indexBy(columns, 'id')
    const newRowByOldRow = new Map()

    setupDragEvents()
    setupPasteEvents()
    setupKeyboardNavEvents()

    if (rowUpdateNeeded) updateRowsFromKeys()
    if (selectedCandidates) updateSelectedFromCandidates()
    if (activeCandidate) updateActiveFromCandidate()

    bundle.dispatcher
        .on('cell-update.cell-selection', onCellUpdate)

    function moveHorizontal (step) {
      if (!active) return

      const { column, row } = active
      const currentColumnIndex = columns.indexOf(column)
      const newColumn = columns[modulo(currentColumnIndex + step, columns.length)]

      setActive({ row, column: newColumn })
      target.dispatch('redraw', { bubbles: true })
    }

    function moveVertical (step, rows) {
      if (!active) return

      const { column, row } = active
      const currentRowIndex = findIndex(rows, r => rowSelectionKey(unwrap(r)) === rowSelectionKey(row))
      const newRow = rows[modulo(currentRowIndex + step, rows.length)]

      setActive({ row: newRow, column })
      target.dispatch('redraw', { bubbles: true })
    }

    function updateRowsFromKeys () {
      rowUpdateNeeded = false
      if (rowSelectionKey === defaultSelectionKey) return

      selectedCandidates = (selected || [])
          .reduce(updateRowFromSelectionKey, [])
          .concat(selectedCandidates || [])

      if (active) activeCandidate = active

      function updateRowFromSelectionKey (acc, {column, row}) {
        const newRow = findNewRowForOld(row)
        if (newRow) acc.push({column, row: newRow})
        return acc
      }
    }

    function findNewRowForOld (row) {
      const rowSeen = newRowByOldRow.has(row)
      let newRow = newRowByOldRow.get(row)
      if (rowSeen && !newRow) return null
      if (!newRow) {
        newRow = find(bundle, r => rowSelectionKey(r) === rowSelectionKey(row))
        newRowByOldRow.set(row, newRow)
      }
      return newRow
    }

    function setActiveIfNone () {
      if (active) return
      if (!bundle.length) return
      if (!columns.length) return
      setActive({ row: bundle[0], column: columns[0] })
      select(this).dispatch('redraw', { bubbles: true })
    }

    function updateActiveFromCandidate () {
      active = candidateToSelection(activeCandidate)
      activeCandidate = null
      dispatch.call('cell-active-update', this, active)
    }

    function updateSelectedFromCandidates () {
      selectedRowsByColumnId = selectedCandidates.reduce(toRealSelection, {})
      selected = compileSelected()
      selectedCandidates = null
      dispatch.call('cell-selected-update', this, selected, active)
    }

    function toRealSelection (acc, candidate) {
      const selection = candidateToSelection(candidate)

      if (!selection) {
        console.warn("Couldn't find cell for", candidate)
        return acc
      }

      const columnId = selection.column.id
      const set = acc[columnId] || new Set()

      set.add(selection.row)
      acc[columnId] = set
      return acc
    }

    function candidateToSelection ({ row, column }) {
      const columnFound = typeof column === 'string' ? columnById[column] : column
      const rowFound = typeof row === 'number' ? bundle[row] : findNewRowForOld(row)

      if (!columnFound || !rowFound) return undefined
      return { row: rowFound, column: columnFound }
    }

    function compileSelected () {
      return reduce(selectedRowsByColumnId, toCells, [])
    }

    function setupKeyboardNavEvents () {
      target.attr('tabindex', '0')
          .on('focus', debounce(setActiveIfNone, 200))
          .on(
            'keydown.keyboard-cell-selection'
          , some(
              keyCodeHandler(() => dispatch.call('cell-active-action', this, active), 13) // enter
            , keyCodeHandler(d => moveVertical(-1, d.rows), 38) // up
            , keyCodeHandler(d => moveVertical(1, d.rows), 40)  // down
            , keyCodeHandler(() => moveHorizontal(-1), 37)      // left
            , keyCodeHandler(() => moveHorizontal(1), 39)       // right
            , keyCodeHandler(onTab, 9)
            , activateFromInput
            )
          )
    }

    function setupPasteEvents () {
      select(document)
          .on(clipboardKeydown, trackPaste ? onClipMaybe : null)
          .on(clipboardKeyup, trackPaste ? onClipAfter : null)
    }

    function setupDragEvents () {
      target.call(
        cellDragBehaviour
            .on('dragstart.select', d => mouseSelection(d))
            .on('dragstart.active', setActive)
            .on('dragstart.redraw', () => target.dispatch('redraw', { bubbles: true }))

            .on('dragend.set-active', activateLastCell)
            .on('dragend.redraw', () => target.dispatch('redraw', { bubbles: true }))

            .on('dragover.select', d => mouseSelection(d, true))
            .on('dragover.cache', d => (lastOverCell = d))
            .on('dragover.redraw', () => target.dispatch('redraw', { bubbles: true }))
      )
    }

    function activateLastCell () {
      if (!lastOverCell) return
      setActive(lastOverCell)
      lastOverCell = null
    }

    function onClipMaybe () {
      const targetNode = target.node()
      const allAcceptedNodes = (acceptPasteFrom || []).concat(targetNode)
      const { ctrlKey } = event

      if (!allAcceptedNodes.some(n => n.contains(document.activeElement))) return
      if (!ctrlKey) return

      clip = select(document.body)
            .select(clipboardProxy)
              .on('paste.cell-selection', onPaste)

      clip.node().focus()
      clip.node().select()

      function onPaste () {
        const clipboardData = event.clipboardData

        if (!clipboardData) {
          isIEPasting = true
          return
        }

        const text = clipboardData.getData('Text')
        dispatch.call('cell-active-paste', targetNode, active, text)
      }
    }

    function onClipAfter () {
      const targetNode = target.node()
      const { key } = event

      if (key !== 'Control') return

      if (isIEPasting) {
        dispatch.call('cell-active-paste', targetNode, active, clip.property('value'))
        isIEPasting = false
      }

      if (clip) {
        clip.remove()
        clip = null
        targetNode.focus()
      }
    }

    function activateFromInput () {
      const { key, ctrlKey, altKey, metaKey } = event

      if (!typeToActivate) return
      if (ctrlKey || altKey || metaKey) return
      if (!active) return
      if (isASpecialKey(key)) return

      dispatch.call('cell-active-action', target.node(), active, key)
    }

    function onTab () {
      moveHorizontal(event.shiftKey ? -1 : 1)
      event.preventDefault()
    }

    function mouseSelection (d, forceAppend) {
      const column = d.column
      const columnId = column.id
      const set = selectedRowsByColumnId[columnId]
      const row = unwrap(d.row)
      const { shiftKey, ctrlKey } = event.sourceEvent ? event.sourceEvent : event
      const cell = { row, column }
      const isAlreadySelected = set && set.has(row)
      const hasActive = !!active

      if (selectable) {
        switch (true) {
          case ctrlKey && shiftKey && hasActive:
            addRangeToSelection(active, cell)
            break

          case (forceAppend || shiftKey) && hasActive:
            setSelectionToRange(active, cell)
            break

          case ctrlKey && isAlreadySelected:
            removeFromSelected(cell)
            break

          case ctrlKey:
            addToSelected(cell)
            break

          default:
            selectOnly(cell)
        }

        selected = compileSelected()
        dispatch.call('cell-selected-change', this, selected, active)
      }
    }

    function setActive (cell) {
      if (!cell) {
        active = null
        return
      }

      const { row, column } = cell
      active = { row: unwrap(row), column }
      dispatch.call('cell-active-change', this, active)
    }

    function setSelectionToRange (a, b) {
      selectedRowsByColumnId = {}
      rangeFrom(a, b).forEach(addToSelected)
    }

    function addRangeToSelection (a, b) {
      rangeFrom(a, b).forEach(addToSelected)
    }

    function rangeFrom (a, b) {
      const columnRange = rangeFromUnorderedIndices(
              columns,
              columns.indexOf(a.column),
              columns.indexOf(b.column)
            )
      const rowRange = rangeFromUnorderedIndices(
              bundle.rows,
              findIndex(bundle.rows, r => unwrap(r) === a.row),
              findIndex(bundle.rows, r => unwrap(r) === b.row)
            ).map(unwrap)

      return rowRange.reduce(allCellsInColumns, [])

      function allCellsInColumns (acc, row) {
        return acc.concat(columnRange.map(column => ({ column, row })))
      }
    }

    function selectOnly (target) {
      selectedRowsByColumnId = {}
      addToSelected(target)
    }

    function rangeFromUnorderedIndices (array, i1, i2) {
      return array.slice(
        Math.min(i1, i2)
      , Math.max(i1, i2) + 1
      )
    }

    function removeFromSelected ({ row, column }) {
      const columnId = column.id
      const set = selectedRowsByColumnId[columnId]

      if (set) set.delete(row)
    }

    function addToSelected ({ row, column }) {
      const columnId = column.id
      const set = selectedRowsByColumnId[columnId] || new Set()

      set.add(row)
      selectedRowsByColumnId[columnId] = set
    }

    function toCells (acc, set, columnId) {
      const column = columnById[columnId]
      return acc.concat(Array.from(set.values()).map(row => ({ row, column })))
    }
  }

  function onCellUpdate () {
    select(this)
        .classed('is-selected', isCellSelected)
        .classed('is-active', isCellActive)
  }

  function isCellSelected (d) {
    const rowSetForColumn = selectedRowsByColumnId[d.column.id]
    return rowSetForColumn && rowSetForColumn.has(unwrap(d.row))
  }

  function isCellActive (d) {
    if (!active) return false
    return areSameCell(d, active)
  }

  function areSameCell (a, b) {
    return a && b && rowSelectionKey(unwrap(a.row)) === rowSelectionKey(unwrap(b.row)) && a.column.id === b.column.id
  }
}

function isASpecialKey (key) {
  return key && key.length > 1
}
<|endoftext|>


<commit_before>/*
 * freepano - WebGL panorama viewer
 *
 * Copyright (c) 2014 FOXEL SA - http://foxel.ch
 * Please read <http://foxel.ch/license> for more information.
 *
 *
 * Author(s):
 *
 *      Luc Deschenaux <l.deschenaux@foxel.ch>
 *
 *
 * Contributor(s):
 *
 *      Alexandre Kraft <a.kraft@foxel.ch>
 *      Kevin Velickovic <k.velickovic@foxel.ch>
 *
 *
 * This file is part of the FOXEL project <http://foxel.ch>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Additional Terms:
 *
 *      You are required to preserve legal notices and author attributions in
 *      that material or in the Appropriate Legal Notices displayed by works
 *      containing it.
 *
 *      You are required to attribute the work as explained in the "Usage and
 *      Attribution" section of <http://foxel.ch/license>.
 */

$(document).ready(function(){

  var filesToLoad=1;

  function file_onload() {
    --filesToLoad;
    if (!filesToLoad) {
      $(document).trigger('filesloaded');
    }
  }

  // load image with alpha channel to use as POI
  window.unicorn_texture=new THREE.ImageUtils.loadTexture(
    'unicorn.png',
    new THREE.UVMapping(),
    file_onload,
    function onloaderror() {
      $.notify('Cannot load unicorn.png !');
    }
  );

}); // ready

$(document).on('filesloaded', function(){

  $('#pano').panorama({

/*
 * Properties and methods defined here will extend or override properties
 * and methods of the panorama object instance before the init() method is run
 *
 * When using panorama.list below to define several panoramas,
 * properties and methods defined there will extend or override properties
 * and methods of the panorama object instance.
 *
 */

/*
    // panorama.fov: camera field of view

    fov: {

      // initial field of view
      start: 120,

      // minimal field of view
      min: 1,

      // maximal field of view
      // fov > 120 results in non-rectilinear projection
      max: 120

    }, // fov

*/
/*
    // panorama.rotation: initial panorama sphere rotation

    rotation: {

      // vertical axis rotation
      heading: -90,

      // horizontal axis rotation
      // adjust in the viewer using <shift>-mousewheel
      tilt: 0,

      // depth axis rotation
      // adjust in the viewer using <alt>-mousewheel
      roll: 0,

      // rotation step for tilt and roll adjustment
      step: 0.1

    }, // rotation

*/
/*
    // panorama.limits: limits

    limits: {

      // panorama vertical rotation limits
      lat: {
        min: -85,
        max: 85
      }

    }

*/
/*
    // panorama.camera: main camera options

    camera: {

      zoom: {

        // initial zoom value
        current: 1.0,

        // maximal zoom value
        max: 1.5

      }

    }, // camera

    // panorama.sphere: sphere object defaults
    // normally you dont need to modify this

    sphere: {

      radius: 15,

      widthSegments: 36,

      heightSegments: 18,

      // panorama.sphere.texture: sphere texture options
      // When using 'panorama.list' to configure several panoramas,
      // 'panorama.list.defaults' will extend or override values below

      texture: {

        // tiles directory relative url
        dirName: 'panoramas/result_1403179805_224762-0-25-1/',

        // tile filename prefix
        baseName: 'result_1403179805_224762-0-25-1',

        // full panorama dimension, in tiles
        columns: 16,
        rows: 8

      } // texture

    }, // sphere
/*
   // panorama.sound: sounds bound to panorama
   // When using 'panorama.list' below to configure several panoramas,
   // 'panorama.list.images[image].sound' properties and methods will
   // extend or modify values below

    sound: {

      // defaults for sounds defined in 'panorama.sound.list' below,
      // where more sound options are described

      defaults: {

        // sound type (only Howler is supported)
        type: 'howler',

        // event handlers
        onloaderror: function sound_onloaderror(sound_event) {
           console.log('sound_onloaderror: ',this,sound_event);
        },
        onload: null,
        onpause: null,
        onplay: null,
        onend: null

      },

      list: {

        // sound ID
        ambient1: {

          // Howler options
          src: ["ambient1.mp3"],
          autoplay: true,
          loop: true

        },

        ambient2: {
          src: ["welcome.mp3"]
          autoplay: true,
          loop: false
        }
      }

    },

    // panorama.poi: points of interest
    // When using panorama.list to define several panoramas,
    // poi options below are extended or overrided with the ones
    // from panorama.list.images[id].poi

    poi: {

      // panorama.poi.defaults: default values for POIs
      defaults: {

          // set to false to disable mouse event handling
          handleMousevents: true,

          color: {
             active: '#0000ff',
             hover: '#ffffff',
             normal: '#000000'
          },

          // event handlers below are already filtered
          // eg: mousein and mouseout are not triggered during panorama rotation
          // if you really need, you can hook to the 'private' methods (eg: _mousein)

          onmousein: function poi_mousein(e) {
            console.log('mousein',this);
          },

          onmouseout: function poi_mouseout(e) {
            console.log('mouseout',this);
          },

          onmouseover: function poi_mouseover(e) {
          },

          onmousedown: function poi_mousedown(e) {
            console.log('mousedown',this);
          },

          onmouseup: function poi_mouseup(e) {
            console.log('mouseup',this);
          },

          onclick: function poi_click(e) {
            console.log('click',this);
          },
      },

      // panorama.poi.list
      list: {

        // POI identifier
        unicorn: {

          // to define the POI geometry you can either specify 'object'
          // as THREE.Object3D (default object is null)
          object: null,

          // ... or 'mesh' as THREE.mesh, (default mesh is a circle)
          mesh: new THREE.Mesh(new THREE.PlaneGeometry(Math.PI/18,Math.PI/18,1,1), new THREE.MeshBasicMaterial({
            map: unicorn_texture,
            transparent: true,
          })),

          // for POI defined as a texture with alpha layer like this one
          // setting 'handleTransparency' to true will disable mouse events
          // occuring over fully transparent pixels
          handleTransparency: true,

          // POI coordinates
          coords: {
            lon: -70,
            lat: 0
          }

        }, // unicorn

        // POI identifier
        circle: {

            // POI coords
            coords: {
              lon: -90,
              lat: 0
            },

            // poi.list.sound:
            // sounds bound to a poi
            sound: {

              // for defaults, see comments for panorama.sounds above
              defaults: {
              },

              // poi.list.sound.list
              list: {

                beep: {

                  // sound type specific options (see Howler.js 2.0 documentation)
                  src: ["sound/argo.mp3"],
                  autoplay: true,
                  loop: true,
                  fadeOut: 2000,

                  // If you specify optional WebAudio sound cone parameters,
                  // the sound is always oriented in the direction opposite
                  // to the camera, and the volume fall to coneOuterGain
                  // outside the cone outer angle.
                  coneInnerAngle: 30,
                  coneOuterAngle: 90,
                  coneOuterGain: 0,

                  // when specifying sound cone parameters above, set rolloffFactor
                  // to 0 will disable gain change relative to z position.
                  rolloffFactor: 0

                },
                plop: {
                  src: ["plop.mp3"]
                }
              }
            }
        } // circle
      } // poi.list
    }, // poi

    // work in progress
    hud: {
      list: {
        testarro: {
          color: {
            active: '#0000ff',
            normal: '#ffffff',
            hover: '#000000'
          },
          coords: {
            lon: 0,
            lat: 0
          }
        }
      }
    },

*/
    // when using jquery.freepano.list.js,
    // you can set the preferences for several images below
    // instead of sphere.texture above

    list: {

        // default options for elements of the 'images' object below
        // (will be merged with the sphere.texture properties above)

        defaults: {

          // tiles directory name
          dirName: 'panoramas/result_1403179805_224762-0-25-1',

          // tile filename prefix
          prefix: 'result_',

          // tile filename suffix
          suffix: '-0-25-1',

          // full panorama dimension, in tiles
          columns: 16,
          rows: 8

        },

        // initial image
        // default is the first element of 'images' below
//      initialImage: '1403179809_224762',

        // panorama list
        images: {

          // the panorama instance will be extended
          // 1. with the list.defaults above
          // 2. with the object below

          '1403179805_224762': {
            rotation: {
               tilt: 0
            },
            coords: {
              lon: 3.902137,
              lat: 43.600233,
            },
            sound: {
              list: {
                1: {
                  src: ["ambiance1.mp3"]
                },
                2: {
                  src: ["ambiance2.mp3"]
                }
              }
            },
            poi: {
              defaults: {
                color: {
                  active: '#0000ff',
                  hover: '#ffffff',
                  normal: '#000000'
                }
              },
              list: {
                circle: {
                    coords: {
                      lon: -90,
                      lat: 0
                    },
                    sound: {
                      list: {
                        beep: {
                          src: ["sound/argo.mp3"],
                          autoplay: true,
                          loop: true,
                          fadeOut: 2000,
                          coneInnerAngle: 30,
                          coneOuterAngle: 90,
                          coneOuterGain: 0,
                          rolloffFactor: 0
                        },
                        plop: {
                          src: ["plop.mp3"]
                        }
                      }
                    }
                }, // circle
                square: {
                    mesh: new THREE.Mesh(new THREE.PlaneGeometry(Math.PI/18,Math.PI/18,1,1), new THREE.MeshBasicMaterial({
                      color: 0x000000,
                      transparent: true,
                      opacity: 0.3
                    })),
                    coords: {
                      lon: -70,
                      lat: 0
                    }
                }, // square
                triangle: {
                    mesh: function test2_mesh() {
                      var poi=this;
                      var geometry=new THREE.Geometry();
                      var s=poi.size;
                      geometry.vertices.push(new THREE.Vector3(-s,-s,0));
                      geometry.vertices.push(new THREE.Vector3(s,-s,0));
                      geometry.vertices.push(new THREE.Vector3(0,s,0));
                      geometry.faces.push(new THREE.Face3(0,1,2));
                      return new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
                        color: 0x000000,
                        transparent: true,
                        opacity: 0.3
                      }));
                    },
                    coords: {
                      lon: -80,
                      lat: 0
                    }
                }, // triangle

                unicorn: {

                  color: {
                    active: 'purple',
                    normal: 'cyan',
                    hover: 'white'
                  },

                  mesh: new THREE.Mesh(new THREE.PlaneGeometry(Math.PI/18,Math.PI/18,1,1), new THREE.MeshBasicMaterial({
                    map: unicorn_texture,
                    transparent: true,
                    opacity: 0.3
                  })),

                  handleTransparency: true,

                  coords: {
                    lon: -60,
                    lat: 0
                  }

                } // unicorn
              } // list
            }, // poi

            arrow: {
              list: {
                0: {
                  coords: {
                    lon: -90
                  }
                },
                1: {
                  coords: {
                    lon: 90
                  }
                }
              }
            }

          },

          // second image
          '1403179809_224762': {
            dirName: 'panoramas/result_1403179809_224762-0-25-1',
            coords: {
              lon: 3.901933,
              lat: 43.600545,
            }
          }
        }
    },

    controls: {
        touch: {
            move: {
                active: true
            },
            zoom: {
                active: true
            }
        },
        keyboard: {
            move: {
                active: true
            },
            zoom: {
                active: true
            }
        },
        devicemotion: {
            move: {
                active: false
            }
        }
    },

    map: {
        active: true
    },

    // THREE.js renderer options

    renderer: {

      precision: 'lowp',

      antialias: false,

      alpha: false

    },

/* incompatible with panorama.list below yet
    pyramid: {
      dirName: 'panoramas/result_1403179805_224762-0-25-1/512',
      baseName: 'result_1403179805_224762-0-25-1',
      levels: 4,
      preload: true
      sphere: [
        {
          radius: 1
        },
        {
          radius: 2
        },
        {
          radius: 4
        },
        {
          radius: 8
        },
        {
          radius: 16
        },
      ]
    },
*/

    postProcessing: {
      enabled: false,
      edge: {
        shader: THREE.EdgeShader,
        enabled: false,
        uniforms: {
          aspect: function(panorama) {
            this.value.x=$(panorama.container).width();
            this.value.y=$(panorama.container).height();
          }
        }
      },

      edge2: {
        shader: THREE.EdgeShader2,
        enabled: false,
        uniforms: {
          aspect: function(panorama) {
            this.value.x=$(panorama.container).width();
            this.value.y=$(panorama.container).height();
          }
        }
      }
    }

  }); // panorama

  var panorama=$('#pano').data('pano');

  $(document).on('keydown',function(e){
    switch(e.keyCode) {
    case 32:
      console.log('lon ['+panorama.lon+'] lat ['+panorama.lat+'] tilt ['+panorama.rotation.tilt+'] roll ['+panorama.rotation.roll+']');
      break;
    case 49:
      toggleEffect(panorama.postProcessing.edge);
      break;
    case 50:
      toggleEffect(panorama.postProcessing.edge2);
      break;
    case 77:
      var map = panorama.map;
      if(map) {
          map.active = !map.active;
      }
      break;
    }

    if (panorama.postProcessing) panorama.postProcessing.enabled=panorama.postProcessing.edge.pass.enabled||panorama.postProcessing.edge2.pass.enabled;
  });

  function toggleEffect(effect){
    effect.pass.enabled=!effect.pass.enabled;
    panorama.drawScene();
  }

});
<commit_msg>updated example/main.js<commit_after>/*
 * freepano - WebGL panorama viewer
 *
 * Copyright (c) 2014 FOXEL SA - http://foxel.ch
 * Please read <http://foxel.ch/license> for more information.
 *
 *
 * Author(s):
 *
 *      Luc Deschenaux <l.deschenaux@foxel.ch>
 *
 *
 * Contributor(s):
 *
 *      Alexandre Kraft <a.kraft@foxel.ch>
 *      Kevin Velickovic <k.velickovic@foxel.ch>
 *
 *
 * This file is part of the FOXEL project <http://foxel.ch>.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *
 * Additional Terms:
 *
 *      You are required to preserve legal notices and author attributions in
 *      that material or in the Appropriate Legal Notices displayed by works
 *      containing it.
 *
 *      You are required to attribute the work as explained in the "Usage and
 *      Attribution" section of <http://foxel.ch/license>.
 */

$(document).ready(function(){

  var filesToLoad=1;

  function file_onload() {
    --filesToLoad;
    if (!filesToLoad) {
      $(document).trigger('filesloaded');
    }
  }

  // load image with alpha channel to use as POI
  window.unicorn_texture=new THREE.ImageUtils.loadTexture(
    'img/unicorn.png',
    new THREE.UVMapping(),
    file_onload,
    function onloaderror() {
      $.notify('Cannot load unicorn.png !');
    }
  );

}); // ready

$(document).on('filesloaded', function(){

  $('#pano').panorama({

/*
 * Properties and methods defined here will extend or override properties
 * and methods of the panorama object instance before the init() method is run
 *
 * When using panorama.list below to define several panoramas,
 * properties and methods defined there will extend or override properties
 * and methods of the panorama object instance.
 *
 */

/*
    // panorama.fov: camera field of view

    fov: {

      // initial field of view
      start: 120,

      // minimal field of view
      min: 1,

      // maximal field of view
      // fov > 120 results in non-rectilinear projection
      max: 120

    }, // fov

*/
/*
    // panorama.rotation: initial panorama sphere rotation

    rotation: {

      // vertical axis rotation
      heading: -90,

      // horizontal axis rotation
      // adjust in the viewer using <shift>-mousewheel
      tilt: 0,

      // depth axis rotation
      // adjust in the viewer using <alt>-mousewheel
      roll: 0,

      // rotation step for tilt and roll adjustment
      step: 0.1

    }, // rotation

*/
/*
    // panorama.limits: limits

    limits: {

      // panorama vertical rotation limits
      lat: {
        min: -85,
        max: 85
      }

    }

*/
/*
    // panorama.camera: main camera options

    camera: {

      zoom: {

        // initial zoom value
        current: 1.0,

        // maximal zoom value
        max: 1.5

      }

    }, // camera

    // panorama.sphere: sphere object defaults
    // normally you dont need to modify this

    sphere: {

      radius: 15,

      widthSegments: 36,

      heightSegments: 18,

      // panorama.sphere.texture: sphere texture options
      // When using 'panorama.list' to configure several panoramas,
      // 'panorama.list.defaults' will extend or override values below

      texture: {

        // tiles directory relative url
        dirName: 'panoramas/result_1403179805_224762-0-25-1/',

        // tile filename prefix
        baseName: 'result_1403179805_224762-0-25-1',

        // full panorama dimension, in tiles
        columns: 16,
        rows: 8

      } // texture

    }, // sphere
/*
   // panorama.sound: sounds bound to panorama
   // When using 'panorama.list' below to configure several panoramas,
   // 'panorama.list.images[image].sound' properties and methods will
   // extend or modify values below

    sound: {

      // defaults for sounds defined in 'panorama.sound.list' below,
      // where more sound options are described

      defaults: {

        // sound type (only Howler is supported)
        type: 'howler',

        // event handlers
        onloaderror: function sound_onloaderror(sound_event) {
           console.log('sound_onloaderror: ',this,sound_event);
        },
        onload: null,
        onpause: null,
        onplay: null,
        onend: null

      },

      list: {

        // sound ID
        ambient1: {

          // Howler options
          src: ["ambient1.mp3"],
          autoplay: true,
          loop: true

        },

        ambient2: {
          src: ["welcome.mp3"]
          autoplay: true,
          loop: false
        }
      }

    },

    // panorama.poi: points of interest
    // When using panorama.list to define several panoramas,
    // poi options below are extended or overrided with the ones
    // from panorama.list.images[id].poi

    poi: {

      // use a secondary scene for rendering widgets (eg when using filters)
      overlay: false,

      // panorama.poi.defaults: default values for POIs
      defaults: {

          // set to false to disable mouse event handling
          handleMousevents: true,

          color: {
             active: '#0000ff',
             hover: '#ffffff',
             normal: '#000000'
          },

          // event handlers below are already filtered
          // eg: mousein and mouseout are not triggered during panorama rotation
          // if you really need, you can hook to the 'private' methods (eg: _mousein)

          onmousein: function poi_mousein(e) {
            console.log('mousein',this);
          },

          onmouseout: function poi_mouseout(e) {
            console.log('mouseout',this);
          },

          onmouseover: function poi_mouseover(e) {
          },

          onmousedown: function poi_mousedown(e) {
            console.log('mousedown',this);
          },

          onmouseup: function poi_mouseup(e) {
            console.log('mouseup',this);
          },

          onclick: function poi_click(e) {
            console.log('click',this);
          },
      },

      // panorama.poi.list
      list: {

        // POI identifier
        unicorn: {

          // to define the POI geometry you can either specify 'object'
          // as THREE.Object3D (default object is null)
          object: null,

          // ... or 'mesh' as THREE.mesh, (default mesh is a circle)
          mesh: new THREE.Mesh(new THREE.PlaneGeometry(Math.PI/18,Math.PI/18,1,1), new THREE.MeshBasicMaterial({
            map: unicorn_texture,
            transparent: true,
          })),

          // for POI defined as a texture with alpha layer like this one
          // setting 'handleTransparency' to true will disable mouse events
          // occuring over fully transparent pixels
          handleTransparency: true,

          // POI coordinates
          coords: {
            lon: -70,
            lat: 0
          }

        }, // unicorn

        // POI identifier
        circle: {

            // POI coords
            coords: {
              lon: -90,
              lat: 0
            },

            // poi.list.sound:
            // sounds bound to a poi
            sound: {

              // for defaults, see comments for panorama.sounds above
              defaults: {
              },

              // poi.list.sound.list
              list: {

                beep: {

                  // sound type specific options (see Howler.js 2.0 documentation)
                  src: ["sound/argo.mp3"],
                  autoplay: true,
                  loop: true,
                  fadeOut: 2000,

                  // If you specify optional WebAudio sound cone parameters,
                  // the sound is always oriented in the direction opposite
                  // to the camera, and the volume fall to coneOuterGain
                  // outside the cone outer angle.
                  coneInnerAngle: 30,
                  coneOuterAngle: 90,
                  coneOuterGain: 0,

                  // when specifying sound cone parameters above, set rolloffFactor
                  // to 0 will disable gain change relative to z position.
                  rolloffFactor: 0

                },
                plop: {
                  src: ["plop.mp3"]
                }
              }
            }
        } // circle
      } // poi.list
    }, // poi

    // work in progress
    hud: {
      list: {
        testarro: {
          color: {
            active: '#0000ff',
            normal: '#ffffff',
            hover: '#000000'
          },
          coords: {
            lon: 0,
            lat: 0
          }
        }
      }
    },

*/
    // when using jquery.freepano.list.js,
    // you can set the preferences for several images below
    // instead of sphere.texture above

    list: {

        // default options for elements of the 'images' object below
        // (will be merged with the sphere.texture properties above)

        defaults: {

          // tiles directory name
          dirName: 'panoramas/result_1403179805_224762-0-25-1',

          // tile filename prefix
          prefix: 'result_',

          // tile filename suffix
          suffix: '-0-25-1',

          // full panorama dimension, in tiles
          columns: 16,
          rows: 8

        },

        // initial image
        // default is the first element of 'images' below
//      initialImage: '1403179809_224762',

        // panorama list
        images: {

          // the panorama instance will be extended
          // 1. with the list.defaults above
          // 2. with the object below

          '1403179805_224762': {
            rotation: {
               tilt: 0
            },
            coords: {
              lon: 3.902137,
              lat: 43.600233,
            },
            sound: {
              list: {
                1: {
                  src: ["ambiance1.mp3"]
                },
                2: {
                  src: ["ambiance2.mp3"]
                }
              }
            },
            poi: {
              overlay: true,
              defaults: {
                color: {
                  active: '#0000ff',
                  hover: '#ffffff',
                  normal: '#000000'
                }
              },
              list: {
                circle: {
                    coords: {
                      lon: -90,
                      lat: 0
                    },
                    sound: {
                      list: {
                        beep: {
                          src: ["sound/argo.mp3"],
                          autoplay: true,
                          loop: true,
                          fadeOut: 2000,
                          coneInnerAngle: 30,
                          coneOuterAngle: 90,
                          coneOuterGain: 0,
                          rolloffFactor: 0
                        },
                        plop: {
                          src: ["plop.mp3"]
                        }
                      }
                    }
                }, // circle
                square: {
                    mesh: new THREE.Mesh(new THREE.PlaneGeometry(Math.PI/18,Math.PI/18,1,1), new THREE.MeshBasicMaterial({
                      color: 0x000000,
                      transparent: true,
                      opacity: 0.3
                    })),
                    coords: {
                      lon: -70,
                      lat: 0
                    }
                }, // square
                triangle: {
                    mesh: function test2_mesh() {
                      var poi=this;
                      var geometry=new THREE.Geometry();
                      var s=poi.size;
                      geometry.vertices.push(new THREE.Vector3(-s,-s,0));
                      geometry.vertices.push(new THREE.Vector3(s,-s,0));
                      geometry.vertices.push(new THREE.Vector3(0,s,0));
                      geometry.faces.push(new THREE.Face3(0,1,2));
                      return new THREE.Mesh(geometry, new THREE.MeshBasicMaterial({
                        color: 0x000000,
                        transparent: true,
                        opacity: 0.3
                      }));
                    },
                    coords: {
                      lon: -80,
                      lat: 0
                    }
                }, // triangle

                unicorn: {

                  color: {
                    active: 'purple',
                    normal: 'cyan',
                    hover: 'white'
                  },

                  mesh: new THREE.Mesh(new THREE.PlaneGeometry(Math.PI/18,Math.PI/18,1,1), new THREE.MeshBasicMaterial({
                    map: unicorn_texture,
                    transparent: true,
                    opacity: 0.3
                  })),

                  handleTransparency: true,

                  coords: {
                    lon: -60,
                    lat: 0
                  }

                } // unicorn
              } // list
            }, // poi

            arrow: {
              list: {
                0: {
                  coords: {
                    lon: -90
                  }
                },
                1: {
                  coords: {
                    lon: 90
                  }
                }
              }
            }

          },

          // second image
          '1403179809_224762': {
            dirName: 'panoramas/result_1403179809_224762-0-25-1',
            coords: {
              lon: 3.901933,
              lat: 43.600545,
            }
          }
        }
    },

    controls: {
        touch: {
            move: {
                active: true
            },
            zoom: {
                active: true
            }
        },
        keyboard: {
            move: {
                active: true
            },
            zoom: {
                active: true
            }
        },
        devicemotion: {
            move: {
                active: false
            }
        }
    },

    map: {
        active: true
    },

    // THREE.js renderer options

    renderer: {

      precision: 'lowp',

      antialias: false,

      alpha: false

    },

/* incompatible with panorama.list below yet
    pyramid: {
      dirName: 'panoramas/result_1403179805_224762-0-25-1/512',
      baseName: 'result_1403179805_224762-0-25-1',
      levels: 4,
      preload: true
      sphere: [
        {
          radius: 1
        },
        {
          radius: 2
        },
        {
          radius: 4
        },
        {
          radius: 8
        },
        {
          radius: 16
        },
      ]
    },
*/

    postProcessing: {
      enabled: false,
      edge: {
        shader: THREE.EdgeShader,
        enabled: false,
        uniforms: {
          aspect: function(panorama) {
            this.value.x=$(panorama.container).width();
            this.value.y=$(panorama.container).height();
          }
        }
      },

      edge2: {
        shader: THREE.EdgeShader2,
        enabled: false,
        uniforms: {
          aspect: function(panorama) {
            this.value.x=$(panorama.container).width();
            this.value.y=$(panorama.container).height();
          }
        }
      }
    }

  }); // panorama

  var panorama=$('#pano').data('pano');

  $(document).on('keydown',function(e){
    switch(e.keyCode) {
    case 32:
      console.log('lon ['+panorama.lon+'] lat ['+panorama.lat+'] tilt ['+panorama.rotation.tilt+'] roll ['+panorama.rotation.roll+']');
      break;
    case 49:
      toggleEffect(panorama.postProcessing.edge);
      break;
    case 50:
      toggleEffect(panorama.postProcessing.edge2);
      break;
    case 77:
      var map = panorama.map;
      if(map) {
          map.active = !map.active;
      }
      break;
    }

    if (panorama.postProcessing) panorama.postProcessing.enabled=panorama.postProcessing.edge.pass.enabled||panorama.postProcessing.edge2.pass.enabled;
  });

  function toggleEffect(effect){
    effect.pass.enabled=!effect.pass.enabled;
    panorama.drawScene();
  }

});
<|endoftext|>


<commit_before>class TabPaper {
  constructor(title = "Untitled", w = 900, h = 900 * 1.41, bps = 4, bl = 4, lw = 840, lh = 140) {
    this.width = w;
    this.height = h;
    this.lineHeight = lh;
    this.lineWidth = lw;
    this.beatLength = bl;
    this.beatPerSection = bps;
    this.vHTML = "";
    this.st = 0;
    this.scale = 1;
    this.content = document.createElement("div");
    this.content.style.outline = "none";
    this.content.onselectstart = function() {
      return false;
    };
    this.content.setAttribute("tabindex", "1");
    this.content.addEventListener("click", this.ckEvent.bind(this));
    this.content.addEventListener("keydown", this.kdEvent.bind(this));
    this.data = null;
    this.title = title;
    this.sel = [];
    this.check();
  }
  check() {
    if (this.width < 500) this.width = 500;
    if (this.height < 500) this.height = 500;
    if (this.lineHeight > 300) this.lineHeight = 120;
    if (this.lineWidth > this.width) this.lineWidth = this.width - 60;
  }
  render() {
    var checkY = function() {
      if (iy + 130 > this.height) {
        iy = 60;
        this.vHTML += `</svg>

				<svg width="${this.width}" height="${this.height}"
				style="background:#FFFFFF";>`;
      }
    }.bind(this);
    let nx = (this.width - this.lineWidth) / 2,
      ix = nx,
      iy = 80;
    if (!this.data) {
      this.drawLine(ix, iy);
      this.vHTML =
        `

			<svg width="${this.width}" height="${this.height}"
			style="background:#FFFFFF";>` +
        this.vHTML +
        "</svg>
";
      this.content.innerHTML = this.vHTML;
      this.zoom();
      return;
    }

    this.vHTML = `

		<svg width="${this.width}" height="${this.height}"
		style="background:#FFFFFF";>`;
    this.drawTitle(ix, iy);
    iy += 40;
    this.drawLine(ix, iy);
    ix += 80;
    this.counter = 0;

    var note_distance = 80;
    var distance_ratio = 1.0;
    for (let i = 0; i < this.data.length; i++) {
      let totaltime = 0;
      // estimate note length

      if (i % 4 == 0) {
        distance_ratio = 1.0;
        var cursor = ix;
        var orig_cursor = ix;
        for (let j = i; j < i + 4 && j < this.data.length; j++) {
          for (let k = 0; k < this.data[j].length; k++) cursor += note_distance * this.beatLength / this.data[j][k][0];
        }
        if (cursor > nx + this.lineWidth) {
          distance_ratio = (nx + this.lineWidth - orig_cursor) / (cursor + 60 - orig_cursor);
        }
      }
      for (let j = 0; j < this.data[i].length; j++) {
        this.drawNote(ix, iy, i, j, this.data[i][j][0], this.data[i][j].slice(1));
        ix += distance_ratio * note_distance * this.beatLength / this.data[i][j][0];
        totaltime += 1 / this.data[i][j][0] * this.beatLength;
      }
      if (i != this.data.length - 1) {
        if (totaltime != this.beatPerSection) this.drawAlert(ix, iy);
        if (i % 4 < 3) this.drawBar(ix, iy);
        ix += distance_ratio * 20;
        if (i % 4 == 3) {
          this.drawBar(nx + this.lineWidth, iy);
          (ix = nx), (iy += this.lineHeight);
          checkY();
          this.drawLine(ix, iy);
          ix += 80;
        }
      }
    }
    this.vHTML += "</svg>
";
    this.content.innerHTML = this.vHTML;
    this.zoom();
    this.noteTextList = this.content.getElementsByClassName("notetext");
    this.noteCircleList = this.content.getElementsByClassName("notecircle");
    this.vHTML = "";
  }

  setDisplayer(e) {
    this.displayer = e;
  }

  zoom() {
    for (let i = 0; i < this.content.children.length; i++) {
      this.content.children[i].style.width = this.width * this.scale + 6 + "px";
      this.content.children[i].style.height = this.height * this.scale + 6 + "px";
      this.content.children[i].style.margin = "auto";
      this.content.children[i].children[0].style.transformOrigin = "0% 0%";
      this.content.children[i].children[0].style.transform = `scale(${this.scale},${this.scale})`;
    }
  }

  selNote(section, pos, id, ln) {
    this.sel[0] = [section, pos, id, ln];
    for (let i = 0; i < this.noteCircleList.length; i++) this.noteCircleList[i].setAttribute("fill", "rgb(255,255,255)");
    this.noteCircleList[ln].setAttribute("fill", "rgb(80,255,80)");
  }

  ckEvent(e) {
    if (e.target.getAttribute("data-type") == "nt") {
      var section = e.target.getAttribute("section");
      var pos = e.target.getAttribute("pos");
      var id = e.target.getAttribute("i");
      this.selNote(section, pos, id, e.target.getAttribute("ln"));
    }
  }

  setScale(s) {
    if (s <= 2.0 && s >= 0.3) this.scale = s;
  }

  kdEvent(e) {
    if (e.keyCode == 107) {
      if (this.sel.length > 0) {
        this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1]++;
        this.noteTextList[this.sel[0][3]].innerHTML = this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1];
      }
    }
    if (e.keyCode == 109) {
      if (this.sel.length > 0) {
        if (this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1] > 0) {
          this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1]--;
          this.noteTextList[this.sel[0][3]].innerHTML = this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1];
        }
      }
    }
    if (e.keyCode == 87) {
      if (this.sel.length > 0) {
        let temp = this.sel[0];
        if (this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1] > 1) {
          this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1]--;
          let ory = this.noteTextList[temp[3]].getAttribute("y");
          let orcy = this.noteCircleList[temp[3]].getAttribute("cy");
          this.noteTextList[temp[3]].setAttribute("y", `${Number(ory) - 14}`);
          this.noteCircleList[temp[3]].setAttribute("cy", `${Number(orcy) - 14}`);
        }
      }
    }
    if (e.keyCode == 83) {
      if (this.sel.length > 0) {
        let temp = this.sel[0];
        if (this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1] < 6) {
          this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1]++;
          let ory = this.noteTextList[temp[3]].getAttribute("y");
          let orcy = this.noteCircleList[temp[3]].getAttribute("cy");
          this.noteTextList[temp[3]].setAttribute("y", `${Number(ory) + 14}`);
          this.noteCircleList[temp[3]].setAttribute("cy", `${Number(orcy) + 14}`);
        }
      }
    }
  }

  load(data) {
    this.data = data;
  }

  drawTitle(x, y) {
    this.vHTML += `<text x="${this.width / 2}" y="${y}" fill="black" text-anchor="middle" font-size="28">${this.title}</text>`;
  }

  drawBar(x, y) {
    this.vHTML += `<path d='M${x} ${y} l0 70'
		style='stroke:black;stroke-width:1'></path>`;
  }

  drawAlert(x, y) {
    this.vHTML += `<circle cx='${x}' cy='${y - 14}' r='5'
		fill='red' stroke-width='0' stroke='red'></circle>`;
  }

  drawLine(x, y) {
    this.vHTML += '<svg  stroke-linecap="square" >';
    for (let i = 0; i < 6; i++) {
      this.vHTML += `<line x1='${x}' y1='${y + i * 14}' x2='${x + this.lineWidth}' y2='${y + i * 14}'
				style="stroke:black;stroke-width:1"
			></line>`;
    }
    this.vHTML += `<text style='fill:black;font:bold 22px Sans-serif'>
		<tspan x='${x + 10}' y='${y + 21}'>T</tspan>
		<tspan x='${x + 10}' y='${y + 21 + 21}'>A</tspan>
		<tspan x='${x + 10}' y='${y + 42 + 21}'>B</tspan>
		</text>`;
    this.vHTML += `<text style='fill:black;font:bold 24px serif'>
		<tspan x='${x + 33}' y='${y + 28}'>${this.beatPerSection}</tspan>
		<tspan x='${x + 33}' y='${y + 23 + 28}'>${this.beatLength}</tspan>
		</text>`;
    this.vHTML += `<path d='M${x} ${y} l0 70 M${x + this.lineWidth} ${y} l0 70'
		style='stroke:black;stroke-width:1'></path>`;
    this.vHTML = this.vHTML + "</svg>";
  }

  drawNote(x, y, section, pos, length, data) {
    this.vHTML += "<svg>";
    for (let i = 0; i < data.length / 2; i++) {
      this.vHTML += `<circle class="notecircle" cx='${x}' cy='${y + 14 * (data[i * 2] - 1)}' r='5'
			fill='white' stroke-width='0' stroke='black' style='cursor:pointer;'></circle>`;
      this.vHTML += `<text class="notetext" ln=${this.counter} data-type="nt" section="${section}" pos="${pos}" i="${i}" x='${x - 4}' y='${y +
        14 * (data[i * 2] - 1) +
        5}'
			fill='black' style='font-size:14px;cursor:pointer;'>${data[i * 2 + 1]}</text>`;
      this.counter++;
    }

    this.vHTML += `<path d='M${x} ${y + 78} l0 25' stroke-width='1' stroke='black'></path>`;
    if (length > this.beatLength) {
      this.vHTML += `<path d='M${x} ${y + 102} l6 1' stroke-width='2' stroke='black'></path>`;
    }
    if (length > this.beatLength * 2) {
      var j = 1;
      for (let i = this.beatLength * 2; i < length; i *= 2) {
        this.vHTML += `<path d='M${x} ${y + 102 - 4 * j} l6 1' stroke-width='1' stroke='black'></path>`;
        j++;
      }
    }
    this.vHTML += "</svg>";
  }
}
<commit_msg>New way to render paper<commit_after>class TabPaper {
  constructor(title = "Untitled", w = 900, h = 900 * 1.41, bps = 4, bl = 4, lw = 840, lh = 140) {
    this.width = w;
    this.height = h;
    this.lineHeight = lh;
    this.lineWidth = lw;
    this.beatLength = bl;
    this.beatPerSection = bps;
    this.vHTML = "";
    this.st = 0;
    this.scale = 1;
    this.content = document.createElement("div");
    this.content.style.outline = "none";
    this.content.onselectstart = function() {
      return false;
    };
    this.content.setAttribute("tabindex", "1");
    this.content.addEventListener("click", this.ckEvent.bind(this));
    this.content.addEventListener("keydown", this.kdEvent.bind(this));
    this.data = null;
    this.title = title;
    this.sel = [];
    this.check();
  }
  check() {
    if (this.width < 500) this.width = 500;
    if (this.height < 500) this.height = 500;
    if (this.lineHeight > 300) this.lineHeight = 120;
    if (this.lineWidth > this.width) this.lineWidth = this.width - 60;
  }
  render() {
    var checkY = function() {
      if (iy + 130 > this.height) {
        iy = 60;
        this.vHTML += `</svg>

				<svg width="${this.width}" height="${this.height}"
				style="background:#FFFFFF";>`;
      }
    }.bind(this);
    let nx = (this.width - this.lineWidth) / 2,
      ix = nx,
      iy = 80;
    if (!this.data) {
      this.drawLine(ix, iy);
      this.vHTML =
        `

			<svg width="${this.width}" height="${this.height}"
			style="background:#FFFFFF";>` +
        this.vHTML +
        "</svg>
";
      this.content.innerHTML = this.vHTML;
      this.zoom();
      return;
    }

    this.vHTML = `

		<svg width="${this.width}" height="${this.height}"
		style="background:#FFFFFF";>`;
    this.drawTitle(ix, iy);
    iy += 40;
    this.drawLine(ix, iy, true);
    ix += 80;
    this.counter = 0;

    var note_distance = 80;
    var distance_ratio = 1.0;
    var notes_four_sections = 0;
    var calc_beats = function(beatLength, arr) {
      var ret = 0;
      for(let i=0; i<arr.length; i++)
        ret += beatLength / arr[i][0];
      return ret;
    };
    for (let i = 0; i < this.data.length; i++) {
      let totaltime = 0;
      // estimate note  section

      if (i % 4 == 0) {
        for (let j = i; j < i + 4 && j < this.data.length; j++) {
            notes_four_sections += this.data[j].length;
        }
      }

      var section_width = (this.lineWidth-80) * (this.data[i].length / notes_four_sections)
      var beat_width = section_width / calc_beats(this.beatLength, this.data[i]);

      for (let j = 0; j < this.data[i].length; j++) {
        if(i%4!=0 || j!=0)
          ix += beat_width * (this.beatLength / this.data[i][j][0]) / 2;
        this.drawNote(ix, iy, i, j, this.data[i][j][0], this.data[i][j].slice(1));
        ix += beat_width * (this.beatLength / this.data[i][j][0]) / 2;
        totaltime += 1 / this.data[i][j][0] * this.beatLength;
      }
      if (totaltime != this.beatPerSection) this.drawAlert(ix, iy);
      if (i % 4 < 3) this.drawBar(ix, iy);
      if (i % 4 == 3) {
        (ix = nx), (iy += this.lineHeight);
        checkY();
        this.drawLine(ix, iy);
        ix += 80;
      }

    }
    this.vHTML += "</svg>
";
    this.content.innerHTML = this.vHTML;
    this.zoom();
    this.noteTextList = this.content.getElementsByClassName("notetext");
    this.noteCircleList = this.content.getElementsByClassName("notecircle");
    this.vHTML = "";
  }

  setDisplayer(e) {
    this.displayer = e;
  }

  zoom() {
    for (let i = 0; i < this.content.children.length; i++) {
      this.content.children[i].style.width = this.width * this.scale + 6 + "px";
      this.content.children[i].style.height = this.height * this.scale + 6 + "px";
      this.content.children[i].style.margin = "auto";
      this.content.children[i].children[0].style.transformOrigin = "0% 0%";
      this.content.children[i].children[0].style.transform = `scale(${this.scale},${this.scale})`;
    }
  }

  selNote(section, pos, id, ln) {
    this.sel[0] = [section, pos, id, ln];
    for (let i = 0; i < this.noteCircleList.length; i++) this.noteCircleList[i].setAttribute("fill", "rgb(255,255,255)");
    this.noteCircleList[ln].setAttribute("fill", "rgb(80,255,80)");
  }

  ckEvent(e) {
    if (e.target.getAttribute("data-type") == "nt") {
      var section = e.target.getAttribute("section");
      var pos = e.target.getAttribute("pos");
      var id = e.target.getAttribute("i");
      this.selNote(section, pos, id, e.target.getAttribute("ln"));
    }
  }

  setScale(s) {
    if (s <= 2.0 && s >= 0.3) this.scale = s;
  }

  kdEvent(e) {
    if (e.keyCode == 107) {
      if (this.sel.length > 0) {
        this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1]++;
        this.noteTextList[this.sel[0][3]].innerHTML = this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1];
      }
    }
    if (e.keyCode == 109) {
      if (this.sel.length > 0) {
        if (this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1] > 0) {
          this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1]--;
          this.noteTextList[this.sel[0][3]].innerHTML = this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1 + 1];
        }
      }
    }
    if (e.keyCode == 87) {
      if (this.sel.length > 0) {
        let temp = this.sel[0];
        if (this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1] > 1) {
          this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1]--;
          let ory = this.noteTextList[temp[3]].getAttribute("y");
          let orcy = this.noteCircleList[temp[3]].getAttribute("cy");
          this.noteTextList[temp[3]].setAttribute("y", `${Number(ory) - 14}`);
          this.noteCircleList[temp[3]].setAttribute("cy", `${Number(orcy) - 14}`);
        }
      }
    }
    if (e.keyCode == 83) {
      if (this.sel.length > 0) {
        let temp = this.sel[0];
        if (this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1] < 6) {
          this.data[this.sel[0][0]][this.sel[0][1]][this.sel[0][2] * 2 + 1]++;
          let ory = this.noteTextList[temp[3]].getAttribute("y");
          let orcy = this.noteCircleList[temp[3]].getAttribute("cy");
          this.noteTextList[temp[3]].setAttribute("y", `${Number(ory) + 14}`);
          this.noteCircleList[temp[3]].setAttribute("cy", `${Number(orcy) + 14}`);
        }
      }
    }
  }

  load(data) {
    this.data = data;
  }

  drawTitle(x, y) {
    this.vHTML += `<text x="${this.width / 2}" y="${y}" fill="black" text-anchor="middle" font-size="28">${this.title}</text>`;
  }

  drawBar(x, y) {
    this.vHTML += `<path d='M${x} ${y} l0 70'
		style='stroke:black;stroke-width:1'></path>`;
  }

  drawAlert(x, y) {
    this.vHTML += `<circle cx='${x}' cy='${y - 14}' r='5'
		fill='red' stroke-width='0' stroke='red'></circle>`;
  }

  drawLine(x, y, first=false) {
    this.vHTML += '<svg  stroke-linecap="square" >';
    for (let i = 0; i < 6; i++) {
      this.vHTML += `<line x1='${x}' y1='${y + i * 14}' x2='${x + this.lineWidth}' y2='${y + i * 14}'
				style="stroke:black;stroke-width:1"
			></line>`;
    }
    this.vHTML += `<text style='fill:black;font:bold 22px Sans-serif'>
		<tspan x='${x + 10}' y='${y + 21}'>T</tspan>
		<tspan x='${x + 10}' y='${y + 21 + 21}'>A</tspan>
		<tspan x='${x + 10}' y='${y + 42 + 21}'>B</tspan>
		</text>`;
    if(first) {
      this.vHTML += `<text style='fill:black;font:bold 24px serif'>
		  <tspan x='${x + 33}' y='${y + 28}'>${this.beatPerSection}</tspan>
		  <tspan x='${x + 33}' y='${y + 23 + 28}'>${this.beatLength}</tspan>
		  </text>`;
    }
    this.vHTML += `<path d='M${x} ${y} l0 70 M${x + this.lineWidth} ${y} l0 70'
		style='stroke:black;stroke-width:1'></path>`;
    this.vHTML = this.vHTML + "</svg>";
  }

  drawNote(x, y, section, pos, length, data) {
    this.vHTML += "<svg>";
    for (let i = 0; i < data.length / 2; i++) {
      this.vHTML += `<circle class="notecircle" cx='${x}' cy='${y + 14 * (data[i * 2] - 1)}' r='5'
			fill='white' stroke-width='0' stroke='black' style='cursor:pointer;'></circle>`;
      this.vHTML += `<text class="notetext" ln=${this.counter} data-type="nt" section="${section}" pos="${pos}" i="${i}" x='${x - 4}' y='${y +
        14 * (data[i * 2] - 1) +
        5}'
			fill='black' style='font-size:14px;cursor:pointer;'>${data[i * 2 + 1]}</text>`;
      this.counter++;
    }

    this.vHTML += `<path d='M${x} ${y + 78} l0 25' stroke-width='1' stroke='black'></path>`;
    if (length > this.beatLength) {
      this.vHTML += `<path d='M${x} ${y + 102} l6 1' stroke-width='2' stroke='black'></path>`;
    }
    if (length > this.beatLength * 2) {
      var j = 1;
      for (let i = this.beatLength * 2; i < length; i *= 2) {
        this.vHTML += `<path d='M${x} ${y + 102 - 4 * j} l6 1' stroke-width='1' stroke='black'></path>`;
        j++;
      }
    }
    this.vHTML += "</svg>";
  }
}
<|endoftext|>


<commit_before>/*
 * We have a few competing goals:
 *
 * First, be as responsive as possible.  Preload nearby post HTML and their images.
 *
 * If data in a post page changes, eg. if the user votes, then coming back to the page
 * later should retain the changes.  This means either requesting the page again, or
 * retaining the document node and reusing it, so we preserve the changes that were
 * made in-place.
 *
 * Don't use too much memory.  If we keep every document node in memory as we use it,
 * the images will probably be kept around too.  Release older nodes, so the browser
 * is more likely to release images that havn't been used in a while.
 *
 * We do the following:
 * - When we load a new post, it's formatted and its scripts are evaluated normally.
 * - When we're replacing the displayed post, its node is stashed away in a node cache.
 * - If we come back to the post while it's in the node cache, we'll use the node directly.
 * - HTML and images for posts are preloaded.  We don't use a simple mechanism like
 *   Preload.preload_raw, because Opera's caching is broken for XHR and it'll always
 *   do a slow revalidation.
 * - We don't depend on browser caching for HTML.  That would require us to expire a
 *   page when we switch away from it if we've made any changes (eg. voting), so we
 *   don't pull an out-of-date page next time.  This is slower, and would require us
 *   to be careful about expiring the cache.
 */

BrowserView = function(container)
{
  this.container = container;

  /* The post that we currently want to display.  This will be either one of the
   * current html_preloads, or be the displayed_post_id. */
  this.wanted_post_id = null;

  /* The post that's currently actually being displayed. */
  this.displayed_post_id = null;

  this.current_ajax_request = null;
  this.last_preload_request = [];
  this.last_preload_request_active = false;

  debug.add_hook(this.get_debug.bind(this));

  this.image_loaded_event = this.image_loaded_event.bindAsEventListener(this);
  this.img = this.container.down(".image");
  this.img.observe("load", this.image_loaded_event);
}

BrowserView.prototype.image_loaded_event = function(event)
{
  document.fire("viewer:displayed-image-loaded", { post_id: this.displayed_post_id });
}

BrowserView.prototype.get_debug = function()
{
  var s = "wanted: " + this.wanted_post_id + ", displayed: " + this.displayed_post_id;
  if(this.lazy_load_timer)
    s += ", lazy load pending";
  return s;
}

/* Begin preloading the HTML and images for the given post IDs. */
BrowserView.prototype.preload = function(post_ids)
{
  /* We're being asked to preload post_ids.  Only do this if it seems to make sense: if
   * the user is actually traversing posts that are being preloaded.  Look at the previous
   * call to preload().  If it didn't include the current post, then skip the preload. */
  var last_preload_request = this.last_preload_request;
  this.last_preload_request = post_ids;
  if(last_preload_request.indexOf(this.wanted_post_id) == -1)
  {
    debug.log("skipped-preload(" + post_ids.join(",") + ")");
    this.last_preload_request_active = false;
    return;
  }
  this.last_preload_request_active = true;
  debug.log("preload(" + post_ids.join(",") + ")");

  var new_preload_container = new PreloadContainer();
  for(var i = 0; i < post_ids.length; ++i)
  {
    var post_id = post_ids[i];
    var post = Post.posts.get(post_id);
    new_preload_container.preload(post.sample_url);
  }

  /* If we already were preloading images, we created the new preloads before
   * deleting the old ones.  That way, any images that are still being preloaded
   * won't be deleted and recreated, possibly causing the download to be interrupted
   * and resumed. */
  if(this.preload_container)
    this.preload_container.destroy();
  this.preload_container = new_preload_container;
}

BrowserView.prototype.load_post_id_data = function(post_id)
{
  debug.log("load needed");

  // If we already have a request in flight, don't start another; wait for the
  // first to finish.
  if(this.current_ajax_request != null)
    return;

  new Ajax.Request("/post/index.json", {
    parameters: { tags: "id:" + post_id, filter: 1 },
    method: "get",

    onCreate: function(resp) {
      this.current_ajax_request = resp.request;
    }.bind(this),

    onComplete: function(resp) {
      if(this.current_ajax_request == resp.request)
        this.current_ajax_request = null;

      if(!resp.request.success() && post_id == this.wanted_post_id)
        return;

      /* This will either load the post we just finished, or request data for the
       * one we want. */
      this.set_post_content(this.wanted_post_id);
    }.bind(this),

    onSuccess: function(resp) {
      if(this.current_ajax_request != resp.request)
        return;

      var post = resp.responseJSON[0];
      Post.register(post);
    }.bind(this),

    onFailure: function(resp) {
      notice("Error " + resp.status + " loading post");
    }.bind(this)
  });
}

BrowserView.prototype.set_post_content = function(post_id)
{
  if(post_id == this.displayed_post_id)
    return;

  var post = Post.posts.get(post_id);
  if(post == null)
  {
    this.load_post_id_data(post_id);
    return;
  }

  this.displayed_post_id = post_id;
  UrlHash.set({"post-id": post_id});

  /* Clear the previous post, if any. */
  this.img.src = "about:blank";

  if(post)
  {
    this.img.hide();
    this.img.width = post.sample_width;
    this.img.height = post.sample_height;
    this.img.src = post.sample_url;
    this.img.show();

    Post.scale_and_fit_image(this.img);
  }

  Post.init_post_show(post_id);
// InitTextAreas();

  document.fire("viewer:displayed-post-changed", { post_id: post_id });
}

BrowserView.prototype.get_url_for_post_page = function(post_id)
{
  return "/post/show/" + post_id + "?browser=1";
}

BrowserView.prototype.set_post = function(post_id)
{
  /* If there was a lazy load pending, cancel it. */
  this.cancel_lazily_load();

  this.wanted_post_id = post_id;

  /* We don't have the node cached.  Open the page from HTML cache or start
   * loading the page as necessary. */
  this.set_post_content(post_id);
}

BrowserView.prototype.cancel_lazily_load = function()
{
  if(this.lazy_load_timer == null)
    return;

   window.clearTimeout(this.lazy_load_timer);
   this.lazy_load_timer = null;
}

BrowserView.prototype.lazily_load = function(post_id)
{
  this.cancel_lazily_load();

  /* If we already started the preload for the requested post, then use a small timeout. */
  var is_cached = this.last_preload_request_active && this.last_preload_request.indexOf(post_id) != -1;

  var ms = is_cached? 50:500;
  debug.log("post:" + post_id + ":" + is_cached + ":" + ms);

  /* Once lazily_load is called with a new post, we should consistently stay on the current
   * post or change to the new post.  We shouldn't change to a post that was previously
   * requested by lazily_load (due to a background request completing).  Mark whatever post
   * we're currently on as the one we want, until we're able to switch to the new one. */
  this.wanted_post_id = this.displayed_post_id;

  this.lazy_load_post_id = post_id;
  this.lazy_load_timer = window.setTimeout(function() {
    this.lazy_load_timer = null;
    this.set_post(post_id);
  }.bind(this), ms);
}

/* Update the window title when the display changes. */
WindowTitleHandler = function()
{
  this.searched_tags = "";
  this.post_id = null;
  this.pool = null;

  document.observe("viewer:searched-tags-changed", function(e) {
    this.searched_tags = e.memo.tags;
    this.update();
  }.bindAsEventListener(this));

  document.observe("viewer:displayed-post-changed", function(e) {
    this.post_id = e.memo.post_id;
    this.update();
  }.bindAsEventListener(this));

  document.observe("viewer:displayed-pool-changed", function(e) {
    this.pool = e.memo.pool;
    this.update();
  }.bindAsEventListener(this));

  this.update();
}

WindowTitleHandler.prototype.update = function()
{
  var post = Post.posts.get(this.post_id);

  if(this.pool)
  {
    var title = "Browse " + this.pool.name.replace(/_/g, " ");

    if(post && post.pool_post)
    {
      var sequence = post.pool_post.sequence;
      title += " ";
      if(sequence.match(/^[0-9]/))
        title += "#";
      title += sequence;
    }

    document.title = title;
    return;
  }

  var title = "Browse /" + this.searched_tags.replace(/_/g, " ");
  document.title = title;
}

<commit_msg>work around chrome redraw bugs<commit_after>/*
 * We have a few competing goals:
 *
 * First, be as responsive as possible.  Preload nearby post HTML and their images.
 *
 * If data in a post page changes, eg. if the user votes, then coming back to the page
 * later should retain the changes.  This means either requesting the page again, or
 * retaining the document node and reusing it, so we preserve the changes that were
 * made in-place.
 *
 * Don't use too much memory.  If we keep every document node in memory as we use it,
 * the images will probably be kept around too.  Release older nodes, so the browser
 * is more likely to release images that havn't been used in a while.
 *
 * We do the following:
 * - When we load a new post, it's formatted and its scripts are evaluated normally.
 * - When we're replacing the displayed post, its node is stashed away in a node cache.
 * - If we come back to the post while it's in the node cache, we'll use the node directly.
 * - HTML and images for posts are preloaded.  We don't use a simple mechanism like
 *   Preload.preload_raw, because Opera's caching is broken for XHR and it'll always
 *   do a slow revalidation.
 * - We don't depend on browser caching for HTML.  That would require us to expire a
 *   page when we switch away from it if we've made any changes (eg. voting), so we
 *   don't pull an out-of-date page next time.  This is slower, and would require us
 *   to be careful about expiring the cache.
 */

BrowserView = function(container)
{
  this.container = container;

  /* The post that we currently want to display.  This will be either one of the
   * current html_preloads, or be the displayed_post_id. */
  this.wanted_post_id = null;

  /* The post that's currently actually being displayed. */
  this.displayed_post_id = null;

  this.current_ajax_request = null;
  this.last_preload_request = [];
  this.last_preload_request_active = false;

  debug.add_hook(this.get_debug.bind(this));

  this.image_loaded_event = this.image_loaded_event.bindAsEventListener(this);
  this.img = this.container.down(".image");
  this.img.observe("load", this.image_loaded_event);

  /* We normally have scrollbars disabled.  This currently causes redraw problems
   * in Chrome 7.0.  Reenable scrolling for that browser; it's less obnoxious than
   * the redraw glitching. */
  if(navigator.userAgent.indexOf('Chrome/7.0') != -1)
    document.body.setStyle({overflow: "auto"});
}

BrowserView.prototype.image_loaded_event = function(event)
{
  document.fire("viewer:displayed-image-loaded", { post_id: this.displayed_post_id });
}

BrowserView.prototype.get_debug = function()
{
  var s = "wanted: " + this.wanted_post_id + ", displayed: " + this.displayed_post_id;
  if(this.lazy_load_timer)
    s += ", lazy load pending";
  return s;
}

/* Begin preloading the HTML and images for the given post IDs. */
BrowserView.prototype.preload = function(post_ids)
{
  /* We're being asked to preload post_ids.  Only do this if it seems to make sense: if
   * the user is actually traversing posts that are being preloaded.  Look at the previous
   * call to preload().  If it didn't include the current post, then skip the preload. */
  var last_preload_request = this.last_preload_request;
  this.last_preload_request = post_ids;
  if(last_preload_request.indexOf(this.wanted_post_id) == -1)
  {
    debug.log("skipped-preload(" + post_ids.join(",") + ")");
    this.last_preload_request_active = false;
    return;
  }
  this.last_preload_request_active = true;
  debug.log("preload(" + post_ids.join(",") + ")");

  var new_preload_container = new PreloadContainer();
  for(var i = 0; i < post_ids.length; ++i)
  {
    var post_id = post_ids[i];
    var post = Post.posts.get(post_id);
    new_preload_container.preload(post.sample_url);
  }

  /* If we already were preloading images, we created the new preloads before
   * deleting the old ones.  That way, any images that are still being preloaded
   * won't be deleted and recreated, possibly causing the download to be interrupted
   * and resumed. */
  if(this.preload_container)
    this.preload_container.destroy();
  this.preload_container = new_preload_container;
}

BrowserView.prototype.load_post_id_data = function(post_id)
{
  debug.log("load needed");

  // If we already have a request in flight, don't start another; wait for the
  // first to finish.
  if(this.current_ajax_request != null)
    return;

  new Ajax.Request("/post/index.json", {
    parameters: { tags: "id:" + post_id, filter: 1 },
    method: "get",

    onCreate: function(resp) {
      this.current_ajax_request = resp.request;
    }.bind(this),

    onComplete: function(resp) {
      if(this.current_ajax_request == resp.request)
        this.current_ajax_request = null;

      if(!resp.request.success() && post_id == this.wanted_post_id)
        return;

      /* This will either load the post we just finished, or request data for the
       * one we want. */
      this.set_post_content(this.wanted_post_id);
    }.bind(this),

    onSuccess: function(resp) {
      if(this.current_ajax_request != resp.request)
        return;

      var post = resp.responseJSON[0];
      Post.register(post);
    }.bind(this),

    onFailure: function(resp) {
      notice("Error " + resp.status + " loading post");
    }.bind(this)
  });
}

BrowserView.prototype.set_post_content = function(post_id)
{
  if(post_id == this.displayed_post_id)
    return;

  var post = Post.posts.get(post_id);
  if(post == null)
  {
    this.load_post_id_data(post_id);
    return;
  }

  this.displayed_post_id = post_id;
  UrlHash.set({"post-id": post_id});

  /* Clear the previous post, if any. */
  this.img.src = "about:blank";

  if(post)
  {
    this.img.hide();
    this.img.width = post.sample_width;
    this.img.height = post.sample_height;
    this.img.src = post.sample_url;
    this.img.show();

    Post.scale_and_fit_image(this.img);
  }

  Post.init_post_show(post_id);
// InitTextAreas();

  document.fire("viewer:displayed-post-changed", { post_id: post_id });
}

BrowserView.prototype.get_url_for_post_page = function(post_id)
{
  return "/post/show/" + post_id + "?browser=1";
}

BrowserView.prototype.set_post = function(post_id)
{
  /* If there was a lazy load pending, cancel it. */
  this.cancel_lazily_load();

  this.wanted_post_id = post_id;

  /* We don't have the node cached.  Open the page from HTML cache or start
   * loading the page as necessary. */
  this.set_post_content(post_id);
}

BrowserView.prototype.cancel_lazily_load = function()
{
  if(this.lazy_load_timer == null)
    return;

   window.clearTimeout(this.lazy_load_timer);
   this.lazy_load_timer = null;
}

BrowserView.prototype.lazily_load = function(post_id)
{
  this.cancel_lazily_load();

  /* If we already started the preload for the requested post, then use a small timeout. */
  var is_cached = this.last_preload_request_active && this.last_preload_request.indexOf(post_id) != -1;

  var ms = is_cached? 50:500;
  debug.log("post:" + post_id + ":" + is_cached + ":" + ms);

  /* Once lazily_load is called with a new post, we should consistently stay on the current
   * post or change to the new post.  We shouldn't change to a post that was previously
   * requested by lazily_load (due to a background request completing).  Mark whatever post
   * we're currently on as the one we want, until we're able to switch to the new one. */
  this.wanted_post_id = this.displayed_post_id;

  this.lazy_load_post_id = post_id;
  this.lazy_load_timer = window.setTimeout(function() {
    this.lazy_load_timer = null;
    this.set_post(post_id);
  }.bind(this), ms);
}

/* Update the window title when the display changes. */
WindowTitleHandler = function()
{
  this.searched_tags = "";
  this.post_id = null;
  this.pool = null;

  document.observe("viewer:searched-tags-changed", function(e) {
    this.searched_tags = e.memo.tags;
    this.update();
  }.bindAsEventListener(this));

  document.observe("viewer:displayed-post-changed", function(e) {
    this.post_id = e.memo.post_id;
    this.update();
  }.bindAsEventListener(this));

  document.observe("viewer:displayed-pool-changed", function(e) {
    this.pool = e.memo.pool;
    this.update();
  }.bindAsEventListener(this));

  this.update();
}

WindowTitleHandler.prototype.update = function()
{
  var post = Post.posts.get(this.post_id);

  if(this.pool)
  {
    var title = "Browse " + this.pool.name.replace(/_/g, " ");

    if(post && post.pool_post)
    {
      var sequence = post.pool_post.sequence;
      title += " ";
      if(sequence.match(/^[0-9]/))
        title += "#";
      title += sequence;
    }

    document.title = title;
    return;
  }

  var title = "Browse /" + this.searched_tags.replace(/_/g, " ");
  document.title = title;
}

<|endoftext|>


<commit_before>function Anything(anything$){
  $init$Anything();
  if(anything$===undefined)throwexc(InvocationException$meta$model("Cannot instantiate abstract class"),'?','?')
  return anything$;
}
Anything.$crtmm$=function(){return{mod:$CCMM$,ps:[],of:[{t:$_Object},{t:Null}],pa:257,an:function(){return[doc$('$:Anything'),by(["Gavin"].rt$({t:$_String}))];},d:['$','Anything']};};
ex$.Anything=Anything;
function $init$Anything(){
  if(Anything.$$===undefined){initTypeProto(Anything,'ceylon.language::Anything');}
  return Anything;
}
ex$.$init$Anything=$init$Anything;$init$Anything();
function Nothing(wat) {
    throw "Nothing";
}
initType(Nothing, 'ceylon.language::Nothing');
Nothing.$crtmm$=function(){return{ps:[],pa:1,mod:$CCMM$,d:['$','Nothing']};}
<commit_msg>missing arg<commit_after>function Anything(anything$){
  $init$Anything();
  if(anything$===undefined)throwexc(InvocationException$meta$model("Cannot instantiate abstract class"),'?','?')
  return anything$;
}
Anything.$crtmm$=function(){return{mod:$CCMM$,ps:[],of:[{t:$_Object},{t:Null}],pa:257,an:function(){return[doc$($CCMM$,'$:Anything'),by(["Gavin"].rt$({t:$_String}))];},d:['$','Anything']};};
ex$.Anything=Anything;
function $init$Anything(){
  if(Anything.$$===undefined){initTypeProto(Anything,'ceylon.language::Anything');}
  return Anything;
}
ex$.$init$Anything=$init$Anything;$init$Anything();
function Nothing(wat) {
    throw "Nothing";
}
initType(Nothing, 'ceylon.language::Nothing');
Nothing.$crtmm$=function(){return{ps:[],pa:1,mod:$CCMM$,d:['$','Nothing']};}
<|endoftext|>


<commit_before>const purgecss =
  // process.env.NODE_ENV === 'production'
  true
    ? {
        '@fullhuman/postcss-purgecss': {
          content: ['./**/*.tsx'],
          whitelist: ['html', 'body', '__next'],
          defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
        },
      }
    : {}

module.exports = {
  plugins: {
    'postcss-import': {},
    tailwindcss: {},
    'postcss-nested': {},
    ...purgecss,
  },
}
<commit_msg>Update postcss config<commit_after>/* eslint-env node */

const purgecss =
  process.env.NODE_ENV === 'production'
    ? {
        '@fullhuman/postcss-purgecss': {
          content: ['./**/*.tsx'],
          safelist: ['html', 'body', '__next'],
          defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
        },
      }
    : {}

module.exports = {
  plugins: {
    'postcss-import': {},
    tailwindcss: {},
    'postcss-nested': {},
    ...purgecss,
  },
}
<|endoftext|>