conveyal/modeify

View on GitHub
client/components/conveyal/transitive.js/master/lib/graph/edge.js

Summary

Maintainability
F
1 wk
Test Coverage
var each = require('each');
var PriorityQueue = require('priorityqueuejs');

var Util = require('../util');

var debug = require('debug')('transitive:edge');

/**
 * Expose `Edge`
 */

module.exports = Edge;

/**
 * Initialize a new edge
 *
 * @param {Array}
 * @param {Vertex}
 * @param {Vertex}
 */

var edgeId = 0;

function Edge(pointArray, fromVertex, toVertex) {
  this.id = edgeId++;
  this.pointArray = pointArray;
  this.fromVertex = fromVertex;
  this.toVertex = toVertex;
  this.pathSegments = [];
  this.renderedEdges = [];
}

Edge.prototype.getId = function() {
  return this.id;
};

/**
 *
 */

Edge.prototype.getLength = function() {
  var dx = this.toVertex.x - this.fromVertex.x,
    dy = this.toVertex.y - this.fromVertex.y;
  return Math.sqrt(dx * dx + dy * dy);
};

Edge.prototype.getWorldLength = function() {
  if (!this.worldLength) this.calculateWorldLengthAndMidpoint();
  return this.worldLength;
};

Edge.prototype.getWorldMidpoint = function() {
  if (!this.worldMidpoint) this.calculateWorldLengthAndMidpoint();
  return this.worldMidpoint;
};

Edge.prototype.calculateWorldLengthAndMidpoint = function() {
  var allPoints = [this.fromVertex.point].concat(this.pointArray, [this.toVertex
    .point
  ]);
  this.worldLength = 0;
  for (var i = 0; i < allPoints.length - 1; i++) {
    this.worldLength += Util.distance(allPoints[i].worldX, allPoints[i].worldY,
      allPoints[i + 1].worldX, allPoints[i + 1].worldY);
  }

  if (this.worldLength === 0) {
    this.worldMidpoint = {
      x: this.fromVertex.point.worldX,
      y: this.fromVertex.point.worldY
    };
  } else {
    var distTraversed = 0;
    for (i = 0; i < allPoints.length - 1; i++) {
      var dist = Util.distance(allPoints[i].worldX, allPoints[i].worldY,
        allPoints[i + 1].worldX, allPoints[i + 1].worldY);
      if ((distTraversed + dist) / this.worldLength >= 0.5) {
        // find the position along this segment (0 <= t <= 1) where the edge midpoint lies
        var t = (0.5 - distTraversed / this.worldLength) / (dist / this.worldLength);
        this.worldMidpoint = {
          x: allPoints[i].worldX + t * (allPoints[i + 1].worldX - allPoints[i].worldX),
          y: allPoints[i].worldY + t * (allPoints[i + 1].worldY - allPoints[i].worldY)
        };
        this.pointsBeforeMidpoint = i;
        this.pointsAfterMidpoint = this.pointArray.length - i;
        break;
      }
      distTraversed += dist;
    }
  }
};

/**
 *
 */

Edge.prototype.isAxial = function() {
  return (this.toVertex.x === this.fromVertex.x) || (this.toVertex.y === this.fromVertex
    .y);
};

/**
 *
 */

Edge.prototype.hasCurvature = function() {
  return this.elbow !== null;
};

/**
 *
 */

Edge.prototype.replaceVertex = function(oldVertex, newVertex) {
  if (oldVertex === this.fromVertex) this.fromVertex = newVertex;
  if (oldVertex === this.toVertex) this.toVertex = newVertex;
};

/**
 *  Add a path segment that traverses this edge
 */

Edge.prototype.addPathSegment = function(segment) {
  this.pathSegments.push(segment);
};

Edge.prototype.copyPathSegments = function(baseEdge) {
  each(baseEdge.pathSegments, function(pathSegment) {
    this.addPathSegment(pathSegment);
  }, this);
};

Edge.prototype.getPathSegmentIds = function(baseEdge) {
  var pathSegIds = [];
  each(this.pathSegments, function(segment) {
    pathSegIds.push(segment.id);
  });
  pathSegIds.sort();
  return pathSegIds.join(',');
};

