conveyal/modeify

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

Summary

Maintainability
F
4 days
Test Coverage
/**
 * Dependencies
 */

var augment = require('augment');
var each = require('each');
var d3 = require('d3');

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

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

/**
 * Labeler object
 */

var Labeler = augment(Object, function() {

  this.constructor = function(transitive) {

    this.transitive = transitive;
    this.clear();
  };

  this.clear = function(transitive) {

    this.points = [];
  };

  this.updateLabelList = function(graph) {

    this.points = [];
    graph.vertices.forEach(function(vertex) {
      var point = vertex.point;
      if (point.getType() === 'PLACE' || point.getType() === 'MULTI' || (
        point.getType() === 'STOP' && point.isSegmentEndPoint)) {
        this.points.push(point);
      }
    }, this);

    this.points.sort(function compare(a, b) {
      if (a.containsFromPoint() || a.containsToPoint()) return -1;
      if (b.containsFromPoint() || b.containsToPoint()) return 1;
      return 0;
    });
  };

  this.updateQuadtree = function() {

    this.quadtree = d3.geom.quadtree().extent([
      [-this.width, -this.height],
      [this.width * 2, this.height * 2]
    ])([]);

    this.addPointsToQuadtree();
    //this.addSegmentsToQuadtree();
  };

  this.addPointsToQuadtree = function() {

    this.points.forEach(function(point) {
      var mbbox = point.getMarkerBBox();
      if (mbbox) this.addBBoxToQuadtree(point.getMarkerBBox());
    }, this);
  };

  this.addSegmentsToQuadtree = function() {

    var disp = this.transitive.display;
    this.transitive.renderSegments.forEach(function(segment) {

      if (segment.getType() !== 'TRANSIT') return;

      var lw = this.transitive.style.compute(this.transitive.style.segments[
        'stroke-width'], this.transitive.display, segment);
      lw = parseFloat(lw.substring(0, lw.length - 2), 10) - 2;

      var x, x1, x2, y, y1, y2;
      //debug(segment.toString());
      if (segment.renderData.length === 2) { // basic straight segment
        if (segment.renderData[0].x === segment.renderData[1].x) { // vertical
          x = segment.renderData[0].x - lw / 2;
          y1 = segment.renderData[0].y;
          y2 = segment.renderData[1].y;
          this.addBBoxToQuadtree({
            x: x,
            y: Math.min(y1, y2),
            width: lw,
            height: Math.abs(y1 - y2)
          });
        } else if (segment.renderData[0].y === segment.renderData[1].y) { // horizontal
          x1 = segment.renderData[0].x;
          x2 = segment.renderData[1].x;
          y = segment.renderData[0].y - lw / 2;
          this.addBBoxToQuadtree({
            x: Math.min(x1, x2),
            y: y,
            width: Math.abs(x1 - x2),
            height: lw
          });
        }
      }

      if (segment.renderData.length === 4) { // basic curved segment

        if (segment.renderData[0].x === segment.renderData[1].x) { // vertical first
          x = segment.renderData[0].x - lw / 2;
          y1 = segment.renderData[0].y;
          y2 = segment.renderData[3].y;
          this.addBBoxToQuadtree({
            x: x,
            y: Math.min(y1, y2),
            width: lw,
            height: Math.abs(y1 - y2)
          });

          x1 = segment.renderData[0].x;
          x2 = segment.renderData[3].x;
          y = segment.renderData[3].y - lw / 2;
          this.addBBoxToQuadtree({
            x: Math.min(x1, x2),
            y: y,
            width: Math.abs(x1 - x2),
            height: lw
          });

        } else if (segment.renderData[0].y === segment.renderData[1].y) { // horiz first
          x1 = segment.renderData[0].x;
          x2 = segment.renderData[3].x;
          y = segment.renderData[0].y - lw / 2;
          this.addBBoxToQuadtree({
            x: Math.min(x1, x2),
            y: y,
            width: Math.abs(x1 - x2),
            height: lw
          });

          x = segment.renderData[3].x - lw / 2;
          y1 = segment.renderData[0].y;
          y2 = segment.renderData[3].y;
          this.addBBoxToQuadtree({
            x: x,
            y: Math.min(y1, y2),
            width: lw,
            height: Math.abs(y1 - y2)
          });
        }
      }

    }, this);
  };

  this.addBBoxToQuadtree = function(bbox) {

    if (bbox.x + bbox.width / 2 < 0 || bbox.x - bbox.width / 2 > this.width ||
      bbox.y + bbox.height / 2 < 0 || bbox.y - bbox.height / 2 > this.height
    ) return;

    this.quadtree.add([bbox.x + bbox.width / 2, bbox.y + bbox.height / 2,
      bbox
    ]);

    this.maxBBoxWidth = Math.max(this.maxBBoxWidth, bbox.width);
    this.maxBBoxHeight = Math.max(this.maxBBoxHeight, bbox.height);
  };

  this.doLayout = function() {

    this.width = this.transitive.el.clientWidth;
    this.height = this.transitive.el.clientHeight;

    this.maxBBoxWidth = 0;
    this.maxBBoxHeight = 0;

    this.updateQuadtree();

    var labeledSegments = this.placeSegmentLabels();
    var labeledPoints = this.placePointLabels();

    return {
      segments: labeledSegments,
      points: labeledPoints
    };
  };

  this.placeSegmentLabels = function() {

    each(this.segmentLabels, function(label) {
      label.clear();
    });
    this.segmentLabels = [];
    this.placedLabelKeys = [];

    // collect the bus RenderSegments
    var busRSegments = [];
    each(this.transitive.network.paths, function(path) {
      each(path.getRenderedSegments(), function(rSegment) {
        if (rSegment.type === 'TRANSIT' && rSegment.mode === 3)
          busRSegments.push(rSegment);
      });
    }, this);

    var edgeGroups = [];
    each(this.transitive.network.paths, function(path) {
      each(path.segments, function(segment) {
        if (segment.type === 'TRANSIT' && segment.getMode() === 3) {
          edgeGroups = edgeGroups.concat(segment.getLabelEdgeGroups());
        }
      });
    }, this);

    // iterate through the sequence collection, labeling as necessary
    //each(busRSegments, function(rSegment) {
    each(edgeGroups, function(edgeGroup) {

      this.currentGroup = edgeGroup;
      // get the array of label strings to be places (typically the unique route short names)
      this.labelTextArray = edgeGroup.getLabelTextArray();

      // create the initial label for placement
      this.labelTextIndex = 0;

      var label = this.getNextLabel(); //this.constructSegmentLabel(rSegment, labelTextArray[labelTextIndex]);
      if (!label) return;

      // iterate through potential anchor locations, attempting placement at each one
      var labelAnchors = edgeGroup.getLabelAnchors(this.transitive.display,
        label.textHeight * 1.5);
      for (var i = 0; i < labelAnchors.length; i++) {
        label.labelAnchor = labelAnchors[i];

        // do not consider this anchor if it is out of the display range
        if (!this.transitive.display.isInRange(label.labelAnchor.x,
          label.labelAnchor.y)) continue;

        // check for conflicts with existing placed elements
        var bbox = label.getBBox();
        var conflicts = this.findOverlaps(label, bbox);

        if (conflicts.length === 0) { // if no conflicts

          // place the current label
          this.segmentLabels.push(label);
          this.quadtree.add([label.labelAnchor.x, label.labelAnchor.y,
            label
          ]);
          //debug('placing seg label for ' + label.labelText);

          label = this.getNextLabel();
          if (!label) break;

        }
      } // end of anchor iteration loop
    }, this); // end of sequence iteration loop
  };

  this.getNextLabel = function() {
    while (this.labelTextIndex < this.labelTextArray.length) {
      var labelText = this.labelTextArray[this.labelTextIndex];
      var key = this.currentGroup.edgeIds + '_' + labelText;
      if (this.placedLabelKeys.indexOf(key) !== -1) {
        this.labelTextIndex++;
        continue;
      }
      var label = this.constructSegmentLabel(this.currentGroup.renderedSegment,
        labelText);
      this.placedLabelKeys.push(key);
      this.labelTextIndex++;
      return label;
    }
    return null;
  };

  this.constructSegmentLabel = function(segment, labelText) {
    var label = new SegmentLabel(segment, labelText);
    var styler = this.transitive.styler;
    label.fontFamily = styler.compute(styler.labels[
      'font-family'], this.transitive.display, {
      segment: segment
    });
    label.fontSize = styler.compute(styler.labels['font-size'],
      this.transitive.display, {
        segment: segment
      });
    var textBBox = Util.getTextBBox(labelText, {
      'font-size': label.fontSize,
      'font-family': label.fontFamily,
    });
    label.textWidth = textBBox.width;
    label.textHeight = textBBox.height;
    label.computeContainerDimensions();

    return label;
  };

  this.placePointLabels = function() {

    var styler = this.transitive.styler;

    var labeledPoints = [];

    this.points.forEach(function(point) {

      var labelText = point.label.getText();
      point.label.fontFamily = styler.compute(styler.labels['font-family'],
        this.transitive
        .display, {
          point: point
        });
      point.label.fontSize = styler.compute(styler.labels['font-size'],
        this.transitive
        .display, {
          point: point
        });
      var textBBox = Util.getTextBBox(labelText, {
        'font-size': point.label.fontSize,
        'font-family': point.label.fontFamily,
      });
      point.label.textWidth = textBBox.width;
      point.label.textHeight = textBBox.height;

      var orientations = styler.compute(styler.labels.orientations, this.transitive
        .display, {
          point: point
        });

      var placedLabel = false;
      for (var i = 0; i < orientations.length; i++) {

        point.label.setOrientation(orientations[i]);
        if (!point.focused) continue;

        if (!point.label.labelAnchor) continue;

        var lx = point.label.labelAnchor.x,
          ly = point.label.labelAnchor.y;

        // do not place label if out of range
        if (lx <= 0 || ly <= 0 || lx >= this.width || ly > this.height)
          continue;

        var labelBBox = point.label.getBBox();

        var overlaps = this.findOverlaps(point.label, labelBBox);

        // do not place label if it overlaps with others
        if (overlaps.length > 0) continue;

        // if we reach this point, the label is good to place

        point.label.setVisibility(true);
        labeledPoints.push(point);

        this.quadtree.add([labelBBox.x + labelBBox.width / 2, labelBBox.y +
          labelBBox.height / 2, point.label
        ]);

        this.maxBBoxWidth = Math.max(this.maxBBoxWidth, labelBBox.width);
        this.maxBBoxHeight = Math.max(this.maxBBoxHeight, labelBBox.height);

        placedLabel = true;
        break; // do not consider any other orientations after places

      } // end of orientation loop

      // if label not placed at all, hide the element
      if (!placedLabel) {
        point.label.setVisibility(false);
      }

    }, this);
    return labeledPoints;
  };

  this.findOverlaps = function(label, labelBBox) {
    var minX = labelBBox.x - this.maxBBoxWidth / 2;
    var minY = labelBBox.y - this.maxBBoxHeight / 2;
    var maxX = labelBBox.x + labelBBox.width + this.maxBBoxWidth / 2;
    var maxY = labelBBox.y + labelBBox.height + this.maxBBoxHeight / 2;
    //debug('findOverlaps %s,%s %s,%s', minX,minY,maxX,maxY);

    var matchItems = [];
    this.quadtree.visit(function(node, x1, y1, x2, y2) {
      var p = node.point;
      if ((p) && (p[0] >= minX) && (p[0] < maxX) && (p[1] >= minY) && (p[
        1] < maxY) && label.intersects(p[2])) {
        matchItems.push(p[2]);
      }
      return x1 > maxX || y1 > maxY || x2 < minX || y2 < minY;
    });
    return matchItems;
  };

  this.findNearbySegmentLabels = function(label, x, y, buffer) {
    var minX = x - buffer;
    var minY = y - buffer;
    var maxX = x + buffer;
    var maxY = y + buffer;
    //debug('findNearby %s,%s %s,%s', minX,minY,maxX,maxY);

    var matchItems = [];
    this.quadtree.visit(function(node, x1, y1, x2, y2) {
      var p = node.point;
      if ((p) && (p[0] >= minX) && (p[0] < maxX) && (p[1] >= minY) && (p[
        1] < maxY) && (p[2].parent) && (label.parent.patternIds === p[2]
        .parent.patternIds)) {
        matchItems.push(p[2]);
      }
      return x1 > maxX || y1 > maxY || x2 < minX || y2 < minY;
    });
    return matchItems;
  };

});

/**
 * Expose `Labeler`
 */

module.exports = Labeler;