conveyal/transitive.js

View on GitHub
lib/util/index.js

Summary

Maintainability
C
7 hrs
Test Coverage
/**
 * General Transitive utilities library
 */

import SphericalMercator from 'sphericalmercator'

const TOLERANCE = 0.000001

function fuzzyEquals(a, b, tolerance = TOLERANCE) {
  return Math.abs(a - b) < tolerance
}

function distance(x1, y1, x2, y2) {
  return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1))
}

function getRadiusFromAngleChord(angleR, chordLen) {
  return chordLen / 2 / Math.sin(angleR / 2)
}

/*
 * CCW utility function. Accepts 3 coord pairs; result is positive if points
 * have counterclockwise orientation, negative if clockwise, 0 if collinear.
 */

function ccw(ax, ay, bx, by, cx, cy) {
  const raw = ccwRaw(ax, ay, bx, by, cx, cy)
  return raw === 0 ? 0 : raw / Math.abs(raw)
}

function ccwRaw(ax, ay, bx, by, cx, cy) {
  return (bx - ax) * (cy - ay) - (cx - ax) * (by - ay)
}

/*
 * Compute angle formed by three points in cartesian plane using law of cosines
 */

function angleFromThreePoints(ax, ay, bx, by, cx, cy) {
  const c = distance(ax, ay, bx, by)
  const a = distance(bx, by, cx, cy)
  const b = distance(ax, ay, cx, cy)
  return Math.acos((a * a + c * c - b * b) / (2 * a * c))
}

function pointAlongArc(x1, y1, x2, y2, r, theta, ccw, t) {
  ccw = Math.abs(ccw) / ccw // convert to 1 or -1

  let rot = Math.PI / 2 - Math.abs(theta) / 2
  const vectToCenter = normalizeVector(
    rotateVector(
      {
        x: x2 - x1,
        y: y2 - y1
      },
      ccw * rot
    )
  )

  // calculate the center of the arc circle
  const cx = x1 + r * vectToCenter.x
  const cy = y1 + r * vectToCenter.y

  let vectFromCenter = negateVector(vectToCenter)
  rot = Math.abs(theta) * t * ccw
  vectFromCenter = normalizeVector(rotateVector(vectFromCenter, rot))

  return {
    x: cx + r * vectFromCenter.x,
    y: cy + r * vectFromCenter.y
  }
}

function getVectorAngle(x, y) {
  let t = Math.atan(y / x)

  if (x < 0 && t <= 0) t += Math.PI
  else if (x < 0 && t >= 0) t -= Math.PI

  return t
}

function rayIntersection(ax, ay, avx, avy, bx, by, bvx, bvy) {
  const u = ((by - ay) * bvx - (bx - ax) * bvy) / (bvx * avy - bvy * avx)
  const v = ((by - ay) * avx - (bx - ax) * avy) / (bvx * avy - bvy * avx)

  return {
    intersect: u > -TOLERANCE && v > -TOLERANCE,
    u: u,
    v: v
  }
}

function lineIntersection(x1, y1, x2, y2, x3, y3, x4, y4) {
  const d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)

  if (d === 0) {
    // lines are parallel
    return {
      intersect: false
    }
  }

  return {
    intersect: true,
    x: ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d,
    y: ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
  }
}

/**
 * Parse a pixel-based style descriptor, returning an number.
 *
 * @param {String/Number}
 */
function parsePixelStyle(descriptor) {
  if (typeof descriptor === 'number') return descriptor
  return parseFloat(descriptor.substring(0, descriptor.length - 2), 10)
}

/**
 * Whether vector is projected into positive xy quadrant.
 */
function isOutwardVector(vector) {
  return !fuzzyEquals(vector.x, 0) ? vector.x > 0 : vector.y > 0
}

/**
 * vector utilities
 */

function normalizeVector(v) {
  const d = Math.sqrt(v.x * v.x + v.y * v.y)
  return {
    x: v.x / d,
    y: v.y / d
  }
}

function rotateVector(v, theta) {
  return {
    x: v.x * Math.cos(theta) - v.y * Math.sin(theta),
    y: v.x * Math.sin(theta) + v.y * Math.cos(theta)
  }
}

function negateVector(v) {
  return {
    x: -v.x,
    y: -v.y
  }
}

function addVectors(v1, v2) {
  return {
    x: v1.x + v2.x,
    y: v1.y + v2.y
  }
}

/**
 * GTFS utilities
 */

function otpModeToGtfsType(otpMode) {
  switch (otpMode) {
    case 'TRAM':
      return 0
    case 'SUBWAY':
      return 1
    case 'RAIL':
      return 2
    case 'BUS':
      return 3
    case 'FERRY':
      return 4
    case 'CABLE_CAR':
      return 5
    case 'GONDOLA':
      return 6
    case 'FUNICULAR':
      return 7
  }
}

// Rendering utilities

function renderDataToSvgPath(renderData) {
  return renderData
    .map((d, k) => {
      if (k === 0) return `M${d.x} ${d.y}`
      if (d.arc) {
        return `A${d.radius} ${d.radius} ${d.arc} 0 ${d.arc > 0 ? 0 : 1} ${
          d.x
        } ${d.y}`
      }
      return `L${d.x} ${d.y}`
    })
    .join(' ')
}

// An instance of the SphericalMercator converter
const sm = new SphericalMercator()

/**
 * @param {*} fontSize A CSS font size or a numerical (pixel) font size.
 * @returns A CSS font size ending with the provided CSS unit or 'px' if none provided.
 */
function getFontSizeWithUnit(fontSize) {
  return fontSize + (isFinite(fontSize) ? 'px' : '')
}

export {
  fuzzyEquals,
  distance,
  getRadiusFromAngleChord,
  ccw,
  ccwRaw,
  angleFromThreePoints,
  pointAlongArc,
  getVectorAngle,
  rayIntersection,
  lineIntersection,
  parsePixelStyle,
  isOutwardVector,
  normalizeVector,
  rotateVector,
  negateVector,
  addVectors,
  otpModeToGtfsType,
  renderDataToSvgPath,
  sm,
  getFontSizeWithUnit
}