/**
 *
 */

Edge.prototype.addRenderedEdge = function(rEdge) {
  if (this.renderedEdges.indexOf(rEdge) !== -1) return;
  this.renderedEdges.push(rEdge);
};

/** internal geometry functions **/

Edge.prototype.calculateGeometry = function(cellSize, angleConstraint) {
  //if(!this.hasTransit()) angleConstraint = 5;
  angleConstraint = angleConstraint || 45;

  this.angleConstraintR = angleConstraint * Math.PI / 180;

  this.fx = this.fromVertex.point.worldX;
  this.fy = this.fromVertex.point.worldY;
  this.tx = this.toVertex.point.worldX;
  this.ty = this.toVertex.point.worldY;

  var midpoint = this.getWorldMidpoint();

  var targetFromAngle = Util.getVectorAngle(midpoint.x - this.fx, midpoint.y -
    this.fy);
  this.constrainedFromAngle = Math.round(targetFromAngle / this.angleConstraintR) *
    this.angleConstraintR;

  var fromAngleDelta = Math.abs(this.constrainedFromAngle - targetFromAngle);
  this.fvx = Math.cos(this.constrainedFromAngle);
  this.fvy = Math.sin(this.constrainedFromAngle);

  var targetToAngle = Util.getVectorAngle(midpoint.x - this.tx, midpoint.y -
    this.ty);

  this.constrainedToAngle = Math.round(targetToAngle / this.angleConstraintR) *
    this.angleConstraintR;

  var toAngleDelta = Math.abs(this.constrainedToAngle - targetToAngle);
  this.tvx = Math.cos(this.constrainedToAngle);
  this.tvy = Math.sin(this.constrainedToAngle);

  var tol = 0.01;
  var v = Util.normalizeVector({
    x: (this.toVertex.x - this.fromVertex.x),
    y: (this.toVertex.y - this.fromVertex.y)
  });

  // check if we need to add curvature
  if (!equalVectors(this.fvx, this.fvy, -this.tvx, -this.tvy, tol) || !
    equalVectors(this.fvx, this.fvy, v.x, v.y, tol)) {

    // see if the default endpoint angles produce a valid intersection
    var isect = this.computeEndpointIntersection();

    if (isect.intersect) { // if so, compute the elbow and we're done
      this.elbow = {
        x: this.fx + isect.u * this.fvx,
        y: this.fy + isect.u * this.fvy,
      };
    } else { // if not, adjust the two endpoint angles until they properly intersect

      // default test: compare angle adjustments (if significant difference)
      if (Math.abs(fromAngleDelta - toAngleDelta) > 0.087) {
        if (fromAngleDelta < toAngleDelta) {
          this.adjustToAngle();
        } else {
          this.adjustFromAngle();
        }
      } else { // secondary test: look at distribution of shapepoints
        if (this.pointsAfterMidpoint < this.pointsBeforeMidpoint) {
          this.adjustToAngle();
        } else {
          this.adjustFromAngle();
        }
      }
    }
  }

  this.fromAngle = this.constrainedFromAngle;
  this.toAngle = this.constrainedToAngle;

  this.calculateVectors();
  this.calculateAlignmentIds();
};

/**
 *  Adjust the 'to' endpoint angle by rotating it increments of angleConstraintR
 *  until a valid intersection between the from and to endpoint rays is achieved.
 */

Edge.prototype.adjustToAngle = function() {
  var ccw = Util.ccw(this.fx, this.fy, (this.fx + this.fvx), (this.fy + this.fvy),
    this.tx, this.ty);
  var delta = (ccw > 0) ? this.angleConstraintR : -this.angleConstraintR;
  var i = 0,
    isect;
  while (i++ < 100) {
    this.constrainedToAngle += delta;
    this.tvx = Math.cos(this.constrainedToAngle);
    this.tvy = Math.sin(this.constrainedToAngle);
    isect = this.computeEndpointIntersection();
    if (isect.intersect) break;
  }
  this.elbow = {
    x: this.fx + isect.u * this.fvx,
    y: this.fy + isect.u * this.fvy,
  };
};

