conveyal/modeify

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

Summary

Maintainability
D
3 days
Test Coverage
var d3 = require('d3');
var debug = require('debug')('transitive:graph');
var each = require('each');
var clone = require('clone');
var PriorityQueue = require('priorityqueuejs');

var Edge = require('./edge');
var EdgeGroup = require('./edgegroup');
var Vertex = require('./vertex');
var MultiPoint = require('../point/multipoint');
var Util = require('../util');

/**
 * Expose `NetworkGraph`
 */

module.exports = NetworkGraph;

/**
 *  An graph representing the underlying 'wireframe' network
 */

function NetworkGraph(network, vertices) {
  this.network = network;
  this.edges = [];
  this.vertices = [];

  /**
   *  Object mapping groups of edges that share the same two vertices.
   *  - Key is string of format A_B, where A and B are vertex IDs and A < B
   *  - Value is array of edges
   */
  this.edgeGroups = {};

  // Add all base vertices
  for (var i in vertices) this.addVertex(vertices[i], vertices[i].worldX,
    vertices[i].worldY);
}

/**
 * Get the bounds of the graph in the graph's internal x/y coordinate space
 *
 * @return [[left, top], [right, bottom]]
 */

NetworkGraph.prototype.bounds = function() {
  var xmax = null,
    xmin = null;
  var ymax = null,
    ymin = null;

  for (var i in this.vertices) {
    var vertex = this.vertices[i];
    xmin = xmin ? Math.min(xmin, vertex.x) : vertex.x;
    xmax = xmax ? Math.max(xmax, vertex.x) : vertex.x;
    ymin = ymin ? Math.min(ymin, vertex.y) : vertex.y;
    ymax = ymax ? Math.max(ymax, vertex.y) : vertex.y;
  }

  var maxExtent = 20037508.34;
  return [
    [xmin || -maxExtent, ymin || -maxExtent],
    [xmax || maxExtent, ymax || maxExtent]
  ];
};

/**
 * Add Vertex
 */

NetworkGraph.prototype.addVertex = function(point, x, y) {
  if (x === undefined || y === undefined) {
    var xy = Util.latLonToSphericalMercator(point.getLat(), point.getLon());
    x = xy[0];
    y = xy[1];
  }
  var vertex = new Vertex(point, x, y);
  this.vertices.push(vertex);
  return vertex;
};

/**
 * Add Edge
 */

NetworkGraph.prototype.addEdge = function(stops, from, to, segmentType) {
  if (this.vertices.indexOf(from) === -1 || this.vertices.indexOf(to) === -1) {
    debug('Error: Cannot add edge. Graph does not contain vertices.');
    return;
  }

  var edge = new Edge(stops, from, to);
  this.edges.push(edge);
  from.edges.push(edge);
  to.edges.push(edge);

  var groupKey = this.network.transitive.options.groupEdges ?
    this.getEdgeGroupKey(edge, segmentType) : edge.getId();

  if (!(groupKey in this.edgeGroups)) {
    this.edgeGroups[groupKey] = new EdgeGroup(edge.fromVertex, edge.toVertex,
      segmentType);
  }
  this.edgeGroups[groupKey].addEdge(edge);

  return edge;
};

NetworkGraph.prototype.removeEdge = function(edge) {

  // remove from the graph's edge collection
  var edgeIndex = this.edges.indexOf(edge);
  if (edgeIndex !== -1) this.edges.splice(edgeIndex, 1);

  // remove from any associated path segment edge lists
  edge.pathSegments.forEach(function(segment) {
    segment.removeEdge(edge);
  });

  // remove from the endpoint vertex incidentEdge collections
  edge.fromVertex.removeEdge(edge);
  edge.toVertex.removeEdge(edge);
};

NetworkGraph.prototype.getEdgeGroup = function(edge) {
  return this.edgeGroups[this.getEdgeGroupKey(edge)];
};

NetworkGraph.prototype.getEdgeGroupKey = function(edge, segmentType) {
  return edge.fromVertex.getId() < edge.toVertex.getId() ?
    segmentType + '_' + edge.fromVertex.getId() + '_' + edge.toVertex.getId() :
    segmentType + '_' + edge.toVertex.getId() + '_' + edge.fromVertex.getId();
};

