|
| 1 | +/** |
| 2 | +* Copyright 2012-2016, 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 constants = require('./constants'); |
| 12 | + |
| 13 | +module.exports = function findAllPaths(pathinfo) { |
| 14 | + var cnt, |
| 15 | + startLoc, |
| 16 | + i, |
| 17 | + pi, |
| 18 | + j; |
| 19 | + |
| 20 | + for(i = 0; i < pathinfo.length; i++) { |
| 21 | + pi = pathinfo[i]; |
| 22 | + |
| 23 | + for(j = 0; j < pi.starts.length; j++) { |
| 24 | + startLoc = pi.starts[j]; |
| 25 | + makePath(pi, startLoc, 'edge'); |
| 26 | + } |
| 27 | + |
| 28 | + cnt = 0; |
| 29 | + while(Object.keys(pi.crossings).length && cnt < 10000) { |
| 30 | + cnt++; |
| 31 | + startLoc = Object.keys(pi.crossings)[0].split(',').map(Number); |
| 32 | + makePath(pi, startLoc); |
| 33 | + } |
| 34 | + if(cnt === 10000) Lib.log('Infinite loop in contour?'); |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +function equalPts(pt1, pt2) { |
| 39 | + return Math.abs(pt1[0] - pt2[0]) < 0.01 && |
| 40 | + Math.abs(pt1[1] - pt2[1]) < 0.01; |
| 41 | +} |
| 42 | + |
| 43 | +function ptDist(pt1, pt2) { |
| 44 | + var dx = pt1[0] - pt2[0], |
| 45 | + dy = pt1[1] - pt2[1]; |
| 46 | + return Math.sqrt(dx * dx + dy * dy); |
| 47 | +} |
| 48 | + |
| 49 | +function makePath(pi, loc, edgeflag) { |
| 50 | + var startLocStr = loc.join(','), |
| 51 | + locStr = startLocStr, |
| 52 | + mi = pi.crossings[locStr], |
| 53 | + marchStep = startStep(mi, edgeflag, loc), |
| 54 | + // start by going backward a half step and finding the crossing point |
| 55 | + pts = [getInterpPx(pi, loc, [-marchStep[0], -marchStep[1]])], |
| 56 | + startStepStr = marchStep.join(','), |
| 57 | + m = pi.z.length, |
| 58 | + n = pi.z[0].length, |
| 59 | + cnt; |
| 60 | + |
| 61 | + // now follow the path |
| 62 | + for(cnt = 0; cnt < 10000; cnt++) { // just to avoid infinite loops |
| 63 | + if(mi > 20) { |
| 64 | + mi = constants.CHOOSESADDLE[mi][(marchStep[0] || marchStep[1]) < 0 ? 0 : 1]; |
| 65 | + pi.crossings[locStr] = constants.SADDLEREMAINDER[mi]; |
| 66 | + } |
| 67 | + else { |
| 68 | + delete pi.crossings[locStr]; |
| 69 | + } |
| 70 | + |
| 71 | + marchStep = constants.NEWDELTA[mi]; |
| 72 | + if(!marchStep) { |
| 73 | + Lib.log('Found bad marching index:', mi, loc, pi.level); |
| 74 | + break; |
| 75 | + } |
| 76 | + |
| 77 | + // find the crossing a half step forward, and then take the full step |
| 78 | + pts.push(getInterpPx(pi, loc, marchStep)); |
| 79 | + loc[0] += marchStep[0]; |
| 80 | + loc[1] += marchStep[1]; |
| 81 | + |
| 82 | + // don't include the same point multiple times |
| 83 | + if(equalPts(pts[pts.length - 1], pts[pts.length - 2])) pts.pop(); |
| 84 | + locStr = loc.join(','); |
| 85 | + |
| 86 | + // have we completed a loop, or reached an edge? |
| 87 | + if((locStr === startLocStr && marchStep.join(',') === startStepStr) || |
| 88 | + (edgeflag && ( |
| 89 | + (marchStep[0] && (loc[0] < 0 || loc[0] > n - 2)) || |
| 90 | + (marchStep[1] && (loc[1] < 0 || loc[1] > m - 2))))) { |
| 91 | + break; |
| 92 | + } |
| 93 | + mi = pi.crossings[locStr]; |
| 94 | + } |
| 95 | + |
| 96 | + if(cnt === 10000) { |
| 97 | + Lib.log('Infinite loop in contour?'); |
| 98 | + } |
| 99 | + var closedpath = equalPts(pts[0], pts[pts.length - 1]), |
| 100 | + totaldist = 0, |
| 101 | + distThresholdFactor = 0.2 * pi.smoothing, |
| 102 | + alldists = [], |
| 103 | + cropstart = 0, |
| 104 | + distgroup, |
| 105 | + cnt2, |
| 106 | + cnt3, |
| 107 | + newpt, |
| 108 | + ptcnt, |
| 109 | + ptavg, |
| 110 | + thisdist; |
| 111 | + |
| 112 | + // check for points that are too close together (<1/5 the average dist, |
| 113 | + // less if less smoothed) and just take the center (or avg of center 2) |
| 114 | + // this cuts down on funny behavior when a point is very close to a contour level |
| 115 | + for(cnt = 1; cnt < pts.length; cnt++) { |
| 116 | + thisdist = ptDist(pts[cnt], pts[cnt - 1]); |
| 117 | + totaldist += thisdist; |
| 118 | + alldists.push(thisdist); |
| 119 | + } |
| 120 | + |
| 121 | + var distThreshold = totaldist / alldists.length * distThresholdFactor; |
| 122 | + |
| 123 | + function getpt(i) { return pts[i % pts.length]; } |
| 124 | + |
| 125 | + for(cnt = pts.length - 2; cnt >= cropstart; cnt--) { |
| 126 | + distgroup = alldists[cnt]; |
| 127 | + if(distgroup < distThreshold) { |
| 128 | + cnt3 = 0; |
| 129 | + for(cnt2 = cnt - 1; cnt2 >= cropstart; cnt2--) { |
| 130 | + if(distgroup + alldists[cnt2] < distThreshold) { |
| 131 | + distgroup += alldists[cnt2]; |
| 132 | + } |
| 133 | + else break; |
| 134 | + } |
| 135 | + |
| 136 | + // closed path with close points wrapping around the boundary? |
| 137 | + if(closedpath && cnt === pts.length - 2) { |
| 138 | + for(cnt3 = 0; cnt3 < cnt2; cnt3++) { |
| 139 | + if(distgroup + alldists[cnt3] < distThreshold) { |
| 140 | + distgroup += alldists[cnt3]; |
| 141 | + } |
| 142 | + else break; |
| 143 | + } |
| 144 | + } |
| 145 | + ptcnt = cnt - cnt2 + cnt3 + 1; |
| 146 | + ptavg = Math.floor((cnt + cnt2 + cnt3 + 2) / 2); |
| 147 | + |
| 148 | + // either endpoint included: keep the endpoint |
| 149 | + if(!closedpath && cnt === pts.length - 2) newpt = pts[pts.length - 1]; |
| 150 | + else if(!closedpath && cnt2 === -1) newpt = pts[0]; |
| 151 | + |
| 152 | + // odd # of points - just take the central one |
| 153 | + else if(ptcnt % 2) newpt = getpt(ptavg); |
| 154 | + |
| 155 | + // even # of pts - average central two |
| 156 | + else { |
| 157 | + newpt = [(getpt(ptavg)[0] + getpt(ptavg + 1)[0]) / 2, |
| 158 | + (getpt(ptavg)[1] + getpt(ptavg + 1)[1]) / 2]; |
| 159 | + } |
| 160 | + |
| 161 | + pts.splice(cnt2 + 1, cnt - cnt2 + 1, newpt); |
| 162 | + cnt = cnt2 + 1; |
| 163 | + if(cnt3) cropstart = cnt3; |
| 164 | + if(closedpath) { |
| 165 | + if(cnt === pts.length - 2) pts[cnt3] = pts[pts.length - 1]; |
| 166 | + else if(cnt === 0) pts[pts.length - 1] = pts[0]; |
| 167 | + } |
| 168 | + } |
| 169 | + } |
| 170 | + pts.splice(0, cropstart); |
| 171 | + |
| 172 | + // don't return single-point paths (ie all points were the same |
| 173 | + // so they got deleted?) |
| 174 | + if(pts.length < 2) return; |
| 175 | + else if(closedpath) { |
| 176 | + pts.pop(); |
| 177 | + pi.paths.push(pts); |
| 178 | + } |
| 179 | + else { |
| 180 | + if(!edgeflag) { |
| 181 | + Lib.log('Unclosed interior contour?', |
| 182 | + pi.level, startLocStr, pts.join('L')); |
| 183 | + } |
| 184 | + |
| 185 | + // edge path - does it start where an existing edge path ends, or vice versa? |
| 186 | + var merged = false; |
| 187 | + pi.edgepaths.forEach(function(edgepath, edgei) { |
| 188 | + if(!merged && equalPts(edgepath[0], pts[pts.length - 1])) { |
| 189 | + pts.pop(); |
| 190 | + merged = true; |
| 191 | + |
| 192 | + // now does it ALSO meet the end of another (or the same) path? |
| 193 | + var doublemerged = false; |
| 194 | + pi.edgepaths.forEach(function(edgepath2, edgei2) { |
| 195 | + if(!doublemerged && equalPts( |
| 196 | + edgepath2[edgepath2.length - 1], pts[0])) { |
| 197 | + doublemerged = true; |
| 198 | + pts.splice(0, 1); |
| 199 | + pi.edgepaths.splice(edgei, 1); |
| 200 | + if(edgei2 === edgei) { |
| 201 | + // the path is now closed |
| 202 | + pi.paths.push(pts.concat(edgepath2)); |
| 203 | + } |
| 204 | + else { |
| 205 | + pi.edgepaths[edgei2] = |
| 206 | + pi.edgepaths[edgei2].concat(pts, edgepath2); |
| 207 | + } |
| 208 | + } |
| 209 | + }); |
| 210 | + if(!doublemerged) { |
| 211 | + pi.edgepaths[edgei] = pts.concat(edgepath); |
| 212 | + } |
| 213 | + } |
| 214 | + }); |
| 215 | + pi.edgepaths.forEach(function(edgepath, edgei) { |
| 216 | + if(!merged && equalPts(edgepath[edgepath.length - 1], pts[0])) { |
| 217 | + pts.splice(0, 1); |
| 218 | + pi.edgepaths[edgei] = edgepath.concat(pts); |
| 219 | + merged = true; |
| 220 | + } |
| 221 | + }); |
| 222 | + |
| 223 | + if(!merged) pi.edgepaths.push(pts); |
| 224 | + } |
| 225 | +} |
| 226 | + |
| 227 | +// special function to get the marching step of the |
| 228 | +// first point in the path (leading to loc) |
| 229 | +function startStep(mi, edgeflag, loc) { |
| 230 | + var dx = 0, |
| 231 | + dy = 0; |
| 232 | + if(mi > 20 && edgeflag) { |
| 233 | + // these saddles start at +/- x |
| 234 | + if(mi === 208 || mi === 1114) { |
| 235 | + // if we're starting at the left side, we must be going right |
| 236 | + dx = loc[0] === 0 ? 1 : -1; |
| 237 | + } |
| 238 | + else { |
| 239 | + // if we're starting at the bottom, we must be going up |
| 240 | + dy = loc[1] === 0 ? 1 : -1; |
| 241 | + } |
| 242 | + } |
| 243 | + else if(constants.BOTTOMSTART.indexOf(mi) !== -1) dy = 1; |
| 244 | + else if(constants.LEFTSTART.indexOf(mi) !== -1) dx = 1; |
| 245 | + else if(constants.TOPSTART.indexOf(mi) !== -1) dy = -1; |
| 246 | + else dx = -1; |
| 247 | + return [dx, dy]; |
| 248 | +} |
| 249 | + |
| 250 | +function getInterpPx(pi, loc, step) { |
| 251 | + var locx = loc[0] + Math.max(step[0], 0), |
| 252 | + locy = loc[1] + Math.max(step[1], 0), |
| 253 | + zxy = pi.z[locy][locx], |
| 254 | + xa = pi.xaxis, |
| 255 | + ya = pi.yaxis; |
| 256 | + |
| 257 | + if(step[1]) { |
| 258 | + var dx = (pi.level - zxy) / (pi.z[locy][locx + 1] - zxy); |
| 259 | + return [xa.c2p((1 - dx) * pi.x[locx] + dx * pi.x[locx + 1], true), |
| 260 | + ya.c2p(pi.y[locy], true)]; |
| 261 | + } |
| 262 | + else { |
| 263 | + var dy = (pi.level - zxy) / (pi.z[locy + 1][locx] - zxy); |
| 264 | + return [xa.c2p(pi.x[locx], true), |
| 265 | + ya.c2p((1 - dy) * pi.y[locy] + dy * pi.y[locy + 1], true)]; |
| 266 | + } |
| 267 | +} |
| 268 | + |
0 commit comments