/**
 *  Adjust the 'from' endpoint angle by rotating it increments of angleConstraintR
 *  until a valid intersection between the from and to endpoint rays is achieved.
 */

Edge.prototype.adjustFromAngle = function() {
  var ccw = Util.ccw(this.tx, this.ty, (this.tx + this.tvx), (this.ty + this.tvy),
    this.fx, this.fy);
  var delta = (ccw > 0) ? this.angleConstraintR : -this.angleConstraintR;
  var i = 0,
    isect;
  while (i++ < 100) {
    this.constrainedFromAngle += delta;
    this.fvx = Math.cos(this.constrainedFromAngle);
    this.fvy = Math.sin(this.constrainedFromAngle);
    isect = this.computeEndpointIntersection();
    if (isect.intersect) break;
  }
  this.elbow = {
    x: this.fx + isect.u * this.fvx,
    y: this.fy + isect.u * this.fvy,
  };
};

Edge.prototype.computeEndpointIntersection = function() {
  return Util.rayIntersection(this.fx, this.fy, this.fvx, this.fvy,
    this.tx, this.ty, this.tvx, this.tvy);
};

function equalVectors(x1, y1, x2, y2, tol) {
  tol = tol || 0;
  return Math.abs(x1 - x2) < tol && Math.abs(y1 - y2) < tol;
}

Edge.prototype.calculateVectors = function(fromAngle, toAngle) {

  this.fromVector = {
    x: Math.cos(this.fromAngle),
    y: Math.sin(this.fromAngle)
  };

  this.fromleftVector = {
    x: -this.fromVector.y,
    y: this.fromVector.x
  };

  this.fromRightVector = {
    x: this.fromVector.y,
    y: -this.fromVector.x
  };

  this.toVector = {
    x: Math.cos(this.toAngle + Math.PI),
    y: Math.sin(this.toAngle + Math.PI)
  };

  this.toleftVector = {
    x: -this.toVector.y,
    y: this.toVector.x
  };

  this.toRightVector = {
    x: this.toVector.y,
    y: -this.toVector.x
  };
};

/**
 *  Compute the 'alignment id', a string that uniquely identifies a line in
 *  2D space given a point and angle relative to the x-axis.
 */

Edge.prototype.calculateAlignmentId = function(x, y, angle) {
  var angleD = Math.round(angle * 180 / Math.PI);
  if (angleD > 90) angleD -= 180;
  if (angleD <= -90) angleD += 180;

  if (angleD === 90) {
    return '90_x' + x;
  }

  // calculate the y-axis crossing
  var ya = Math.round(y - x * Math.tan(angle));
  return angleD + '_y' + ya;
};

Edge.prototype.calculateAlignmentIds = function() {
  this.fromAlignmentId = this.calculateAlignmentId(this.fromVertex.x, this.fromVertex
    .y, this.fromAngle);
  this.toAlignmentId = this.calculateAlignmentId(this.toVertex.x, this.toVertex
    .y, this.toAngle);
};

Edge.prototype.hasTransit = function(cellSize) {
  //debug(this);
  for (var i = 0; i < this.pathSegments.length; i++) {
    if (this.pathSegments[i].getType() === 'TRANSIT') {
      return true;
    }
  }
  return false;
};

Edge.prototype.getFromAlignmentId = function() {
  return this.fromAlignmentId;
};

Edge.prototype.getToAlignmentId = function() {
  return this.toAlignmentId;
};

Edge.prototype.getAlignmentRange = function(alignmentId) {

  var p1, p2;
  if (alignmentId === this.fromAlignmentId) {
    p1 = this.fromVertex;
    p2 = this.elbow || this.toVertex;
  } else if (alignmentId === this.toAlignmentId) {
    p1 = this.toVertex;
    p2 = this.elbow || this.fromVertex;
  } else {
    return null;
  }

  var min, max;
  if (alignmentId.substring(0, 2) === '90') {
    min = Math.min(p1.y, p2.y);
    max = Math.max(p1.y, p2.y);
  } else {
    min = Math.min(p1.x, p2.x);
    max = Math.max(p1.x, p2.x);
  }

  return {
    min: min,
    max: max
  };

};