NetworkGraph.prototype.mergeVertices = function(vertexArray) {
  var xTotal = 0,
    yTotal = 0;

  var vertexGroups = {
    'STOP': [],
    'PLACE': [],
    'TURN': [],
    'MULTI': []
  };
  vertexArray.forEach(function(vertex) {
    if (vertex.point.getType() in vertexGroups) vertexGroups[vertex.point.getType()]
      .push(vertex);
  });

  var mergePoint;

  // don't merge stops and places, or multiple places:
  if ((vertexGroups.STOP.length > 0 && vertexGroups.PLACE.length > 0) ||
    vertexGroups.PLACE.length > 1 ||
    vertexGroups.MULTI.length > 0) return;

  // if merging turns with a place, create a new merged vertex around the place
  if (vertexGroups.PLACE.length === 1 && vertexGroups.TURN.length > 0) {
    mergePoint = vertexGroups.PLACE[0].point;
  }

  // if merging turns with a single place, create a new merged vertex around the stop
  else if (vertexGroups.STOP.length === 1 && vertexGroups.TURN.length > 0) {
    mergePoint = vertexGroups.STOP[0].point;
  }

  // if merging multiple stops, create a new MultiPoint vertex
  else if (vertexGroups.STOP.length > 1) {
    mergePoint = new MultiPoint();
    each(vertexGroups.STOP, function(stopVertex) {
      mergePoint.addPoint(stopVertex.point);
    });
  }

  // if merging multiple turns
  else if (vertexGroups.TURN.length > 1) {
    mergePoint = vertexGroups.TURN[0].point;
  }

  if (!mergePoint) return;
  var mergedVertex = new Vertex(mergePoint, 0, 0);

  var origPoints = [];
  vertexArray.forEach(function(vertex) {
    xTotal += vertex.x;
    yTotal += vertex.y;

    var edges = [];
    each(vertex.edges, function(edge) {
      edges.push(edge);
    });

    each(edges, function(edge) {
      if (vertexArray.indexOf(edge.fromVertex) !== -1 && vertexArray.indexOf(
        edge.toVertex) !== -1) {
        this.removeEdge(edge);
        return;
      }
      edge.replaceVertex(vertex, mergedVertex);
      mergedVertex.addEdge(edge);
    }, this);
    var index = this.vertices.indexOf(vertex);
    if (index !== -1) this.vertices.splice(index, 1);
  }, this);

  mergedVertex.x = xTotal / vertexArray.length;
  mergedVertex.y = yTotal / vertexArray.length;
  mergedVertex.oldVertices = vertexArray;

  this.vertices.push(mergedVertex);
};

NetworkGraph.prototype.sortVertices = function() {
  this.vertices.sort(function(a, b) {
    if (a.point && a.point.getType() === 'PLACE') return -1;
    if (b.point && b.point.getType() === 'PLACE') return 1;

    if (a.point && a.point.getType() === 'MULTI') return -1;
    if (b.point && b.point.getType() === 'MULTI') return 1;

    if (a.point && a.point.getType() === 'STOP') return -1;
    if (b.point && b.point.getType() === 'STOP') return 1;
  });
};

/**
 * Get the equivalent edge
 */

NetworkGraph.prototype.getEquivalentEdge = function(pointArray, from, to) {
  for (var e = 0; e < this.edges.length; e++) {
    var edge = this.edges[e];
    if (edge.fromVertex === from && edge.toVertex === to && pointArray.length ===
      edge.pointArray.length && equal(pointArray, edge.pointArray)) {
      return edge;
    }
    if (edge.fromVertex === to && edge.toVertex === from && pointArray.length ===
      edge.pointArray.length && equal(pointArray.slice(0).reverse(), edge.pointArray)) {
      return edge;
    }
  }
};

NetworkGraph.prototype.splitEdgeAtInternalPoints = function(edge, points) {
  var subEdgePoints = [],
    newEdge, newEdgeInfoArr = [];
  var fromVertex = edge.fromVertex;
  each(edge.pointArray, function(point) {
    if (points.indexOf(point) !== -1) {
      var x = point.worldX;
      var y = point.worldY;
      var newVertex = point.graphVertex || this.addVertex(point, x, y);
      newVertex.isInternal = true;
      newEdge = this.addEdge(subEdgePoints, fromVertex, newVertex, edge.edgeGroup
        .type);
      newEdge.isInternal = true;
      newEdge.copyPathSegments(edge);
      newEdgeInfoArr.push({
        graphEdge: newEdge,
        fromVertex: fromVertex
      });
      subEdgePoints = [];
      fromVertex = newVertex;
    } else {
      subEdgePoints.push(point);
    }
  }, this);

  // create the last sub-edge
  newEdge = this.addEdge(subEdgePoints, fromVertex, edge.toVertex, edge.edgeGroup
    .type);
  newEdge.isInternal = true;
  newEdge.copyPathSegments(edge);
  newEdgeInfoArr.push({
    graphEdge: newEdge,
    fromVertex: fromVertex
  });

  // insert the new edge sequence into the affected segments
  each(edge.pathSegments, function(pathSegment) {
    var indexInSegment = pathSegment.getEdgeIndex(edge);
    var forward = pathSegment.edges[indexInSegment].forward;
    var index = pathSegment.getEdgeIndex(edge);
    each(forward ? newEdgeInfoArr : newEdgeInfoArr.reverse(), function(edgeInfo) {
      pathSegment.insertEdgeAt(index, edgeInfo.graphEdge, forward ? edgeInfo.fromVertex : edgeInfo.toVertex);
      index++;
    });
  });

  // remove the original edge from the graph
  this.removeEdge(edge);
};

