Esempio n. 1
0
var Point = augment(Object, function () {

  this.constructor = function(data) {
    for (var key in data) {
      this[key] = data[key];
    }

    this.paths = [];
    this.renderData = [];

    this.label = new PointLabel(this);
    this.renderLabel = true;

    this.focused = true;
    this.sortableType = 'POINT';
  };


  /**
   * Get unique ID for point -- must be defined by subclass
   */

  this.getId = function() { };

  this.getElementId = function() {
    return this.getType().toLowerCase() + '-' + this.getId();
  };


  /**
   * Get Point type -- must be defined by subclass
   */

  this.getType = function() { };


  /**
   * Get Point name
   */

  this.getName = function() {
    return this.getType() + ' point (ID=' + this.getId() + ')';
  };


  /**
   * Get latitude
   */

  this.getLat = function() {
    return 0;
  };


  /**
   * Get longitude
   */

  this.getLon = function() {
    return 0;
  };


  this.containsSegmentEndPoint = function() {
    return false;
  };


  this.containsBoardPoint = function() {
    return false;
  };


  this.containsAlightPoint = function() {
    return false;
  };


  this.containsTransferPoint = function() {
    return false;
  };


  this.getPatterns = function() {
    return [];
  };


  /**
   * Draw the point
   *
   * @param {Display} display
   */

  this.render = function(display) {
    this.label.svgGroup = null;
  };


  /**
   * Refresh a previously drawn point
   *
   * @param {Display} display
   */

  this.refresh = function(display) { };

  this.clearRenderData = function() { };

  this.containsFromPoint = function() {
    return false;
  };

  this.containsToPoint = function() {
    return false;
  };

  this.initSvg = function(display) {
    // set up the main svg group for this stop
    this.svgGroup = display.svg.append('g')
      .attr('id', 'transitive-' + this.getType().toLowerCase() + '-' + this.getId())
      //.attr('class', 'transitive-sortable')
      .datum(this);

    this.markerSvg = this.svgGroup.append('g');
    this.labelSvg = this.svgGroup.append('g');
  };

  //** Shared geom utility functions **//

  this.constructMergedMarker = function(display, patternStylerKey) {


    var markerType = display.styler.compute(display.styler.stops_merged['marker-type'], display, { owner : this });
    var markerPadding = display.styler.compute(display.styler.stops_merged['marker-padding'], display, { owner : this }) || 0;

    var dataArray = this.getRenderDataArray();

    var xValues = [], yValues = [];
    dataArray.forEach(function(data) {
      var x = display.xScale(data.x) + data.offsetX;
      var y = display.yScale(data.y) - data.offsetY;
      xValues.push(x);
      yValues.push(y);
    });
    var minX = Math.min.apply(Math, xValues), minY = Math.min.apply(Math, yValues);
    var maxX = Math.max.apply(Math, xValues), maxY = Math.max.apply(Math, yValues);
    var dx = maxX - minX, dy = maxY - minY;

    var width, height;
    var patternRadius = display.styler.compute(display.styler[patternStylerKey].r, display, { owner: this });
    var r = parseFloat(patternRadius) + markerPadding;
    
    if(markerType === 'circle') {
      width = height = Math.max(dx, dy) + 2 * r;
      r = width/2;
    }
    else {
      width = dx + 2 * r;
      height = dy + 2 * r;
      if(markerType === 'rectangle') r = 0;
    }

    return {
      x: (minX+maxX)/2 - width/2,
      y: (minY+maxY)/2 - height/2,
      width: width,
      height: height,
      rx: r,
      ry: r
    };

  };


  this.refreshLabel = function(display) {
    if(!this.renderLabel) return;
    this.label.refresh(display);
  };


  this.getMarkerBBox = function() {
    //console.log(this.markerSvg.node());
    return this.markerSvg.node().getBBox();
  };


  this.setFocused = function(focused) {
    this.focused = focused;
  };


  this.isFocused = function() {
    return (this.focused === true);
  };


  this.getZIndex = function() {
    return 10000;
  };

});
Esempio n. 2
0
var Stop = augment(Point, function(base) {

  this.constructor =  function(data) {
    base.constructor.call(this, data);

    this.patterns = [];

    this.patternRenderData = {};
    this.patternFocused = {};
    this.patternCount = 0;
  };

  /**
   * Get id
   */

  this.getId = function() {
    return this.stop_id;
  };

  /**
   * Get type
   */

  this.getType = function() {
    return 'STOP';
  };


  /**
   * Get name
   */

  this.getName = function() {
    return this.stop_name.replace('METRO STATION', '');
  };


  /**
   * Get lat
   */

  this.getLat = function() {
    return this.stop_lat;
  };


  /**
   * Get lon
   */

  this.getLon = function() {
    return this.stop_lon;
  };


  this.containsSegmentEndPoint = function() {
    return this.isSegmentEndPoint;
  };


  this.containsBoardPoint = function() {
    return this.isBoardPoint;
  };


  this.containsAlightPoint = function() {
    return this.isAlightPoint;
  };


  this.containsTransferPoint = function() {
    return this.isTransferPoint;
  };


  this.getPatterns = function() {
    return this.patterns;
  };


  this.addPattern = function(pattern) {
    if(this.patterns.indexOf(pattern) === -1) this.patterns.push(pattern);
  };

  /**
   * Add render data
   *
   * @param {Object} stopInfo
   */

  this.addRenderData = function(stopInfo) {
    if(stopInfo.segment.getType() === 'TRANSIT') {

      var s = {
        sortableType : 'POINT_STOP_PATTERN',
        owner : this,
        getZIndex : function() {
          return this.segment.getZIndex() + 1;
        }
      };

      for(var key in stopInfo)
        s[key] = stopInfo[key];

      var patternId = stopInfo.segment.pattern.pattern_id;
      if(!(patternId in this.patternRenderData)) this.patternRenderData[patternId] = {};
      this.patternRenderData[patternId][stopInfo.segment.getId()] = s; //.push(s);
      this.addPattern(stopInfo.segment.pattern);
      //console.log('added to '+ this.getName());
      //console.log(stopInfo);
    }
    this.patternCount = Object.keys(this.patternRenderData).length;
  };


  this.isPatternFocused = function(patternId) {
    if(!(patternId in this.patternFocused)) return true;
    return(this.patternFocused[patternId]);
  };

  this.setPatternFocused = function(patternId, focused) {
    this.patternFocused[patternId] = focused;
  };


  this.setAllPatternsFocused = function(focused) {
    for(var key in this.patternRenderData) {
      this.patternFocused[key] = focused;
    }
  };


  /**
   * Draw a stop
   *
   * @param {Display} display
   */

  this.render = function(display) {
    base.render.call(this, display);
    if(Object.keys(this.patternRenderData).length === 0) return;
    //if (this.renderData.length === 0) return;

    var renderDataArray = this.getRenderDataArray();

    this.initSvg(display);

    // set up the merged marker
    this.mergedMarker = this.markerSvg.append('g').append('rect')
      .attr('class', 'transitive-sortable transitive-stop-marker-merged')
      .datum(this.getMergedRenderData());

    // set up the pattern-specific markers
    this.patternMarkers = this.markerSvg.append('g').selectAll('circle')
      .data(renderDataArray)
      .enter()
      .append('circle')
      .attr('class', 'transitive-sortable transitive-stop-marker-pattern');

  };

  /**
   * Refresh the stop
   *
   * @param {Display} display
   */

  this.refresh = function(display) {

    if(this.patternCount === 0) return;

    // refresh the pattern-level markers
    this.patternMarkers.data(this.getRenderDataArray());
    this.patternMarkers.attr('transform', function (d, i) {
      var x = d.x; //display.xScale(d.x) + d.offsetX;
      var y = d.y; //display.yScale(d.y) - d.offsetY;
      return 'translate(' + x +', ' + y +')';
    });

    // refresh the merged marker
    if(this.mergedMarker) {
      var a = this.constructMergedMarker(display, 'stops_pattern');
      this.mergedMarker.datum(this.getMergedRenderData());
      this.mergedMarker.attr(a);
    }

  };

  this.getMergedRenderData = function() {
    return {
      owner: this,
      sortableType : 'POINT_STOP_MERGED'
    };
  };

  this.getRenderDataArray = function() {
    var dataArray = [];
    for(var patternId in this.patternRenderData) {
      var segmentData = this.patternRenderData[patternId];
      for(var segmentId in segmentData) {
        dataArray.push(segmentData[segmentId]);
      }
    }
    return dataArray;
  };

  this.getMarkerBBox = function() {
    //console.log('gMBB ' + this.getName());
    //console.log(this);
    if(this.mergedMarker) return this.mergedMarker.node().getBBox();
    console.log(this.patternMarkers[0]);
    return this.patternMarkers.node().getBBox();
  };

  this.isFocused = function() {
    if(this.mergedMarker || !this.patternRenderData) return (this.focused === true);

    var focused = true;
    for(var patternId in this.patternRenderData) {
      focused = this && this.isPatternFocused(patternId);
    }
    return focused;
  };

  this.clearRenderData = function() {
    this.patternRenderData = {};
  };


});
Esempio n. 3
0
var DefaultRenderer = augment(Renderer, function(base) {

  this.constructor = function(transitive) {
    base.constructor.call(this, transitive);
  };

  this.render = function() {
    base.render.call(this);

    var self = this;
    var display = this.transitive.display;
    var network = this.transitive.network;
    var options = this.transitive.options;
    display.styler = this.transitive.styler;

    var legendSegments = {};

    each(network.renderedEdges, function(rEdge) {
      rEdge.refreshRenderData(display);
    });

    each(network.paths, function(path) {
      each(path.segments, function(pathSegment) {
        each(pathSegment.renderedSegments, function(renderedSegment) {
          renderedSegment.render(display);
          var legendType = renderedSegment.getLegendType();
          if (!(legendType in legendSegments)) {
            legendSegments[legendType] = renderedSegment;
          }
        });
      });
    });

    // draw the vertex-based points

    each(network.graph.vertices, function(vertex) {
      vertex.point.render(display);
      if (self.isDraggable(vertex.point)) {
        vertex.point.makeDraggable(self.transitive);
      }
    });

    // draw the edge-based points
    each(network.graph.edges, function(edge) {
      edge.pointArray.forEach(function(point) {
        point.render(display);
      });
    });

    if (display.legend) display.legend.render(legendSegments);

    this.transitive.refresh();
  };

  /**
   * Refresh
   */

  this.refresh = function(panning) {
    base.refresh.call(this, panning);

    var display = this.transitive.display;
    var network = this.transitive.network;
    var options = this.transitive.options;
    var styler = this.transitive.styler;

    network.graph.vertices.forEach(function(vertex) {
      vertex.point.clearRenderData();
    });
    network.graph.edges.forEach(function(edge) {
      edge.clearRenderData();
    });

    // refresh the segment and point marker data
    this.refreshSegmentRenderData();
    network.graph.vertices.forEach(function(vertex) {
      vertex.point.initMarkerData(display);
    });

    this.renderedSegments = [];
    each(network.paths, function(path) {
      each(path.segments, function(pathSegment) {
        each(pathSegment.renderedSegments, function(rSegment) {
          rSegment.refresh(display);
          this.renderedSegments.push(rSegment);
        }, this);
      }, this);
    }, this);

    network.graph.vertices.forEach(function(vertex) {
      var point = vertex.point;
      if (!point.svgGroup) return; // check if this point is not currently rendered
      styler.stylePoint(display, point);
      point.refresh(display);
    });

    // re-draw the edge-based points
    network.graph.edges.forEach(function(edge) {
      edge.pointArray.forEach(function(point) {
        if (!point.svgGroup) return; // check if this point is not currently rendered
        styler.styleStop(display, point);
        point.refresh(display);
      });
    });

    // refresh the label layout
    var labeledElements = this.transitive.labeler.doLayout();
    labeledElements.points.forEach(function(point) {
      point.refreshLabel(display);
      styler.stylePointLabel(display, point);
    });
    each(this.transitive.labeler.segmentLabels, function(label) {
      label.refresh(display);
      styler.styleSegmentLabel(display, label);
    });

    this.sortElements();

  };

  this.refreshSegmentRenderData = function() {
    each(this.transitive.network.renderedEdges, function(rEdge) {
      rEdge.refreshRenderData(this.transitive.display);
    }, this);

    // try intersecting adjacent rendered edges to create a smooth transition

    var isectKeys = []; // keep track of edge-edge intersections we've already computed
    each(this.transitive.network.paths, function(path) {
      each(path.segments, function(pathSegment) {
        each(pathSegment.renderedSegments, function(rSegment) {
          for (var s = 0; s < rSegment.renderedEdges.length - 1; s++) {
            var rEdge1 = rSegment.renderedEdges[s];
            var rEdge2 = rSegment.renderedEdges[s + 1];
            var key = rEdge1.getId() + '_' + rEdge2.getId();
            if (isectKeys.indexOf(key) !== -1) continue;
            if (rEdge1.graphEdge.isInternal && rEdge2.graphEdge.isInternal) {
              rEdge1.intersect(rEdge2);
            }
            isectKeys.push(key);
          }
        });
      });
    });
  };

  /**
   * sortElements
   */

  this.sortElements = function() {

    this.renderedSegments.sort(function(a, b) {
      return (a.compareTo(b));
    });

    var focusBaseZIndex = 100000;

    this.renderedSegments.forEach(function(rSegment, index) {
      rSegment.zIndex = index * 10 + (rSegment.isFocused() ? focusBaseZIndex :
        0);
    });

    this.transitive.network.graph.vertices.forEach(function(vertex) {
      var point = vertex.point;
      point.zIndex = point.zIndex + (point.isFocused() ? focusBaseZIndex : 0);
    });

    this.transitive.display.svg.selectAll('.transitive-sortable').sort(function(a, b) {
      var aIndex = (typeof a.getZIndex === 'function') ? a.getZIndex() : a.owner
        .getZIndex();
      var bIndex = (typeof b.getZIndex === 'function') ? b.getZIndex() : b.owner
        .getZIndex();
      return aIndex - bIndex;
    });
  };

  /**
   * focusPath
   */

  this.focusPath = function(path) {

    var self = this;
    var pathRenderedSegments = [];
    var graph = this.transitive.network.graph;

    if (path) { // if we're focusing a specific path
      pathRenderedSegments = path.getRenderedSegments();

      // un-focus all internal points
      graph.edges.forEach(function(edge) {
        edge.pointArray.forEach(function(point, i) {
          point.setAllPatternsFocused(false);
        });
      }, this);
    } else { // if we're returing to 'all-focused' mode
      // re-focus all internal points
      graph.edges.forEach(function(edge) {
        edge.pointArray.forEach(function(point, i) {
          point.setAllPatternsFocused(true);
        });
      }, this);
    }

    var focusChangeSegments = [],
      focusedVertexPoints = [];
    each(this.renderedSegments, function(rSegment) {
      if (path && pathRenderedSegments.indexOf(rSegment) === -1) {
        if (rSegment.isFocused()) focusChangeSegments.push(rSegment);
        rSegment.setFocused(false);
      } else {
        if (!rSegment.isFocused()) focusChangeSegments.push(rSegment);
        rSegment.setFocused(true);
        focusedVertexPoints.push(rSegment.pathSegment.startVertex().point);
        focusedVertexPoints.push(rSegment.pathSegment.endVertex().point);
      }
    });

    var focusChangePoints = [];
    graph.vertices.forEach(function(vertex) {
      var point = vertex.point;
      if (focusedVertexPoints.indexOf(point) !== -1) {
        if (!point.isFocused()) focusChangePoints.push(point);
        point.setFocused(true);
      } else {
        if (point.isFocused()) focusChangePoints.push(point);
        point.setFocused(false);
      }
    }, this);

    // bring the focused elements to the front for the transition
    //if (path) this.sortElements();

    // create a transition callback function that invokes refresh() after all transitions complete
    var n = 0;
    var refreshOnEnd = function(transition, callback) {
      transition
        .each(function() {
          ++n;
        })
        .each("end", function() {
          if (!--n) self.transitive.refresh();
        });
    };

    // run the transtions on the affected elements
    each(focusChangeSegments, function(segment) {
      segment.runFocusTransition(this.transitive.display, refreshOnEnd);
    }, this);

    each(focusChangePoints, function(point) {
      point.runFocusTransition(this.transitive.display, refreshOnEnd);
    }, this);

  };
});
Esempio n. 4
0
var Point = augment(Object, function() {

  this.constructor = function(data) {
    for (var key in data) {
      this[key] = data[key];
    }

    this.paths = [];
    this.renderData = [];

    this.label = new PointLabel(this);
    this.renderLabel = true;

    this.focused = true;
    this.sortableType = 'POINT';

    this.placeOffsets = {
      x: 0,
      y: 0
    };

    this.zIndex = 10000;
  };

  /**
   * Get unique ID for point -- must be defined by subclass
   */

  this.getId = function() {};

  this.getElementId = function() {
    return this.getType().toLowerCase() + '-' + this.getId();
  };

  /**
   * Get Point type -- must be defined by subclass
   */

  this.getType = function() {};

  /**
   * Get Point name
   */

  this.getName = function() {
    return this.getType() + ' point (ID=' + this.getId() + ')';
  };

  /**
   * Get latitude
   */

  this.getLat = function() {
    return 0;
  };

  /**
   * Get longitude
   */

  this.getLon = function() {
    return 0;
  };

  this.containsSegmentEndPoint = function() {
    return false;
  };

  this.containsBoardPoint = function() {
    return false;
  };

  this.containsAlightPoint = function() {
    return false;
  };

  this.containsTransferPoint = function() {
    return false;
  };

  this.getPatterns = function() {
    return [];
  };

  /**
   * Draw the point
   *
   * @param {Display} display
   */

  this.render = function(display) {
    this.label.svgGroup = null;
  };

  /**
   * Refresh a previously drawn point
   *
   * @param {Display} display
   */

  this.refresh = function(display) {};

  this.addRenderData = function() {};

  this.clearRenderData = function() {};

  this.containsFromPoint = function() {
    return false;
  };

  this.containsToPoint = function() {
    return false;
  };

  this.initSvg = function(display) {
    // set up the main svg group for this stop
    this.svgGroup = display.svg.append('g')
      .attr('id', 'transitive-' + this.getType().toLowerCase() + '-' + this
        .getId())
    //.attr('class', 'transitive-sortable')
    .datum(this);

    this.markerSvg = this.svgGroup.append('g');
    this.labelSvg = this.svgGroup.append('g');
  };

  //** Shared geom utility functions **//

  this.constructMergedMarker = function(display) {

    var dataArray = this.getRenderDataArray();
    var xValues = [],
      yValues = [];
    dataArray.forEach(function(data) {
      var x = data.x; //display.xScale(data.x) + data.offsetX;
      var y = data.y; //display.yScale(data.y) - data.offsetY;
      xValues.push(x);
      yValues.push(y);
    });
    var minX = Math.min.apply(Math, xValues),
      minY = Math.min.apply(Math, yValues);
    var maxX = Math.max.apply(Math, xValues),
      maxY = Math.max.apply(Math, yValues);

    // retrieve marker type and radius from the styler
    var markerType = display.styler.compute(display.styler.stops_merged[
      'marker-type'], display, {
      owner: this
    });
    var stylerRadius = display.styler.compute(display.styler.stops_merged.r,
      display, {
        owner: this
      });

    var width, height, r;

    // if this is a circle marker w/ a styler-defined fixed radius, use that
    if (markerType === 'circle' && stylerRadius) {
      width = height = stylerRadius * 2;
      r = stylerRadius;
    }

    // otherwise, this is a dynamically-sized marker
    else {

      var dx = maxX - minX,
        dy = maxY - minY;

      var markerPadding = display.styler.compute(display.styler.stops_merged[
        'marker-padding'], display, {
        owner: this
      }) || 0;

      var patternRadius = display.styler.compute(display.styler[
        this.patternStylerKey].r, display, {
        owner: this
      });
      r = parseFloat(patternRadius) + markerPadding;

      if (markerType === 'circle') {
        width = height = Math.max(dx, dy) + 2 * r;
        r = width / 2;
      } else {
        width = dx + 2 * r;
        height = dy + 2 * r;
        if (markerType === 'rectangle') r = 0;
      }
    }

    return {
      x: (minX + maxX) / 2 - width / 2,
      y: (minY + maxY) / 2 - height / 2,
      width: width,
      height: height,
      rx: r,
      ry: r
    };

  };

  this.initMarkerData = function(display) {

    if (this.getType() !== 'STOP' && this.getType() !== 'MULTI') return;

    this.mergedMarkerData = this.constructMergedMarker(display);

    this.placeOffsets = {
      x: 0,
      y: 0
    };
    if (this.adjacentPlace) {
      var placeBBox = this.adjacentPlace.getMarkerBBox();

      var placeR = display.styler.compute(display.styler.places.r, display, {
        owner: this.adjacentPlace
      });

      var placeX = display.xScale(this.adjacentPlace.worldX);
      var placeY = display.yScale(this.adjacentPlace.worldY);

      var thisR = this.mergedMarkerData.width / 2;
      var thisX = this.mergedMarkerData.x + thisR,
        thisY = this.mergedMarkerData.y + thisR;

      var dx = thisX - placeX,
        dy = thisY - placeY;
      var dist = Math.sqrt(dx * dx + dy * dy);

      if (placeR + thisR > dist) {
        var f = (placeR + thisR) / dist;
        this.placeOffsets = {
          x: (dx * f) - dx,
          y: (dy * f) - dy
        };

        this.mergedMarkerData.x += this.placeOffsets.x;
        this.mergedMarkerData.y += this.placeOffsets.y;

        each(this.graphVertex.incidentEdges(), function(edge) {
          each(edge.renderSegments, function(segment) {
            segment.refreshRenderData(display);
          });
        });
      }
    }
  };

  this.refreshLabel = function(display) {
    if (!this.renderLabel) return;
    this.label.refresh(display);
  };

  this.getMarkerBBox = function() {
    return this.markerSvg.node().getBBox();
  };

  this.setFocused = function(focused) {
    this.focused = focused;
  };

  this.isFocused = function() {
    return (this.focused === true);
  };

  this.runFocusTransition = function(display, callback) {};

  this.setAllPatternsFocused = function() {};

  this.getZIndex = function() {
    return this.zIndex;
  };

  this.getAverageCoord = function() {
    var dataArray = this.getRenderDataArray();

    var xTotal = 0,
      yTotal = 0;
    each(dataArray, function(data) {
      xTotal += data.x;
      yTotal += data.y;
    });

    return {
      x: xTotal / dataArray.length,
      y: yTotal / dataArray.length
    };
  };

  this.hasRenderData = function() {
    var dataArray = this.getRenderDataArray();
    return (dataArray && dataArray.length > 0);
  };

  this.makeDraggable = function(transitive) { };

  this.toString = function() {
    return this.getType() + ' point: ' + this.getId() + ' (' + this.getName() +
      ')';
  };

});
Esempio n. 5
0
var PointLabel = augment(Label, function(base) {

  this.constructor = function(parent) {

    base.constructor.call(this, parent);

    this.labelAngle = 0;
    this.labelPosition = 1;
  };


  this.initText = function() {
    return this.transformText(this.parent.getName());
  };


  this.render = function() {
    this.svgGroup = this.parent.labelSvg.append('g');

    var typeStr = this.parent.getType().toLowerCase();

    this.mainLabel = this.svgGroup.append('text')
      .datum({ point: this.parent })
      .attr('id', 'transitive-' + typeStr + '-label-' + this.parent.getId())
      .text(this.getText())
      .attr('class', 'transitive-' + typeStr + '-label');
  };


  this.refresh = function() {
    if(!this.labelAnchor) return;

    if(!this.svgGroup) this.render();

    this.svgGroup
      .attr('text-anchor', this.labelPosition > 0 ? 'start' : 'end')
      //.attr('visibility', this.visibility ? 'visible' : 'hidden')
      .attr('transform', (function (d, i) {
        return 'translate(' + this.labelAnchor.x +',' + this.labelAnchor.y +')';
      }).bind(this));

    this.mainLabel
      .attr('transform', (function (d, i) {
        return 'rotate(' + this.labelAngle + ', 0, 0)';
      }).bind(this));
  };


  this.setOrientation = function(orientation) {
    //console.log('lab anch: '+ this.parent.getName());
    this.orientation = orientation;

    var markerBBox = this.parent.getMarkerBBox();
    if(!markerBBox) return;

    var x, y;
    var offset = 5;

    if(orientation === 'E') {
      x = markerBBox.x + markerBBox.width + offset;
      y = markerBBox.y + markerBBox.height / 2;
      this.labelPosition = 1;
      this.labelAngle = 0;
    }

    else if(orientation === 'W') {
      x = markerBBox.x - offset;
      y = markerBBox.y + markerBBox.height / 2;
      this.labelPosition = -1;
      this.labelAngle = 0;
    }

    else if(orientation === 'NE') {
      x = markerBBox.x + markerBBox.width + offset;
      y = markerBBox.y - offset;
      this.labelPosition = 1;
      this.labelAngle = -45;
    }

    else if(orientation === 'SE') {
      x = markerBBox.x + markerBBox.width + offset;
      y = markerBBox.y + markerBBox.height + offset;
      this.labelPosition = 1;
      this.labelAngle = 45;
    }

    else if(orientation === 'NW') {
      x = markerBBox.x - offset;
      y = markerBBox.y - offset;
      this.labelPosition = -1;
      this.labelAngle = 45;
    }

    else if(orientation === 'SW') {
      x = markerBBox.x - offset;
      y = markerBBox.y + markerBBox.height + offset;
      this.labelPosition = -1;
      this.labelAngle = -45;
    }

    else if(orientation === 'N') {
      x = markerBBox.x + markerBBox.width / 2;
      y = markerBBox.y - offset;
      this.labelPosition = 1;
      this.labelAngle = -90;
    }

    else if(orientation === 'S') {
      x = markerBBox.x + markerBBox.width / 2;
      y = markerBBox.y + markerBBox.height + offset;
      this.labelPosition = -1;
      this.labelAngle = -90;
    }

    this.labelAnchor = { x : x, y : y };
  };


  this.getBBox = function() {

    if(this.orientation === 'E') {
      return {
        x : this.labelAnchor.x,
        y : this.labelAnchor.y - this.textHeight,
        width : this.textWidth,
        height : this.textHeight
      };
    }

    if(this.orientation === 'W') {
      return {
        x : this.labelAnchor.x - this.textWidth,
        y : this.labelAnchor.y - this.textHeight,
        width : this.textWidth,
        height : this.textHeight
      };
    }

    if(this.orientation === 'N') {
      return {
        x : this.labelAnchor.x - this.textHeight,
        y : this.labelAnchor.y - this.textWidth,
        width : this.textHeight,
        height : this.textWidth
      };
    }

    if(this.orientation === 'S') {
      return {
        x : this.labelAnchor.x - this.textHeight,
        y : this.labelAnchor.y,
        width : this.textHeight,
        height : this.textWidth
      };
    }

    var bboxSide = this.textWidth * Math.sqrt(2)/2;
    
    if(this.orientation === 'NE') {
      return {
        x : this.labelAnchor.x,
        y : this.labelAnchor.y - bboxSide,
        width : bboxSide,
        height : bboxSide
      };
    }

    if(this.orientation === 'SE') {
      return {
        x : this.labelAnchor.x,
        y : this.labelAnchor.y,
        width : bboxSide,
        height : bboxSide
      };
    }

    if(this.orientation === 'NW') {
      return {
        x : this.labelAnchor.x - bboxSide,
        y : this.labelAnchor.y - bboxSide,
        width : bboxSide,
        height : bboxSide
      };
    }

    if(this.orientation === 'SW') {
      return {
        x : this.labelAnchor.x - bboxSide,
        y : this.labelAnchor.y,
        width : bboxSide,
        height : bboxSide
      };
    }

  };


  this.intersects = function(obj) {
    if(obj instanceof Label) {
      // todo: handle label-label intersection for diagonally placed labels separately
      return this.intersectsBBox(obj.getBBox());
    }
    else if(obj.x && obj.y && obj.width && obj.height) {
      return this.intersectsBBox(obj);
    }

    return false;
  };


  this.transformText = function(str) {
    // basic 'title case' for now
    return str.replace(/\w\S*/g, function(txt) {
      return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
    });
  };

});
Esempio n. 6
0
var Stop = augment(Point, function(base) {

  this.constructor = function(data) {
    base.constructor.call(this, data);

    if (data && data.stop_lat && data.stop_lon) {
      var xy = Util.latLonToSphericalMercator(data.stop_lat, data.stop_lon);
      this.worldX = xy[0];
      this.worldY = xy[1];
    }

    this.patterns = [];

    this.patternRenderData = {};
    this.patternFocused = {};
    this.patternCount = 0;

    this.patternStylerKey = 'stops_pattern';

    this.isSegmentEndPoint = false;
  };

  /**
   * Get id
   */

  this.getId = function() {
    return this.stop_id;
  };

  /**
   * Get type
   */

  this.getType = function() {
    return 'STOP';
  };

  /**
   * Get name
   */

  this.getName = function() {
    if (!this.stop_name) return ('Unnamed Stop (ID=' + this.getId() + ')');
    return this.stop_name.replace('METRO STATION', '');
  };

  /**
   * Get lat
   */

  this.getLat = function() {
    return this.stop_lat;
  };

  /**
   * Get lon
   */

  this.getLon = function() {
    return this.stop_lon;
  };

  this.containsSegmentEndPoint = function() {
    return this.isSegmentEndPoint;
  };

  this.containsBoardPoint = function() {
    return this.isBoardPoint;
  };

  this.containsAlightPoint = function() {
    return this.isAlightPoint;
  };

  this.containsTransferPoint = function() {
    return this.isTransferPoint;
  };

  this.getPatterns = function() {
    return this.patterns;
  };

  this.addPattern = function(pattern) {
    if (this.patterns.indexOf(pattern) === -1) this.patterns.push(pattern);
  };

  /**
   * Add render data
   *
   * @param {Object} stopInfo
   */

  this.addRenderData = function(stopInfo) {

    if (stopInfo.segment.getType() === 'TRANSIT') {

      var s = {
        sortableType: 'POINT_STOP_PATTERN',
        owner: this,
        getZIndex: function() {
          if (this.owner.graphVertex) {
            return this.owner.getZIndex();
          }
          return this.segment.getZIndex() + 1;
        }
      };

      for (var key in stopInfo)
        s[key] = stopInfo[key];

      var patternId = stopInfo.segment.patternIds;
      if (!(patternId in this.patternRenderData)) this.patternRenderData[
        patternId] = {};
      this.patternRenderData[patternId][stopInfo.segment.getId()] = s; //.push(s);

      each(stopInfo.segment.patterns, function(pattern) {
        this.addPattern(pattern);
      }, this);
    }
    this.patternCount = Object.keys(this.patternRenderData).length;
  };

  this.isPatternFocused = function(patternId) {
    if (!(patternId in this.patternFocused)) return true;
    return (this.patternFocused[patternId]);
  };

  this.setPatternFocused = function(patternId, focused) {
    this.patternFocused[patternId] = focused;
  };

  this.setAllPatternsFocused = function(focused) {
    for (var key in this.patternRenderData) {
      this.patternFocused[key] = focused;
    }
  };

  /**
   * Draw a stop
   *
   * @param {Display} display
   */

  this.render = function(display) {
    base.render.call(this, display);
    if (Object.keys(this.patternRenderData).length === 0) return;
    //if (this.renderData.length === 0) return;

    var renderDataArray = this.getRenderDataArray();

    this.initSvg(display);

    // set up the merged marker
    this.mergedMarker = this.markerSvg.append('g').append('rect')
      .attr('class', 'transitive-sortable transitive-stop-marker-merged')
      .datum(this.getMergedRenderData());

    // set up the pattern-specific markers
    this.patternMarkers = this.markerSvg.append('g').selectAll('circle')
      .data(renderDataArray)
      .enter()
      .append('circle')
      .attr('class', 'transitive-sortable transitive-stop-marker-pattern');

  };

  /**
   * Refresh the stop
   *
   * @param {Display} display
   */

  this.refresh = function(display) {

    if (this.patternCount === 0) return;

    if (!this.mergedMarkerData) this.initMarkerData(display);

    // refresh the pattern-level markers
    this.patternMarkers.data(this.getRenderDataArray());
    this.patternMarkers.attr('transform', (function(d, i) {
      if (!isNaN(d.x) && !isNaN(d.y)) {
        var x = d.x + this.placeOffsets.x;
        var y = d.y + this.placeOffsets.y;
        return 'translate(' + x + ', ' + y + ')';
      }
    }).bind(this));

    // refresh the merged marker
    if (this.mergedMarker) {
      var a = this.constructMergedMarker(display, 'stops_pattern');
      this.mergedMarker.datum(this.getMergedRenderData());
      if (!isNaN(this.mergedMarkerData.x) && !isNaN(this.mergedMarkerData.y))
        this.mergedMarker.attr(this.mergedMarkerData);
    }

  };

  this.getMergedRenderData = function() {
    return {
      owner: this,
      sortableType: 'POINT_STOP_MERGED'
    };
  };

  this.getRenderDataArray = function() {
    var dataArray = [];
    for (var patternId in this.patternRenderData) {
      var segmentData = this.patternRenderData[patternId];
      for (var segmentId in segmentData) {
        dataArray.push(segmentData[segmentId]);
      }
    }
    return dataArray;
  };

  this.getMarkerBBox = function() {
    if (this.mergedMarker) return this.mergedMarkerData;
  };

  this.isFocused = function() {
    if (this.mergedMarker || !this.patternRenderData) {
      return (this.focused === true);
    }

    var focused = true;
    for (var patternId in this.patternRenderData) {
      focused = this && this.isPatternFocused(patternId);
    }
    return focused;
  };

  this.runFocusTransition = function(display, callback) {
    if (this.mergedMarker) {
      var newStrokeColor = display.styler.compute(display.styler.stops_merged
        .stroke, display, {
          owner: this
        });
      this.mergedMarker.transition().style('stroke', newStrokeColor).call(
        callback);
    }
    if (this.label) this.label.runFocusTransition(display, callback);
  };

  this.clearRenderData = function() {
    this.patternRenderData = {};
    this.mergedMarkerData = null;
    this.placeOffsets = {
      x: 0,
      y: 0
    };
  };

});
Esempio n. 7
0
var MultiPoint = augment(Point, function(base) {

  this.constructor = function(pointArray) {
    base.constructor.call(this);
    this.points = [];
    if(pointArray) {
      pointArray.forEach(function(point) {
        this.addPoint(point);
      }, this);
    }
    this.renderData = [];
    this.id = 'multi';
    this.toPoint = this.fromPoint = null;
  };

  /**
   * Get id
   */

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

  /**
   * Get type
   */

  this.getType = function() {
    return 'MULTI';
  };


  this.getName = function() {
    if(this.fromPoint) return this.fromPoint.getName();
    if(this.toPoint) return this.toPoint.getName();
    var shortest = null;
    this.points.forEach(function(point) {
      if(!shortest || point.getName().length < shortest.length) shortest = point.getName();
    });
    return shortest + ' AREA';
  };


  this.containsSegmentEndPoint = function() {
    for(var i = 0; i < this.points.length; i++) { 
      if(this.points[i].containsSegmentEndPoint()) return true;
    }
    return false;
  };


  this.containsBoardPoint = function() {
    for(var i = 0; i < this.points.length; i++) { 
      if(this.points[i].containsBoardPoint()) return true;
    }
    return false;
  };


  this.containsAlightPoint = function() {
    for(var i = 0; i < this.points.length; i++) { 
      if(this.points[i].containsAlightPoint()) return true;
    }
    return false;
  };


  this.containsTransferPoint = function() {
    for(var i = 0; i < this.points.length; i++) { 
      if(this.points[i].containsTransferPoint()) return true;
    }
    return false;
  };


  this.containsFromPoint = function() {
    return (this.fromPoint !== null);
  };


  this.containsToPoint = function() {
    return (this.toPoint !== null);
  };


  this.getPatterns = function() {
    var patterns = [];
    
    this.points.forEach(function(point) {
      point.patterns.forEach(function(pattern) {
        if(patterns.indexOf(pattern) === -1) patterns.push(pattern);
      });
    });

    return patterns;
  };


  this.addPoint = function(point) {
    if(this.points.indexOf(point) !== -1) return;
    this.points.push(point);
    this.id += '-' + point.getId();
    if(point.containsFromPoint()) { // getType() === 'PLACE' && point.getId() === 'from') {
      this.fromPoint = point;
    }
    if(point.containsToPoint()) { // getType() === 'PLACE' && point.getId() === 'to') {
      this.toPoint = point;
    }
  };


  /**
   * Add render data
   *
   * @param {Object} stopInfo
   */

  this.addRenderData = function(pointInfo) {
    if(pointInfo.offsetX !== 0 || pointInfo.offsetY !==0) this.hasOffsetPoints = true;
    this.renderData.push(pointInfo);
  };


  this.clearRenderData = function() {
    this.hasOffsetPoints = false;
    this.renderData = [];
  };


  /**
   * Draw a multipoint
   *
   * @param {Display} display
   */

  this.render = function(display) {
    base.render.call(this, display);

    if (!this.renderData) return;

    // set up the main svg group for this stop
    this.initSvg(display);
    this.svgGroup
      .attr('class', 'transitive-sortable')
      .datum({
        owner: this,
        sortableType: 'POINT_MULTI'
      });

    this.initMergedMarker(display);

    // set up the pattern markers
    /*this.marker = this.markerSvg.selectAll('circle')
      .data(this.renderData)
      .enter()
      .append('circle')
      .attr('class', 'transitive-multipoint-marker-pattern');*/
  };


  this.initMergedMarker = function(display) {
    // set up the merged marker
    if(this.fromPoint || this.toPoint) {
      this.mergedMarker = this.markerSvg.append('g').append('circle')
        .datum({ owner : this })
        .attr('class', 'transitive-multipoint-marker-merged');
    }
    else if(this.hasOffsetPoints || this.renderData.length > 1) {

      this.mergedMarker = this.markerSvg.append('g').append('rect')
        .datum({ owner : this })
        .attr('class', 'transitive-multipoint-marker-merged');
    }
  };


  /**
   * Refresh the point
   *
   * @param {Display} display
   */

  this.refresh = function(display) {
    if (!this.renderData) return;

    // refresh the merged marker
    if(this.mergedMarker) {
      this.mergedMarker.datum({ owner : this });
      this.mergedMarker.attr(this.constructMergedMarker(display, 'multipoints_pattern'));
    }    

    
    /*var cx, cy;
    // refresh the pattern-level markers
    this.marker.data(this.renderData);
    this.marker.attr('transform', function (d, i) {
      cx = d.x;
      cy = d.y;
      var x = display.xScale(d.x) + d.offsetX;
      var y = display.yScale(d.y) - d.offsetY;
      return 'translate(' + x +', ' + y +')';
    });*/

  };

  this.getRenderDataArray = function() {
    return this.renderData;
  };
});
Esempio n. 8
0
var Label = augment(Object, function () {

  this.constructor = function(parent) {
    this.parent = parent;
    this.sortableType = 'LABEL';
  };


  this.getText = function() {
    if(!this.labelText) this.labelText = this.initText();
    return this.labelText;
  };


  this.initText = function() {
    return this.parent.getName();
  };


  this.render = function() {
  };


  this.refresh = function() {
  };


  this.setVisibility = function(visibility) {
    if(this.svgGroup) this.svgGroup.attr('visibility', visibility ? 'visible' : 'hidden');
  };


  this.getBBox = function() {
    return null;
  };


  this.intersects = function(obj) {
    return null;
  };


  this.intersectsBBox = function(bbox) {
    var thisBBox = this.getBBox(this.orientation);
    var r = (thisBBox.x <= bbox.x + bbox.width &&
            bbox.x <= thisBBox.x + thisBBox.width &&
            thisBBox.y <= bbox.y + bbox.height &&
            bbox.y <= thisBBox.y + thisBBox.height);
    return r;
  };


  this.isFocused = function() {
    return this.parent.isFocused();
  };


  this.getZIndex = function() {
    return 20000;
  };

});
Esempio n. 9
0
var Labeler = augment(Object, function () {

  this.constructor = function(transitive) {

    this.transitive = transitive;
    this.points = [];

  };


  this.updateLabelList = function() {

    this.points = [];
    this.transitive.graph.vertices.forEach(function(vertex) {
      //console.log('- ' + vertex.point.getName());
      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.points.forEach(function(point) {
      this.addBBoxToQuadtree(point.getMarkerBBox());
    }, this);

    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;
      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) {
    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() {

    var styler = this.transitive.style;

    var labeledSegments = [];

    this.transitive.renderSegments.forEach(function(segment) {
      if(segment.getType() === 'TRANSIT' && segment.pattern.route.route_type === 3) {
  
        var labelText = segment.label.getText();
        var fontFamily = styler.compute(styler.segment_labels['font-family'], this.transitive.display, {segment: segment});
        var fontSize = styler.compute(styler.segment_labels['font-size'], this.transitive.display, {segment: segment});
        var textBBox = Util.getTextBBox(labelText, {
          'font-size' : fontSize,
          'font-family' : fontFamily,
        });
        segment.label.textWidth = textBBox.width;
        segment.label.textHeight = textBBox.height;
        var labelAnchors = segment.getLabelAnchors(this.transitive.display);
        segment.label.labelAnchor = labelAnchors[0]; /*{
          x : this.transitive.display.xScale(segment.renderData[0].x) + segment.renderData[0].offsetX,
          y : this.transitive.display.yScale(segment.renderData[0].y) - segment.renderData[0].offsetY
        };*/

        labeledSegments.push(segment);

        this.quadtree.add([segment.label.labelAnchor.x, segment.label.labelAnchor.y, segment.label]);

      }
    }, this);

    return labeledSegments;
  };


  this.placePointLabels = function() {

    var styler = this.transitive.style;

    var labeledPoints = [];

    this.points.forEach(function(point) {

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

      var orientations = ['E', 'W', 'NE', 'SE', 'NW', 'SW', 'N', 'S'];

      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;

    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;
  };

});
Esempio n. 10
0
var Place = augment(Point, function(base) {

  /**
   *  the constructor
   */

  this.constructor = function(data) {
    base.constructor.call(this, data);

    if (data && data.place_lat && data.place_lon) {
      var xy = Util.latLonToSphericalMercator(data.place_lat, data.place_lon);
      this.worldX = xy[0];
      this.worldY = xy[1];
    }

  };

  /**
   * Get Type
   */

  this.getType = function() {
    return 'PLACE';
  };

  /**
   * Get ID
   */

  this.getId = function() {
    return this.place_id;
  };

  /**
   * Get Name
   */

  this.getName = function() {
    return this.place_name;
  };

  /**
   * Get lat
   */

  this.getLat = function() {
    return this.place_lat;
  };

  /**
   * Get lon
   */

  this.getLon = function() {
    return this.place_lon;
  };

  this.containsSegmentEndPoint = function() {
    return true;
  };

  this.containsFromPoint = function() {
    return (this.getId() === 'from');
  };

  this.containsToPoint = function() {
    return (this.getId() === 'to');
  };

  this.addRenderData = function(pointInfo) {
    this.renderData.push(pointInfo);
  };

  this.getRenderDataArray = function() {
    return this.renderData;
  };

  this.clearRenderData = function() {
    this.renderData = [];
  };

  /**
   * Draw a place
   *
   * @param {Display} display
   */

  this.render = function(display) {
    base.render.call(this, display);
    if (!this.renderData) return;

    this.initSvg(display);
    this.svgGroup
      .attr('class', 'transitive-sortable')
      .datum({
        owner: this,
        sortableType: 'POINT_PLACE'
      });

    // set up the markers
    this.marker = this.markerSvg.append('circle')
      .datum({
        owner: this
      })
      .attr('class', 'transitive-place-circle');

    var iconUrl = display.styler.compute(display.styler.places_icon[
      'xlink:href'], display, {
      owner: this
    });
    if (iconUrl) {
      this.icon = this.markerSvg.append('image')
        .datum({
          owner: this
        })
        .attr('class', 'transitive-place-icon')
        .attr('xlink:href', iconUrl);
    }
  };

  /**
   * Refresh the place
   *
   * @param {Display} display
   */

  this.refresh = function(display) {
    if (!this.renderData) return;

    // refresh the marker/icon
    var x = display.xScale(this.worldX);
    var y = display.yScale(this.worldY);
    var translate = 'translate(' + x + ', ' + y + ')';
    this.marker.attr('transform', translate);
    if (this.icon) this.icon.attr('transform', translate);

  };
});