|
| 1 | +/** |
| 2 | +* Copyright 2012-2017, Plotly, Inc. |
| 3 | +* All rights reserved. |
| 4 | +* |
| 5 | +* This source code is licensed under the MIT license found in the |
| 6 | +* LICENSE file in the root directory of this source tree. |
| 7 | +*/ |
| 8 | + |
| 9 | +'use strict'; |
| 10 | + |
| 11 | +var mod = require('./mod'); |
| 12 | + |
| 13 | +/* |
| 14 | + * look for intersection of two line segments |
| 15 | + * (1->2 and 3->4) - returns array [x,y] if they do, null if not |
| 16 | + */ |
| 17 | +exports.segmentsIntersect = segmentsIntersect; |
| 18 | +function segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4) { |
| 19 | + var a = x2 - x1, |
| 20 | + b = x3 - x1, |
| 21 | + c = x4 - x3, |
| 22 | + d = y2 - y1, |
| 23 | + e = y3 - y1, |
| 24 | + f = y4 - y3, |
| 25 | + det = a * f - c * d; |
| 26 | + // parallel lines? intersection is undefined |
| 27 | + // ignore the case where they are colinear |
| 28 | + if(det === 0) return null; |
| 29 | + var t = (b * f - c * e) / det, |
| 30 | + u = (b * d - a * e) / det; |
| 31 | + // segments do not intersect? |
| 32 | + if(u < 0 || u > 1 || t < 0 || t > 1) return null; |
| 33 | + |
| 34 | + return {x: x1 + a * t, y: y1 + d * t}; |
| 35 | +} |
| 36 | + |
| 37 | +/* |
| 38 | + * find the minimum distance between two line segments (1->2 and 3->4) |
| 39 | + */ |
| 40 | +exports.segmentDistance = function segmentDistance(x1, y1, x2, y2, x3, y3, x4, y4) { |
| 41 | + if(segmentsIntersect(x1, y1, x2, y2, x3, y3, x4, y4)) return 0; |
| 42 | + |
| 43 | + // the two segments and their lengths squared |
| 44 | + var x12 = x2 - x1; |
| 45 | + var y12 = y2 - y1; |
| 46 | + var x34 = x4 - x3; |
| 47 | + var y34 = y4 - y3; |
| 48 | + var l2_12 = x12 * x12 + y12 * y12; |
| 49 | + var l2_34 = x34 * x34 + y34 * y34; |
| 50 | + |
| 51 | + // calculate distance squared, then take the sqrt at the very end |
| 52 | + var dist2 = Math.min( |
| 53 | + perpDistance2(x12, y12, l2_12, x3 - x1, y3 - y1), |
| 54 | + perpDistance2(x12, y12, l2_12, x4 - x1, y4 - y1), |
| 55 | + perpDistance2(x34, y34, l2_34, x1 - x3, y1 - y3), |
| 56 | + perpDistance2(x34, y34, l2_34, x2 - x3, y2 - y3) |
| 57 | + ); |
| 58 | + |
| 59 | + return Math.sqrt(dist2); |
| 60 | +}; |
| 61 | + |
| 62 | +/* |
| 63 | + * distance squared from segment ab to point c |
| 64 | + * [xab, yab] is the vector b-a |
| 65 | + * [xac, yac] is the vector c-a |
| 66 | + * l2_ab is the length squared of (b-a), just to simplify calculation |
| 67 | + */ |
| 68 | +function perpDistance2(xab, yab, l2_ab, xac, yac) { |
| 69 | + var fc_ab = (xac * xab + yac * yab); |
| 70 | + if(fc_ab < 0) { |
| 71 | + // point c is closer to point a |
| 72 | + return xac * xac + yac * yac; |
| 73 | + } |
| 74 | + else if(fc_ab > l2_ab) { |
| 75 | + // point c is closer to point b |
| 76 | + var xbc = xac - xab; |
| 77 | + var ybc = yac - yab; |
| 78 | + return xbc * xbc + ybc * ybc; |
| 79 | + } |
| 80 | + else { |
| 81 | + // perpendicular distance is the shortest |
| 82 | + var crossProduct = xac * yab - yac * xab; |
| 83 | + return crossProduct * crossProduct / l2_ab; |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +// a very short-term cache for getTextLocation, just because |
| 88 | +// we're often looping over the same locations multiple times |
| 89 | +// invalidated as soon as we look at a different path |
| 90 | +var locationCache, workingPath, workingTextWidth; |
| 91 | + |
| 92 | +// turn a path and position along it into x, y, and angle for the given text |
| 93 | +exports.getTextLocation = function getTextLocation(path, totalPathLen, positionOnPath, textWidth) { |
| 94 | + if(path !== workingPath || textWidth !== workingTextWidth) { |
| 95 | + locationCache = {}; |
| 96 | + workingPath = path; |
| 97 | + workingTextWidth = textWidth; |
| 98 | + } |
| 99 | + if(locationCache[positionOnPath]) { |
| 100 | + return locationCache[positionOnPath]; |
| 101 | + } |
| 102 | + |
| 103 | + // for the angle, use points on the path separated by the text width |
| 104 | + // even though due to curvature, the text will cover a bit more than that |
| 105 | + var p0 = path.getPointAtLength(mod(positionOnPath - textWidth / 2, totalPathLen)); |
| 106 | + var p1 = path.getPointAtLength(mod(positionOnPath + textWidth / 2, totalPathLen)); |
| 107 | + // note: atan handles 1/0 nicely |
| 108 | + var theta = Math.atan((p1.y - p0.y) / (p1.x - p0.x)); |
| 109 | + // center the text at 2/3 of the center position plus 1/3 the p0/p1 midpoint |
| 110 | + // that's the average position of this segment, assuming it's roughly quadratic |
| 111 | + var pCenter = path.getPointAtLength(mod(positionOnPath, totalPathLen)); |
| 112 | + var x = (pCenter.x * 4 + p0.x + p1.x) / 6; |
| 113 | + var y = (pCenter.y * 4 + p0.y + p1.y) / 6; |
| 114 | + |
| 115 | + var out = {x: x, y: y, theta: theta}; |
| 116 | + locationCache[positionOnPath] = out; |
| 117 | + return out; |
| 118 | +}; |
| 119 | + |
| 120 | +exports.clearLocationCache = function() { |
| 121 | + workingPath = null; |
| 122 | +}; |
| 123 | + |
| 124 | +/* |
| 125 | + * Find the segment of `path` that's within the visible area |
| 126 | + * given by `bounds` {left, right, top, bottom}, to within a |
| 127 | + * precision of `buffer` px |
| 128 | + * |
| 129 | + * returns: undefined if nothing is visible, else object: |
| 130 | + * { |
| 131 | + * min: position where the path first enters bounds, or 0 if it |
| 132 | + * starts within bounds |
| 133 | + * max: position where the path last exits bounds, or the path length |
| 134 | + * if it finishes within bounds |
| 135 | + * len: max - min, ie the length of visible path |
| 136 | + * total: the total path length - just included so the caller doesn't |
| 137 | + * need to call path.getTotalLength() again |
| 138 | + * isClosed: true iff the start and end points of the path are both visible |
| 139 | + * and are at the same point |
| 140 | + * } |
| 141 | + * |
| 142 | + * Works by starting from either end and repeatedly finding the distance from |
| 143 | + * that point to the plot area, and if it's outside the plot, moving along the |
| 144 | + * path by that distance (because the plot must be at least that far away on |
| 145 | + * the path). Note that if a path enters, exits, and re-enters the plot, we |
| 146 | + * will not capture this behavior. |
| 147 | + */ |
| 148 | +exports.getVisibleSegment = function getVisibleSegment(path, bounds, buffer) { |
| 149 | + var left = bounds.left; |
| 150 | + var right = bounds.right; |
| 151 | + var top = bounds.top; |
| 152 | + var bottom = bounds.bottom; |
| 153 | + |
| 154 | + var pMin = 0; |
| 155 | + var pTotal = path.getTotalLength(); |
| 156 | + var pMax = pTotal; |
| 157 | + |
| 158 | + var pt0, ptTotal; |
| 159 | + |
| 160 | + function getDistToPlot(len) { |
| 161 | + var pt = path.getPointAtLength(len); |
| 162 | + |
| 163 | + // hold on to the start and end points for `closed` |
| 164 | + if(len === 0) pt0 = pt; |
| 165 | + else if(len === pTotal) ptTotal = pt; |
| 166 | + |
| 167 | + var dx = (pt.x < left) ? left - pt.x : (pt.x > right ? pt.x - right : 0); |
| 168 | + var dy = (pt.y < top) ? top - pt.y : (pt.y > bottom ? pt.y - bottom : 0); |
| 169 | + return Math.sqrt(dx * dx + dy * dy); |
| 170 | + } |
| 171 | + |
| 172 | + var distToPlot = getDistToPlot(pMin); |
| 173 | + while(distToPlot) { |
| 174 | + pMin += distToPlot + buffer; |
| 175 | + if(pMin > pMax) return; |
| 176 | + distToPlot = getDistToPlot(pMin); |
| 177 | + } |
| 178 | + |
| 179 | + distToPlot = getDistToPlot(pMax); |
| 180 | + while(distToPlot) { |
| 181 | + pMax -= distToPlot + buffer; |
| 182 | + if(pMin > pMax) return; |
| 183 | + distToPlot = getDistToPlot(pMax); |
| 184 | + } |
| 185 | + |
| 186 | + return { |
| 187 | + min: pMin, |
| 188 | + max: pMax, |
| 189 | + len: pMax - pMin, |
| 190 | + total: pTotal, |
| 191 | + isClosed: pMin === 0 && pMax === pTotal && |
| 192 | + Math.abs(pt0.x - ptTotal.x) < 0.1 && |
| 193 | + Math.abs(pt0.y - ptTotal.y) < 0.1 |
| 194 | + }; |
| 195 | +}; |
0 commit comments