Edge.prototype.align = function(vertex, vector) {
  if (this.aligned || !this.hasCurvature()) return;
  var currentVector = this.getVector(vertex);
  if (Math.abs(currentVector.x) !== Math.abs(vector.x) || Math.abs(
    currentVector.y) !== Math.abs(vector.y)) {
    this.curveAngle = -this.curveAngle;
    this.calculateGeometry();
  }
  this.aligned = true;
};

Edge.prototype.getGeometricCoords = function(fromOffsetPx, toOffsetPx, display, forward) {
  var coords = [], lastX = null, lastY = null;

  // reverse the coords array if needed
  var geomCoords = forward ? this.geomCoords : this.geomCoords.concat().reverse();

  each(geomCoords, function(coord, i) {

    var fromVector = null, toVector = null, rightVector;
    var xOffset, yOffset;
    var x1 = display.xScale(coord[0]);
    var y1 = display.yScale(coord[1]);

    // calculate the vector leading in to this coordinate
    if (i > 0) {
      var prevCoord = geomCoords[i - 1];
      var x0 = display.xScale(prevCoord[0]);
      var y0 = display.yScale(prevCoord[1]);
      if (x1 === x0 && y1 === y0) return;

      toVector = {
        x: x1 - x0,
        y: y1 - y0
      };
    }

    // calculate the vector leading out from this coordinate
    if (i < geomCoords.length - 1) {
      var nextCoord = geomCoords[i + 1];
      var x2 = display.xScale(nextCoord[0]);
      var y2 = display.yScale(nextCoord[1]);
      if(x2 === x1 && y2 === y1) return;

      fromVector = {
        x: x2 - x1,
        y: y2 - y1
      };
    }

    if(fromVector && !toVector) { // the first point in the geomCoords sequence
      rightVector = Util.normalizeVector({
        x: fromVector.y,
        y: -fromVector.x
      });
      xOffset = fromOffsetPx * rightVector.x;
      yOffset = fromOffsetPx * rightVector.y;
    }
    else if(!fromVector && toVector) { // the last point in the geomCoords sequence
      rightVector = Util.normalizeVector({
        x: toVector.y,
        y: -toVector.x
      });
      xOffset = fromOffsetPx * rightVector.x;
      yOffset = fromOffsetPx * rightVector.y;

    }
    else { // an internal point
      rightVector = Util.normalizeVector({
        x: fromVector.y,
        y: -fromVector.x
      });
      xOffset = fromOffsetPx * rightVector.x;
      yOffset = fromOffsetPx * rightVector.y;

      // TODO: properly compute the offsets based on both vectors
    }

    coords.push({
      x: x1 + xOffset,
      y: y1 + yOffset
    });
  }, this);
  return coords;
};