/*NetworkGraph.prototype.collapseTransfers = function(threshold) {
  if(!threshold) return;
  this.edges.forEach(function(edge) {
    if (edge.getLength() > threshold ||
      edge.fromVertex.point.containsFromPoint() ||
      edge.fromVertex.point.containsToPoint() ||
      edge.toVertex.point.containsFromPoint() ||
      edge.toVertex.point.containsToPoint()) return;
    //if(edge.fromVertex.point.getType() === 'PLACE' || edge.toVertex.point.getType() === 'PLACE') return;
    var notTransit = true;
    edge.pathSegments.forEach(function(segment) {
      notTransit = notTransit && segment.type !== 'TRANSIT';
    });
    if (notTransit) {
      this.mergeVertices([edge.fromVertex, edge.toVertex]);
    }
  }, this);
};*/

NetworkGraph.prototype.pruneVertices = function() {
  each(this.vertices, function(vertex) {
    if (vertex.point.containsSegmentEndPoint()) return;

    var opposites = [];
    var pathSegmentBundles = {}; // maps pathSegment id list (string) to collection of edges (array)

    each(vertex.edges, function(edge) {
      var pathSegmentIds = edge.getPathSegmentIds();
      if (!(pathSegmentIds in pathSegmentBundles)) pathSegmentBundles[
        pathSegmentIds] = [];
      pathSegmentBundles[pathSegmentIds].push(edge);
      var opp = edge.oppositeVertex(vertex);
      if (opposites.indexOf(opp) === -1) opposites.push(opp);
    });

    if (opposites.length !== 2) return;

    each(pathSegmentBundles, function(ids) {
      var edgeArr = pathSegmentBundles[ids];
      if (edgeArr.length === 2) this.mergeEdges(edgeArr[0], edgeArr[1]);
    }, this);

  }, this);
};

NetworkGraph.prototype.mergeEdges = function(edge1, edge2) {

  // reverse edges if necessary
  if (edge1.fromVertex === edge2.toVertex) {
    this.mergeEdges(edge2, edge1);
    return;
  }

  if (edge1.toVertex !== edge2.fromVertex) return; // edges cannot be merged

  var internalPoints = edge1.pointArray.concat(edge2.pointArray);

  var newEdge = this.addEdge(internalPoints, edge1.fromVertex, edge2.toVertex,
    edge1.edgeGroup.type);
  newEdge.pathSegments = edge1.pathSegments;
  each(newEdge.pathSegments, function(segment) {
    //var i = segment.graphEdges.indexOf(edge1);
    //segment.graphEdges.splice(i, 0, newEdge);
    var i = segment.getEdgeIndex(edge1);
    segment.insertEdgeAt(i, newEdge, newEdge.fromVertex);
  });

  // if both input edges are have coordinate geometry, merge the coords arrays in the new edge
  if(edge1.geomCoords && edge2.geomCoords) {
    newEdge.geomCoords = edge1.geomCoords.concat(edge2.geomCoords.length > 0 ?
      edge2.geomCoords.slice(1) : []);
  }

  debug('merging:');
  debug(edge1);
  debug(edge2);
  this.removeEdge(edge1);
  this.removeEdge(edge2);
};

NetworkGraph.prototype.snapToGrid = function(cellSize) {
  var coincidenceMap = {};
  this.vertices.forEach(function(vertex) {
    var nx = Math.round(vertex.x / cellSize) * cellSize;
    var ny = Math.round(vertex.y / cellSize) * cellSize;
    vertex.x = nx;
    vertex.y = ny;

    var key = nx + '_' + ny;
    if (!(key in coincidenceMap)) coincidenceMap[key] = [vertex];
    else coincidenceMap[key].push(vertex);
  });

  each(coincidenceMap, function(key) {
    var vertexArr = coincidenceMap[key];
    if (vertexArr.length > 1) {
      this.mergeVertices(vertexArr);
    }
  }, this);
};

