Skip to content

Commit

Permalink
Merge pull request #54 from conveyal/fix-label-rendering
Browse files Browse the repository at this point in the history
Fix missing label rendering + label colors
  • Loading branch information
landonreed authored Jun 22, 2020
2 parents 50d9e79 + 3471b99 commit e5cfc4e
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 96 deletions.
5 changes: 5 additions & 0 deletions cpbuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
echo 'rebuilding..'
yarn prepublish
echo 'copying built js..'
cp -r ./build ~/git/otp-react-redux/node_modules/transitive-js
echo 'done'
11 changes: 11 additions & 0 deletions lib/core/route.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {invertColor} from '../util/color'

/**
* A transit Route, as defined in the input data.
* Routes contain one or more Patterns.
Expand All @@ -24,6 +26,15 @@ export default class Route {
pattern.route = this
}

/**
* Rather than rely on the route text color field to be defined, simply return
* the black or white inverse of the background color.
*/
getTextColor () {
const bgColor = this.getColor()
if (bgColor) return invertColor(bgColor)
}

getColor () {
if (this.route_color) {
if (this.route_color.charAt(0) === '#') return this.route_color
Expand Down
115 changes: 61 additions & 54 deletions lib/graph/edge.js
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,10 @@ export default class Edge {
return coords
}

/**
* Get render coords for the provided offsets (0 values for offsets imply base
* render coordinates).
*/
getRenderCoords (fromOffsetPx, toOffsetPx, display, forward) {
var isBase = (fromOffsetPx === 0 && toOffsetPx === 0)

Expand Down Expand Up @@ -627,81 +631,84 @@ export default class Edge {
* @param {Display} display
* @returns {Object} - the coordinate as an {x,y} Object
*/

// TODO: not working for geographically-rendered edges?
coordAlongEdge (t, coords, display) {
if (!this.baseRenderCoords) {
this.calculateBaseRenderCoords(display)
}

if (coords.length !== this.baseRenderCoords.length) {
// If the render edge coordinates do not match the base render coordinates,
// get coordinate along "offset edge."
return this.coordAlongOffsetEdge(t, coords, display)
}

// get the length of this edge in screen units using the "base" (i.e. un-offset) render coords
var len = this.getRenderLength()

var loc = t * len // the target distance along the Edge's base geometry
var cur = 0 // our current location along the edge (in world units)

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

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

return pointAlongArc(coords[1].x, coords[1].y, coords[2].x, coords[2].y, r, theta, isCcw, t2)
} else {
var dx = coords[i].x - coords[i - 1].x
var dy = coords[i].y - coords[i - 1].y

return {
x: coords[i - 1].x + dx * t2,
y: coords[i - 1].y + dy * t2
}
}
}
cur += this.baseRenderCoords[i].len
}
// the target distance along the Edge's base geometry
var loc = t * len
return this.coordAlongRefEdge(loc, coords, this.baseRenderCoords)
}

coordAlongOffsetEdge (t, coords, display) {
if (!this.baseRenderCoords) this.calculateBaseRenderCoords(display)

/**
* Get coordinate along "offset edge."
* @param {[type]} t - a value between 0 and 1 representing the location of the
* point to be computed
* @param {Object[]} coords - the offset coordinates computed for this edge.
* @returns {Object} - the coordinate as an {x,y} Object
*/
coordAlongOffsetEdge (t, coords) {
// Get total length of edge by summing inter-coordinate distances.
var len = 0
for (var i = 1; i < coords.length; i++) {
len += coords[i].len
const c0 = coords[i - 1]
const c = coords[i]
if (!c.len) {
// If length between coord not available, calculate.
c.len = distance(c0.x, c0.y, c.x, c.y)
}
len += c.len
}
// the target distance along the Edge's base geometry
var loc = t * len
return this.coordAlongRefEdge(loc, coords)
}

var loc = t * len // the target distance along the Edge's base geometry
var cur = 0 // our current location along the edge (in world units)

for (i = 1; i < coords.length; i++) {
if (loc < cur + coords[i].len) {
var t2 = (loc - cur) / coords[i].len

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

return pointAlongArc(coords[1].x, coords[1].y, coords[2].x, coords[2].y, r, theta, isCcw, t2)
} else { // straight segment
var dx = coords[i].x - coords[i - 1].x
var dy = coords[i].y - coords[i - 1].y

/**
* Iterate over reference coordinates (which default to input coords) to find
* the coordinate along the edge at distance along edge.
* @param {[type]} targetDistance - target distance along edge's base geometry
* @param {[type]} coords - coordinates for edge
* @param {[type]} [refCoordinates=coords] - reference coordinates to use for
* distance. Defaults to coords.
* @returns {Object} - the coordinate as an {x,y} Object
*/
coordAlongRefEdge (targetDistance, coords, refCoordinates = coords) {
// our current location along the edge (in world units)
let currentLocation = 0
for (var i = 1; i < refCoordinates.length; i++) {
const distanceToNextCoord = refCoordinates[i].len
const {arc, radius, x, y} = coords[i]
if (targetDistance < currentLocation + distanceToNextCoord) {
// Location falls within the previous and next coordinates. Calculate
// percentage along segment.
const percentAlong = (targetDistance - currentLocation) / distanceToNextCoord
if (arc) {
// Generate coordinate on arc segment
const theta = Math.PI * arc / 180
const [c0, c1, c2] = coords
const isCcw = ccw(c0.x, c0.y, c1.x, c1.y, c2.x, c2.y)
return pointAlongArc(c1.x, c1.y, c2.x, c2.y, radius, theta, isCcw, percentAlong)
} else {
// Generate coordinate on straight segment
const dx = x - coords[i - 1].x
const dy = y - coords[i - 1].y
return {
x: coords[i - 1].x + dx * t2,
y: coords[i - 1].y + dy * t2
x: coords[i - 1].x + dx * percentAlong,
y: coords[i - 1].y + dy * percentAlong
}
}
}
cur += coords[i].len
currentLocation += distanceToNextCoord
}
}

Expand Down
31 changes: 28 additions & 3 deletions lib/labeler/labeledgegroup.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,39 +24,64 @@ export default class LabelEdgeGroup {
return textArray
}

/**
* Find the potential anchors for a label given a display and spacing.
* @param {Display} display
* @param {Number} spacing - spacing needed for label placement (i.e., label
* height with buffer)
*/
getLabelAnchors (display, spacing) {
var labelAnchors = []
var renderLen = this.getRenderLength(display)
// Determine how many anchors might fit along length.
var anchorCount = Math.floor(renderLen / spacing)
var pctSpacing = spacing / renderLen

for (var i = 0; i < anchorCount; i++) {
var t = (i % 2 === 0)
// Calculate potential position of anchor.
const t = (i % 2 === 0)
? 0.5 + (i / 2) * pctSpacing
: 0.5 - ((i + 1) / 2) * pctSpacing
var coord = this.coordAlongRenderedPath(t, display)
// Attempt to find coordinate along path for potential anchor.
const coord = this.coordAlongRenderedPath(t, display)
if (coord) labelAnchors.push(coord)
}

return labelAnchors
}

/**
* Get the coordinate located at a defined percentage along along a rendered path.
* @param {Number} t - a value between 0 and 1 representing the location of the
* point to be computed
* @param {Display} display
* @returns {Object} - the coordinate as an {x,y} Object
*/
coordAlongRenderedPath (t, display) {
var renderLen = this.getRenderLength(display)
// Get location along path.
var loc = t * renderLen

// Iterate over each edge in path, accumulating distance as we go.
var cur = 0
for (var i = 0; i < this.renderedEdges.length; i++) {
var rEdge = this.renderedEdges[i]
var edgeRenderLen = rEdge.graphEdge.getRenderLength(display)
if (loc <= cur + edgeRenderLen) {
// If location is along this edge, find and return the position along
// the edge.
var t2 = (loc - cur) / edgeRenderLen
return rEdge.graphEdge.coordAlongEdge(t2, rEdge.renderData, display)
const coord = rEdge.graphEdge.coordAlongEdge(t2, rEdge.renderData, display)
return coord
}
cur += edgeRenderLen
}
}

/**
* Get the total render length for the edge group, which consists of the sum
* of each graph edge length.
*/
getRenderLength (display) {
if (!this.renderLength) {
this.renderLength = 0
Expand Down
53 changes: 32 additions & 21 deletions lib/labeler/labeler.js
Original file line number Diff line number Diff line change
Expand Up @@ -260,35 +260,33 @@ export default class Labeler {
// iterate through the sequence collection, labeling as necessary
forEach(edgeGroups, edgeGroup => {
this.currentGroup = edgeGroup
// get the array of label strings to be places (typically the unique route short names)
// get the array of label strings to be places (typically the unique
// route short names)
this.labelTextArray = edgeGroup.getLabelTextArray()

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

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

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

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

if (!this.transitive.display.isInRange(x, y)) continue
// check for conflicts with existing placed elements
var bbox = label.getBBox()
var conflicts = this.findOverlaps(label, bbox)
var conflicts = this.findOverlaps(label, label.getBBox())

if (conflicts.length === 0) { // if no conflicts
// place the current label
if (conflicts.length === 0) {
// If no overlaps/conflicts encountered, place the current label.
placedLabels.push(label)
this.quadtree.add([label.labelAnchor.x, label.labelAnchor.y,
label
])
// Track new label in quadtree.
this.quadtree.add([x, y, label])
label = this.getNextLabel()
if (!label) break
}
Expand Down Expand Up @@ -335,18 +333,31 @@ export default class Labeler {
}

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

var matchItems = []
// Check quadtree for potential collisions.
this.quadtree.visit((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])
const {point} = node
if (point) {
const [pX, pY, pLabel] = point
if (
pX >= minX &&
pX < maxX &&
pY >= minY &&
pY < maxY &&
label.intersects(pLabel)
) {
matchItems.push(pLabel)
}
}
// No need to visit children of this node if bbox falls entirely within
// this node.
return x1 > maxX || y1 > maxY || x2 < minX || y2 < minY
})
return matchItems
Expand Down
16 changes: 11 additions & 5 deletions lib/labeler/segmentlabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,27 @@ export default class SegmentLabel extends Label {
}

render (display) {
const x = this.labelAnchor.x - this.containerWidth / 2
const y = this.labelAnchor.y - this.containerHeight / 2
// Draw rounded rectangle for label.
display.drawRect({
x: this.labelAnchor.x - this.containerWidth / 2,
y: this.labelAnchor.y - this.containerHeight / 2
x,
y
}, {
// background color
fill: display.styler.compute2('segment_labels', 'background', this.parent),
width: this.containerWidth,
height: this.containerHeight,
rx: this.containerHeight / 2,
ry: this.containerHeight / 2
})

// Offset text location by padding
display.drawText(this.getText(), {
x: this.labelAnchor.x - this.containerWidth / 2 + this.getPadding(),
y: this.labelAnchor.y - this.containerHeight / 2 + this.getPadding()
x: x + this.getPadding(),
// Offset y by a couple of pixels to account for off-centeredness.
y: y + this.getPadding() + 2
}, {
// text color
fill: display.styler.compute2('segment_labels', 'color', this.parent),
'font-size': this.fontSize
})
Expand Down
4 changes: 2 additions & 2 deletions lib/renderer/renderededge.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export default class RenderedEdge {
}

offsetAlignment (alignmentId, offset) {
// If from/to alignment IDs match, set respective offset.
if (this.graphEdge.getFromAlignmentId() === alignmentId) {
this.setFromOffset(isOutwardVector(this.graphEdge.fromVector)
? offset
Expand Down Expand Up @@ -106,8 +107,7 @@ export default class RenderedEdge {
if (this.useGeographicRendering && this.graphEdge.geomCoords) {
this.renderData = this.graphEdge.getGeometricCoords(fromOffsetPx, toOffsetPx, display, this.forward)
} else {
this.renderData = this.graphEdge.getRenderCoords(fromOffsetPx, toOffsetPx,
display, this.forward)
this.renderData = this.graphEdge.getRenderCoords(fromOffsetPx, toOffsetPx, display, this.forward)
}

var firstRenderPoint = this.renderData[0]
Expand Down
Loading

0 comments on commit e5cfc4e

Please sign in to comment.