Edge.prototype.getRenderCoords = function(fromOffsetPx, toOffsetPx, display, forward) {
  var fromOffsetX, fromOffsetY;
  var isBase = (fromOffsetPx === 0 && toOffsetPx === 0);

  if (!this.baseRenderCoords && !isBase) {
    this.calculateBaseRenderCoords(display);
  }

  // TODO: This can't be right...
  try {
    fromOffsetX = fromOffsetPx * this.fromRightVector.x;
    fromOffsetY = fromOffsetPx * this.fromRightVector.y;
  } catch (err) {
    debug('edge err');
    debug(this);
  }
  var toOffsetX = toOffsetPx * this.toRightVector.x;
  var toOffsetY = toOffsetPx * this.toRightVector.y;

  var fx = this.fromVertex.getRenderX(display) + fromOffsetX;
  var fy = this.fromVertex.getRenderY(display) - fromOffsetY;
  var fvx = this.fromVector.x,
    fvy = -this.fromVector.y;

  var tx = this.toVertex.getRenderX(display) + toOffsetX;
  var ty = this.toVertex.getRenderY(display) - toOffsetY;
  var tvx = -this.toVector.x,
    tvy = this.toVector.y;

  var coords = [];

  // append the first ('from') coordinate
  coords.push({
    x: forward ? fx : tx,
    y: forward ? fy : ty
  });

  var len = null,
    x1, y1, x2, y2;

  // determine if this edge has an elbow, i.e. a bend in the middle
  if ((isBase && !this.isStraight()) || (!isBase && this.baseRenderCoords.length ===
    4)) {

    var isect = Util.rayIntersection(fx, fy, fvx, fvy, tx, ty, tvx, tvy);
    if (isect.intersect) {
      var u = isect.u;
      var ex = fx + fvx * u;
      var ey = fy + fvy * u;

      this.ccw = Util.ccw(fx, fy, ex, ey, tx, ty);

      // calculate the angle of the arc
      var angleR = this.getElbowAngle();

      // calculate the radius of the arc in pixels, taking offsets into consideration
      var rPx = this.getBaseRadiusPx() - this.ccw * (fromOffsetPx + toOffsetPx) /
        2;

      // calculate the distance from the elbow to place the arc endpoints in each direction
      var d = rPx * Math.tan(angleR / 2);

      // make sure the arc endpoint placement distance is not longer than the either of the
      // elbow-to-edge-endpoint distances
      var l1 = Util.distance(fx, fy, ex, ey),
        l2 = Util.distance(tx, ty, ex, ey);
      d = Math.min(Math.min(l1, l2), d);

      x1 = ex - this.fromVector.x * d;
      y1 = ey + this.fromVector.y * d;

      x2 = ex + this.toVector.x * d;
      y2 = ey - this.toVector.y * d;

      var radius = Util.getRadiusFromAngleChord(angleR, Util.distance(x1, y1,
        x2, y2));
      var arc = angleR * (180 / Math.PI) * (this.ccw < 0 ? 1 : -1);

      if (forward) {
        coords.push({
          x: x1,
          y: y1,
          len: Util.distance(fx, fy, x1, y1)
        });

        coords.push({
          x: x2,
          y: y2,
          len: angleR * radius,
          arc: arc,
          radius: radius
        });

        len = Util.distance(x2, y2, tx, ty);
      } else {
        coords.push({
          x: x2,
          y: y2,
          len: Util.distance(tx, ty, x2, y2)
        });

        coords.push({
          x: x1,
          y: y1,
          len: angleR * radius,
          arc: -arc,
          radius: radius
        });

        len = Util.distance(x1, y1, fx, fy);
      }

    } else if (!isBase) {
      var flen = this.baseRenderCoords[1].len;
      var tlen = this.baseRenderCoords[3].len;

      if (flen === 0 || tlen === 0) {
        x1 = fx + fvx * flen;
        y1 = fy + fvy * flen;
        x2 = tx + tvx * tlen;
        y2 = ty + tvy * tlen;

        coords.push({
          x: forward ? x1 : x2,
          y: forward ? y1 : y2,
          len: flen
        });

        coords.push({
          x: forward ? x2 : x1,
          y: forward ? y2 : y1,
          len: Util.distance(x1, y1, x2, y2)
        });

        len = forward ? tlen : flen;
      }
    }
  }

  // if the length wasn't calculated during elbow-creation, do it now
  if (len === null) len = Util.distance(fx, fy, tx, ty);

  // append the final ('to') coordinate
  coords.push({
    x: forward ? tx : fx,
    y: forward ? ty : fy,
    len: len
  });

  return coords;
};

Edge.prototype.calculateBaseRenderCoords = function(display, forward) {
  this.baseRenderCoords = this.getRenderCoords(0, 0, display, forward);
};

Edge.prototype.isStraight = function() {
  var tol = 0.00001;
  return (Math.abs(this.fromVector.x - this.toVector.x) < tol &&
    Math.abs(this.fromVector.y - this.toVector.y) < tol);
};

Edge.prototype.getBaseRadiusPx = function() {
  return 15;
};