NetworkGraph.prototype.calculateGeometry = function(cellSize, angleConstraint) {
  this.edges.forEach(function(edge) {
    edge.calculateGeometry(cellSize, angleConstraint);
  });
};

NetworkGraph.prototype.resetCoordinates = function() {
  this.vertices.forEach(function(vertex) {
    vertex.x = vertex.origX;
    vertex.y = vertex.origY;
  });
};

NetworkGraph.prototype.recenter = function() {

  var xCoords = [],
    yCoords = [];
  this.vertices.forEach(function(v) {
    xCoords.push(v.x);
    yCoords.push(v.y);
  });

  var mx = d3.median(xCoords),
    my = d3.median(yCoords);

  this.vertices.forEach(function(v) {
    v.x = v.x - mx;
    v.y = v.y - my;
  });
};

NetworkGraph.prototype.clone = function() {
  var vertices = [];
  this.vertices.forEach(function(vertex) {
    vertices.push(vertex.clone());
  });

  var edges = [];
  this.edges.forEach(function(edge) {
    edge.push(edge.clone());
  });
};

/** 2D line bundling & offsetting **/

NetworkGraph.prototype.apply2DOffsets = function() {

  this.initComparisons();

  var alignmentBundles = {}; // maps alignment ID to array of range-bounded bundles on that alignment

  var addToBundle = function(rEdge, alignmentId) {
    var bundle;

    // compute the alignment range of the edge being bundled
    var range = rEdge.graphEdge.getAlignmentRange(alignmentId);

    // check if bundles already exist for this alignment
    if (!(alignmentId in alignmentBundles)) { // if not, create new and add to collection
      bundle = new AlignmentBundle();
      bundle.addEdge(rEdge, range.min, range.max);
      alignmentBundles[alignmentId] = [bundle]; // new AlignmentBundle();
    } else { // 1 or more bundles currently exist for this alignmentId
      var bundleArr = alignmentBundles[alignmentId];

      // see if the segment range overlaps with that of an existing bundle
      for (var i = 0; i < bundleArr.length; i++) {
        if (bundleArr[i].rangeOverlaps(range.min, range.max)) {
          bundleArr[i].addEdge(rEdge, range.min, range.max);
          return;
        }
      }

      // ..if not, create a new bundle
      bundle = new AlignmentBundle();
      bundle.addEdge(rEdge, range.min, range.max);
      bundleArr.push(bundle);
    }
  };

  each(this.edges, function(edge) {

    var fromAlignmentId = edge.getFromAlignmentId();
    var toAlignmentId = edge.getToAlignmentId();

    each(edge.renderedEdges, function(rEdge) {
      addToBundle(rEdge, fromAlignmentId);
      addToBundle(rEdge, toAlignmentId);
    });
  });

  var bundleSorter = (function(a, b) {

    var aId = a.patternIds || a.pathSegmentIds;
    var bId = b.patternIds || b.pathSegmentIds;

    var aVector = a.getAlignmentVector(this.currentAlignmentId);
    var bVector = b.getAlignmentVector(this.currentAlignmentId);
    var isOutward = (Util.isOutwardVector(aVector) && Util.isOutwardVector(
      bVector)) ? 1 : -1;

    var abCompId = aId + '_' + bId;
    if (abCompId in this.bundleComparisons) {
      return isOutward * this.bundleComparisons[abCompId];
    }

    var baCompId = bId + '_' + aId;
    if (baCompId in this.bundleComparisons) {
      return isOutward * this.bundleComparisons[baCompId];
    }

    if (a.route && b.route && a.route.route_type !== b.route.route_type) {
      return a.route.route_type > b.route.route_type ? 1 : -1;
    }

    var isForward = (a.forward && b.forward) ? 1 : -1;
    return isForward * isOutward * (aId < bId ? -1 : 1);
  }).bind(this);

  each(alignmentBundles, function(alignmentId) {
    var bundleArr = alignmentBundles[alignmentId];
    each(bundleArr, function(bundle) {
      if (bundle.items.length <= 1) return;
      var lw = 1.2;
      var bundleWidth = lw * (bundle.items.length - 1);

      this.currentAlignmentId = alignmentId;
      bundle.items.sort(bundleSorter);
      each(bundle.items, function(rEdge, i) {
        var offset = (-bundleWidth / 2) + i * lw;
        if (rEdge.getType() === 'TRANSIT') {
          each(rEdge.patterns, function(pattern) {
            pattern.offsetAlignment(alignmentId, offset);
          });
        } else rEdge.offsetAlignment(alignmentId, offset);
      });
    }, this);
  }, this);
};

