conveyal/transitive.js

View on GitHub
lib/display/display.js

Summary

Maintainability
F
3 days
Test Coverage
import LinearScale from '../util/linear-scale.js'

export default class Display {
  constructor(transitive) {
    this.transitive = transitive

    this.zoomFactors =
      transitive.options.zoomFactors || this.getDefaultZoomFactors()

    this.updateActiveZoomFactors(1)
  }

  setDimensions(width, height) {
    this.width = width
    this.height = height
  }

  setXDomain(domain) {
    // [minX , maxX]
    this.xDomain = domain
    this.xScale = new LinearScale(domain, [0, this.width])
    if (!this.initialXDomain) {
      this.initialXDomain = domain
      this.initialXRes = (domain[1] - domain[0]) / this.width
    }
  }

  setYDomain(domain) {
    // [minY , maxY]
    this.yDomain = domain
    this.yScale = new LinearScale(domain, [this.height, 0])
    if (!this.initialYDomain) this.initialYDomain = domain
  }

  fitToWorldBounds(bounds) {
    const domains = this.computeDomainsFromBounds(bounds)
    this.setXDomain(domains[0])
    this.setYDomain(domains[1])
    this.computeScale()
  }

  reset() {
    this.initialXDomain = null
    this.initialYDomain = null
    this.scaleSet = false
    this.lastScale = undefined
  }

  /**
   * Apply a transformation {x, y, k} to the *initial* state of the map, where
   * (x, y) is the pixel offset and k is a scale factor relative to an initial
   * zoom level of 1.0. Intended primarily to support D3-style panning/zooming.
   */

  applyTransform(transform) {
    const { k, x, y } = transform

    let xMin = this.initialXDomain[0]
    let xMax = this.initialXDomain[1]
    let yMin = this.initialYDomain[0]
    let yMax = this.initialYDomain[1]

    // Apply the scale factor
    xMax = xMin + (xMax - xMin) / k
    yMin = yMax - (yMax - yMin) / k

    // Apply the translation
    const xOffset = (-x * (xMax - xMin)) / this.width
    xMin += xOffset
    xMax += xOffset
    const yOffset = (y * (yMax - yMin)) / this.height
    yMin += yOffset
    yMax += yOffset

    // Update the scale functions and recompute the internal scale factor
    this.setXDomain([xMin, xMax])
    this.setYDomain([yMin, yMax])
    this.computeScale()
  }

  getDefaultZoomFactors(data) {
    return [
      {
        angleConstraint: 45,
        gridCellSize: 25,
        internalVertexFactor: 1000000,
        mergeVertexThreshold: 200,
        minScale: 0
      },
      {
        angleConstraint: 5,
        gridCellSize: 0,
        internalVertexFactor: 0,
        mergeVertexThreshold: 0,
        minScale: 1.5
      }
    ]
  }

  updateActiveZoomFactors(scale) {
    let updated = false
    for (let i = 0; i < this.zoomFactors.length; i++) {
      const min = this.zoomFactors[i].minScale
      const max =
        i < this.zoomFactors.length - 1
          ? this.zoomFactors[i + 1].minScale
          : Number.MAX_VALUE

      // check if we've crossed into a new zoomFactor partition
      if (
        (!this.lastScale || this.lastScale < min || this.lastScale >= max) &&
        scale >= min &&
        scale < max
      ) {
        this.activeZoomFactors = this.zoomFactors[i]
        updated = true
      }
    }
    return updated
  }

  computeScale() {
    this.lastScale = this.scale
    this.scaleSet = true
    const newXRes = (this.xDomain[1] - this.xDomain[0]) / this.width
    this.scale = this.initialXRes / newXRes
    if (this.lastScale !== this.scale) this.scaleChanged()
  }

  scaleChanged() {
    const zoomFactorsChanged = this.updateActiveZoomFactors(this.scale)
    if (zoomFactorsChanged) {
      this.transitive.network = null
      this.transitive.render()
    }
  }

  /**
   * Compute the x/y coordinate space domains to fit the graph.
   */

  computeDomainsFromBounds(bounds) {
    const xmin = bounds[0][0]
    const xmax = bounds[1][0]
    const ymin = bounds[0][1]
    const ymax = bounds[1][1]
    const xRange = xmax - xmin
    const yRange = ymax - ymin

    const { options } = this.transitive

    const paddingFactor =
      options && options.paddingFactor ? options.paddingFactor : 0.1

    const margins = this.getMargins()

    const usableHeight = this.height - margins.top - margins.bottom
    const usableWidth = this.width - margins.left - margins.right
    const displayAspect = this.width / this.height
    const usableDisplayAspect = usableWidth / usableHeight
    const graphAspect = xRange / (yRange === 0 ? -Infinity : yRange)

    let padding
    let dispX1, dispX2, dispY1, dispY2
    let dispXRange, dispYRange

    if (usableDisplayAspect > graphAspect) {
      // y-axis is limiting
      padding = paddingFactor * yRange
      dispY1 = ymin - padding
      dispY2 = ymax + padding
      dispYRange = yRange + 2 * padding
      const addedYRange = (this.height / usableHeight) * dispYRange - dispYRange
      if (margins.top > 0 || margins.bottom > 0) {
        dispY1 -=
          (margins.bottom / (margins.bottom + margins.top)) * addedYRange
        dispY2 += (margins.top / (margins.bottom + margins.top)) * addedYRange
      }
      dispXRange = (dispY2 - dispY1) * displayAspect
      const xOffset = (margins.left - margins.right) / this.width
      const xMidpoint = (xmax + xmin - dispXRange * xOffset) / 2
      dispX1 = xMidpoint - dispXRange / 2
      dispX2 = xMidpoint + dispXRange / 2
    } else {
      // x-axis limiting
      padding = paddingFactor * xRange
      dispX1 = xmin - padding
      dispX2 = xmax + padding
      dispXRange = xRange + 2 * padding
      const addedXRange = (this.width / usableWidth) * dispXRange - dispXRange
      if (margins.left > 0 || margins.right > 0) {
        dispX1 -= (margins.left / (margins.left + margins.right)) * addedXRange
        dispX2 += (margins.right / (margins.left + margins.right)) * addedXRange
      }

      dispYRange = (dispX2 - dispX1) / displayAspect
      const yOffset = (margins.bottom - margins.top) / this.height
      const yMidpoint = (ymax + ymin - dispYRange * yOffset) / 2
      dispY1 = yMidpoint - dispYRange / 2
      dispY2 = yMidpoint + dispYRange / 2
    }

    return [
      [dispX1, dispX2],
      [dispY1, dispY2]
    ]
  }

  getMargins() {
    return Object.assign(
      {
        bottom: 0,
        left: 0,
        right: 0,
        top: 0
      },
      this.transitive.options.displayMargins
    )
  }

  isInRange(x, y) {
    return x >= 0 && x <= this.width && y >= 0 && y <= this.height
  }

  /** Methods to be defined by subclasses **/

  clear() {
    throw new Error('method not implemented by subclass!')
  }

  drawCircle(coord, attrs) {
    throw new Error('method not implemented by subclass!')
  }

  drawEllipse(coord, attrs) {
    throw new Error('method not implemented by subclass!')
  }

  drawRect(upperLeft, attrs) {
    throw new Error('method not implemented by subclass!')
  }

  drawText(text, anchor, attrs) {
    throw new Error('method not implemented by subclass!')
  }

  drawPath(renderData, attrs) {
    throw new Error('method not implemented by subclass!')
  }
}