Edge.prototype.getElbowAngle = function() {
  var cx = this.fromVector.x - this.toVector.x;
  var cy = this.fromVector.y - this.toVector.y;

  var c = Math.sqrt(cx * cx + cy * cy) / 2;

  var theta = Math.asin(c);

  return theta * 2;
};

Edge.prototype.getRenderLength = function(display) {

  if (!this.baseRenderCoords) this.calculateBaseRenderCoords(display);

  if (!this.renderLength) {
    this.renderLength = 0;
    for (var i = 1; i < this.baseRenderCoords.length; i++) {
      this.renderLength += this.baseRenderCoords[i].len;
    }
  }
  return this.renderLength;
};

Edge.prototype.coordAlongEdge = function(t, coords, display) {

  if (!this.baseRenderCoords) this.calculateBaseRenderCoords(display);

  if (coords.length === 2 && this.baseRenderCoords.length === 4) {
    return {
      x: coords[0].x + t * (coords[1].x - coords[0].x),
      y: coords[0].y + t * (coords[1].y - coords[0].y)
    };
  }

  /*var len = 0;
  for (var i = 1; i < this.baseRenderCoords.length; i++) {
    len += this.baseRenderCoords[i].len;
  }*/
  var len = this.getRenderLength();

  var loc = t * len;
  var cur = 0;
  for (var i = 1; i < this.baseRenderCoords.length; i++) {
    if (loc < cur + this.baseRenderCoords[i].len) {
      var t2 = (loc - cur) / this.baseRenderCoords[i].len;

      if (coords[i].arc) {

        var r = coords[i].radius;
        var theta = Math.PI * coords[i].arc / 180;
        var ccw = Util.ccw(coords[0].x, coords[0].y, coords[1].x, coords[1].y,
          coords[2].x, coords[2].y);

        return Util.pointAlongArc(coords[1].x, coords[1].y, coords[2].x, coords[
          2].y, r, theta, ccw, t2);

      } else {
        var dx = coords[i].x - coords[i - 1].x;
        var dy = coords[i].y - coords[i - 1].y;

        return {
          x: coords[i - 1].x + dx * t2,
          y: coords[i - 1].y + dy * t2
        };
      }
    }
    cur += this.baseRenderCoords[i].len;
  }

};

Edge.prototype.clearRenderData = function() {
  this.baseRenderCoords = null;
  this.renderLength = null;
};

Edge.prototype.getVector = function(vertex) {
  if (vertex === this.fromVertex) return this.fromVector;
  if (vertex === this.toVertex) return this.toVector;
};

/**
 *  Gets the vertex opposite another vertex on an edge
 */

Edge.prototype.oppositeVertex = function(vertex) {
  if (vertex === this.toVertex) return this.fromVertex;
  if (vertex === this.fromVertex) return this.toVertex;
  return null;
};

Edge.prototype.commonVertex = function(edge) {
  if (this.fromVertex === edge.fromVertex || this.fromVertex === edge.toVertex)
    return this.fromVertex;
  if (this.toVertex === edge.fromVertex || this.toVertex === edge.toVertex)
    return this.toVertex;
  return null;
};

/**
 *
 */

Edge.prototype.setPointLabelPosition = function(pos, skip) {
  if (this.fromVertex.point !== skip) this.fromVertex.point.labelPosition = pos;
  if (this.toVertex.point !== skip) this.toVertex.point.labelPosition = pos;

  this.pointArray.forEach(function(point) {
    if (point !== skip) point.labelPosition = pos;
  });
};

/**
 *  Determines if this edge is part of a standalone, non-transit path
 *  (e.g. a walk/bike/drive-only journey)
 */

Edge.prototype.isNonTransitPath = function() {
  return this.pathSegments.length === 1 && this.pathSegments[0] !== 'TRANSIT' &&
    this.pathSegments[0].path.segments.length === 1;
};

/**
 *
 */

Edge.prototype.toString = function() {
  return 'Edge ' + this.getId() + ' (' + this.fromVertex.toString() + ' to ' +
    this.toVertex.toString() + ')';
};