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; }; });
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 = {}; }; });
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); }; });
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() + ')'; }; });
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(); }); }; });
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 }; }; });
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; }; });
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; }; });
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; }; });
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); }; });