/**
 * Traverses the graph vertex-by-vertex, creating comparisons between all pairs of
 * edges for which a topological relationship can be established.
 */

NetworkGraph.prototype.initComparisons = function() {

  this.bundleComparisons = {};

  each(this.vertices, function(vertex) {
    var incidentGraphEdges = vertex.incidentEdges();

    var angleREdges = {};
    each(incidentGraphEdges, function(incidentGraphEdge) {
      var angle = (incidentGraphEdge.fromVertex === vertex) ? incidentGraphEdge.fromAngle : incidentGraphEdge.toAngle;
      var angleDeg = 180 * angle / Math.PI;
      if (!(angleDeg in angleREdges)) angleREdges[angleDeg] = [];
      angleREdges[angleDeg] = angleREdges[angleDeg].concat(incidentGraphEdge.renderedEdges);
    });

    each(angleREdges, function(angle) {
      var rEdges = angleREdges[angle];
      if (rEdges.length < 2) return;
      for (var i = 0; i < rEdges.length - 1; i++) {
        for (var j = i + 1; j < rEdges.length; j++) {
          var re1 = rEdges[i],
            re2 = rEdges[j];

          var opp1 = re1.graphEdge.oppositeVertex(vertex);
          var opp2 = re2.graphEdge.oppositeVertex(vertex);

          var ccw = Util.ccw(opp1.x, opp1.y, vertex.x, vertex.y, opp2.x,
            opp2.y);

          if (ccw === 0) {
            var s1Ext = re1.findExtension(opp1);
            var s2Ext = re2.findExtension(opp2);
            if (s1Ext) opp1 = s1Ext.graphEdge.oppositeVertex(opp1);
            if (s2Ext) opp2 = s2Ext.graphEdge.oppositeVertex(opp2);
            ccw = Util.ccw(opp1.x, opp1.y, vertex.x, vertex.y, opp2.x, opp2
              .y);
          }

          ccw = getInverse(re1, re2, vertex) * ccw;

          if (ccw > 0) {
            // e1 patterns are 'less' than e2 patterns
            this.storeComparison(re1, re2);
          }

          if (ccw < 0) {
            // e2 patterns are 'less' than e2 patterns
            this.storeComparison(re2, re1);
          }

        }
      }
    }, this);
  }, this);
};

function getInverse(s1, s2, vertex) {
  return ((s1.graphEdge.toVertex === vertex && s2.graphEdge.toVertex === vertex) ||
      (s1.graphEdge.toVertex === vertex && s2.graphEdge.fromVertex === vertex)) ?
    -1 : 1;
}

NetworkGraph.prototype.storeComparison = function(s1, s2) {
  var s1Id = s1.patternIds || s1.pathSegmentIds;
  var s2Id = s2.patternIds || s2.pathSegmentIds;
  debug('storing comparison: ' + s1Id + ' < ' + s2Id);
  this.bundleComparisons[s1Id + '_' + s2Id] = -1;
  this.bundleComparisons[s2Id + '_' + s1Id] = 1;
};

/**
 *  AlignmentBundle class
 */

function AlignmentBundle() {
  this.items = []; // RenderedEdges
  this.min = Number.MAX_VALUE;
  this.max = -Number.MAX_VALUE;
}

AlignmentBundle.prototype.addEdge = function(rEdge, min, max) {

  if (this.items.indexOf(rEdge) === -1) {
    this.items.push(rEdge);
  }

  this.min = Math.min(this.min, min);
  this.max = Math.max(this.max, max);
};

AlignmentBundle.prototype.rangeOverlaps = function(min, max) {
  return this.min < max && min < this.max;
};

/** other helper functions **/

function getAlignmentVector(alignmentId) {
  if (alignmentId.charAt(0) === 'x') return {
    x: 0,
    y: 1
  };
  if (alignmentId.charAt(0) === 'y') return {
    x: 1,
    y: 0
  };
}

function getOutVector(edge, vertex) {

  if (edge.fromVertex === vertex) {
    return edge.fromVector;
  }
  if (edge.toVertex === vertex) {
    var v = {
      x: -edge.toVector.x,
      y: -edge.toVector.y,
    };
    return v;
  }

  debug('Warning: getOutVector() called on invalid edge / vertex pair');
  debug(' - Edge: ' + edge.toString());
  debug(' - Vertex: ' + vertex.toString());
}

/**
 * Check if arrays are equal
 */

function equal(a, b) {
  if (a.length !== b.length) {
    return false;
  }

  for (var i in a) {
    if (a[i] !== b[i]) {
      return false;
    }
  }

  return true;
}