labelGuideHelper.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one
  3. * or more contributor license agreements. See the NOTICE file
  4. * distributed with this work for additional information
  5. * regarding copyright ownership. The ASF licenses this file
  6. * to you under the Apache License, Version 2.0 (the
  7. * "License"); you may not use this file except in compliance
  8. * with the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing,
  13. * software distributed under the License is distributed on an
  14. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  15. * KIND, either express or implied. See the License for the
  16. * specific language governing permissions and limitations
  17. * under the License.
  18. */
  19. /**
  20. * AUTO-GENERATED FILE. DO NOT MODIFY.
  21. */
  22. /*
  23. * Licensed to the Apache Software Foundation (ASF) under one
  24. * or more contributor license agreements. See the NOTICE file
  25. * distributed with this work for additional information
  26. * regarding copyright ownership. The ASF licenses this file
  27. * to you under the Apache License, Version 2.0 (the
  28. * "License"); you may not use this file except in compliance
  29. * with the License. You may obtain a copy of the License at
  30. *
  31. * http://www.apache.org/licenses/LICENSE-2.0
  32. *
  33. * Unless required by applicable law or agreed to in writing,
  34. * software distributed under the License is distributed on an
  35. * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
  36. * KIND, either express or implied. See the License for the
  37. * specific language governing permissions and limitations
  38. * under the License.
  39. */
  40. import { Point, Path, Polyline } from '../util/graphic.js';
  41. import PathProxy from 'zrender/lib/core/PathProxy.js';
  42. import { normalizeRadian } from 'zrender/lib/contain/util.js';
  43. import { cubicProjectPoint, quadraticProjectPoint } from 'zrender/lib/core/curve.js';
  44. import { defaults, retrieve2 } from 'zrender/lib/core/util.js';
  45. import { invert } from 'zrender/lib/core/matrix.js';
  46. import * as vector from 'zrender/lib/core/vector.js';
  47. import { DISPLAY_STATES, SPECIAL_STATES } from '../util/states.js';
  48. var PI2 = Math.PI * 2;
  49. var CMD = PathProxy.CMD;
  50. var DEFAULT_SEARCH_SPACE = ['top', 'right', 'bottom', 'left'];
  51. function getCandidateAnchor(pos, distance, rect, outPt, outDir) {
  52. var width = rect.width;
  53. var height = rect.height;
  54. switch (pos) {
  55. case 'top':
  56. outPt.set(rect.x + width / 2, rect.y - distance);
  57. outDir.set(0, -1);
  58. break;
  59. case 'bottom':
  60. outPt.set(rect.x + width / 2, rect.y + height + distance);
  61. outDir.set(0, 1);
  62. break;
  63. case 'left':
  64. outPt.set(rect.x - distance, rect.y + height / 2);
  65. outDir.set(-1, 0);
  66. break;
  67. case 'right':
  68. outPt.set(rect.x + width + distance, rect.y + height / 2);
  69. outDir.set(1, 0);
  70. break;
  71. }
  72. }
  73. function projectPointToArc(cx, cy, r, startAngle, endAngle, anticlockwise, x, y, out) {
  74. x -= cx;
  75. y -= cy;
  76. var d = Math.sqrt(x * x + y * y);
  77. x /= d;
  78. y /= d;
  79. // Intersect point.
  80. var ox = x * r + cx;
  81. var oy = y * r + cy;
  82. if (Math.abs(startAngle - endAngle) % PI2 < 1e-4) {
  83. // Is a circle
  84. out[0] = ox;
  85. out[1] = oy;
  86. return d - r;
  87. }
  88. if (anticlockwise) {
  89. var tmp = startAngle;
  90. startAngle = normalizeRadian(endAngle);
  91. endAngle = normalizeRadian(tmp);
  92. } else {
  93. startAngle = normalizeRadian(startAngle);
  94. endAngle = normalizeRadian(endAngle);
  95. }
  96. if (startAngle > endAngle) {
  97. endAngle += PI2;
  98. }
  99. var angle = Math.atan2(y, x);
  100. if (angle < 0) {
  101. angle += PI2;
  102. }
  103. if (angle >= startAngle && angle <= endAngle || angle + PI2 >= startAngle && angle + PI2 <= endAngle) {
  104. // Project point is on the arc.
  105. out[0] = ox;
  106. out[1] = oy;
  107. return d - r;
  108. }
  109. var x1 = r * Math.cos(startAngle) + cx;
  110. var y1 = r * Math.sin(startAngle) + cy;
  111. var x2 = r * Math.cos(endAngle) + cx;
  112. var y2 = r * Math.sin(endAngle) + cy;
  113. var d1 = (x1 - x) * (x1 - x) + (y1 - y) * (y1 - y);
  114. var d2 = (x2 - x) * (x2 - x) + (y2 - y) * (y2 - y);
  115. if (d1 < d2) {
  116. out[0] = x1;
  117. out[1] = y1;
  118. return Math.sqrt(d1);
  119. } else {
  120. out[0] = x2;
  121. out[1] = y2;
  122. return Math.sqrt(d2);
  123. }
  124. }
  125. function projectPointToLine(x1, y1, x2, y2, x, y, out, limitToEnds) {
  126. var dx = x - x1;
  127. var dy = y - y1;
  128. var dx1 = x2 - x1;
  129. var dy1 = y2 - y1;
  130. var lineLen = Math.sqrt(dx1 * dx1 + dy1 * dy1);
  131. dx1 /= lineLen;
  132. dy1 /= lineLen;
  133. // dot product
  134. var projectedLen = dx * dx1 + dy * dy1;
  135. var t = projectedLen / lineLen;
  136. if (limitToEnds) {
  137. t = Math.min(Math.max(t, 0), 1);
  138. }
  139. t *= lineLen;
  140. var ox = out[0] = x1 + t * dx1;
  141. var oy = out[1] = y1 + t * dy1;
  142. return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y));
  143. }
  144. function projectPointToRect(x1, y1, width, height, x, y, out) {
  145. if (width < 0) {
  146. x1 = x1 + width;
  147. width = -width;
  148. }
  149. if (height < 0) {
  150. y1 = y1 + height;
  151. height = -height;
  152. }
  153. var x2 = x1 + width;
  154. var y2 = y1 + height;
  155. var ox = out[0] = Math.min(Math.max(x, x1), x2);
  156. var oy = out[1] = Math.min(Math.max(y, y1), y2);
  157. return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y));
  158. }
  159. var tmpPt = [];
  160. function nearestPointOnRect(pt, rect, out) {
  161. var dist = projectPointToRect(rect.x, rect.y, rect.width, rect.height, pt.x, pt.y, tmpPt);
  162. out.set(tmpPt[0], tmpPt[1]);
  163. return dist;
  164. }
  165. /**
  166. * Calculate min distance corresponding point.
  167. * This method won't evaluate if point is in the path.
  168. */
  169. function nearestPointOnPath(pt, path, out) {
  170. var xi = 0;
  171. var yi = 0;
  172. var x0 = 0;
  173. var y0 = 0;
  174. var x1;
  175. var y1;
  176. var minDist = Infinity;
  177. var data = path.data;
  178. var x = pt.x;
  179. var y = pt.y;
  180. for (var i = 0; i < data.length;) {
  181. var cmd = data[i++];
  182. if (i === 1) {
  183. xi = data[i];
  184. yi = data[i + 1];
  185. x0 = xi;
  186. y0 = yi;
  187. }
  188. var d = minDist;
  189. switch (cmd) {
  190. case CMD.M:
  191. // moveTo 命令重新创建一个新的 subpath, 并且更新新的起点
  192. // 在 closePath 的时候使用
  193. x0 = data[i++];
  194. y0 = data[i++];
  195. xi = x0;
  196. yi = y0;
  197. break;
  198. case CMD.L:
  199. d = projectPointToLine(xi, yi, data[i], data[i + 1], x, y, tmpPt, true);
  200. xi = data[i++];
  201. yi = data[i++];
  202. break;
  203. case CMD.C:
  204. d = cubicProjectPoint(xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], x, y, tmpPt);
  205. xi = data[i++];
  206. yi = data[i++];
  207. break;
  208. case CMD.Q:
  209. d = quadraticProjectPoint(xi, yi, data[i++], data[i++], data[i], data[i + 1], x, y, tmpPt);
  210. xi = data[i++];
  211. yi = data[i++];
  212. break;
  213. case CMD.A:
  214. // TODO Arc 判断的开销比较大
  215. var cx = data[i++];
  216. var cy = data[i++];
  217. var rx = data[i++];
  218. var ry = data[i++];
  219. var theta = data[i++];
  220. var dTheta = data[i++];
  221. // TODO Arc 旋转
  222. i += 1;
  223. var anticlockwise = !!(1 - data[i++]);
  224. x1 = Math.cos(theta) * rx + cx;
  225. y1 = Math.sin(theta) * ry + cy;
  226. // 不是直接使用 arc 命令
  227. if (i <= 1) {
  228. // 第一个命令起点还未定义
  229. x0 = x1;
  230. y0 = y1;
  231. }
  232. // zr 使用scale来模拟椭圆, 这里也对x做一定的缩放
  233. var _x = (x - cx) * ry / rx + cx;
  234. d = projectPointToArc(cx, cy, ry, theta, theta + dTheta, anticlockwise, _x, y, tmpPt);
  235. xi = Math.cos(theta + dTheta) * rx + cx;
  236. yi = Math.sin(theta + dTheta) * ry + cy;
  237. break;
  238. case CMD.R:
  239. x0 = xi = data[i++];
  240. y0 = yi = data[i++];
  241. var width = data[i++];
  242. var height = data[i++];
  243. d = projectPointToRect(x0, y0, width, height, x, y, tmpPt);
  244. break;
  245. case CMD.Z:
  246. d = projectPointToLine(xi, yi, x0, y0, x, y, tmpPt, true);
  247. xi = x0;
  248. yi = y0;
  249. break;
  250. }
  251. if (d < minDist) {
  252. minDist = d;
  253. out.set(tmpPt[0], tmpPt[1]);
  254. }
  255. }
  256. return minDist;
  257. }
  258. // Temporal variable for intermediate usage.
  259. var pt0 = new Point();
  260. var pt1 = new Point();
  261. var pt2 = new Point();
  262. var dir = new Point();
  263. var dir2 = new Point();
  264. /**
  265. * Calculate a proper guide line based on the label position and graphic element definition
  266. * @param label
  267. * @param labelRect
  268. * @param target
  269. * @param targetRect
  270. */
  271. export function updateLabelLinePoints(target, labelLineModel) {
  272. if (!target) {
  273. return;
  274. }
  275. var labelLine = target.getTextGuideLine();
  276. var label = target.getTextContent();
  277. // Needs to create text guide in each charts.
  278. if (!(label && labelLine)) {
  279. return;
  280. }
  281. var labelGuideConfig = target.textGuideLineConfig || {};
  282. var points = [[0, 0], [0, 0], [0, 0]];
  283. var searchSpace = labelGuideConfig.candidates || DEFAULT_SEARCH_SPACE;
  284. var labelRect = label.getBoundingRect().clone();
  285. labelRect.applyTransform(label.getComputedTransform());
  286. var minDist = Infinity;
  287. var anchorPoint = labelGuideConfig.anchor;
  288. var targetTransform = target.getComputedTransform();
  289. var targetInversedTransform = targetTransform && invert([], targetTransform);
  290. var len = labelLineModel.get('length2') || 0;
  291. if (anchorPoint) {
  292. pt2.copy(anchorPoint);
  293. }
  294. for (var i = 0; i < searchSpace.length; i++) {
  295. var candidate = searchSpace[i];
  296. getCandidateAnchor(candidate, 0, labelRect, pt0, dir);
  297. Point.scaleAndAdd(pt1, pt0, dir, len);
  298. // Transform to target coord space.
  299. pt1.transform(targetInversedTransform);
  300. // Note: getBoundingRect will ensure the `path` being created.
  301. var boundingRect = target.getBoundingRect();
  302. var dist = anchorPoint ? anchorPoint.distance(pt1) : target instanceof Path ? nearestPointOnPath(pt1, target.path, pt2) : nearestPointOnRect(pt1, boundingRect, pt2);
  303. // TODO pt2 is in the path
  304. if (dist < minDist) {
  305. minDist = dist;
  306. // Transform back to global space.
  307. pt1.transform(targetTransform);
  308. pt2.transform(targetTransform);
  309. pt2.toArray(points[0]);
  310. pt1.toArray(points[1]);
  311. pt0.toArray(points[2]);
  312. }
  313. }
  314. limitTurnAngle(points, labelLineModel.get('minTurnAngle'));
  315. labelLine.setShape({
  316. points: points
  317. });
  318. }
  319. // Temporal variable for the limitTurnAngle function
  320. var tmpArr = [];
  321. var tmpProjPoint = new Point();
  322. /**
  323. * Reduce the line segment attached to the label to limit the turn angle between two segments.
  324. * @param linePoints
  325. * @param minTurnAngle Radian of minimum turn angle. 0 - 180
  326. */
  327. export function limitTurnAngle(linePoints, minTurnAngle) {
  328. if (!(minTurnAngle <= 180 && minTurnAngle > 0)) {
  329. return;
  330. }
  331. minTurnAngle = minTurnAngle / 180 * Math.PI;
  332. // The line points can be
  333. // /pt1----pt2 (label)
  334. // /
  335. // pt0/
  336. pt0.fromArray(linePoints[0]);
  337. pt1.fromArray(linePoints[1]);
  338. pt2.fromArray(linePoints[2]);
  339. Point.sub(dir, pt0, pt1);
  340. Point.sub(dir2, pt2, pt1);
  341. var len1 = dir.len();
  342. var len2 = dir2.len();
  343. if (len1 < 1e-3 || len2 < 1e-3) {
  344. return;
  345. }
  346. dir.scale(1 / len1);
  347. dir2.scale(1 / len2);
  348. var angleCos = dir.dot(dir2);
  349. var minTurnAngleCos = Math.cos(minTurnAngle);
  350. if (minTurnAngleCos < angleCos) {
  351. // Smaller than minTurnAngle
  352. // Calculate project point of pt0 on pt1-pt2
  353. var d = projectPointToLine(pt1.x, pt1.y, pt2.x, pt2.y, pt0.x, pt0.y, tmpArr, false);
  354. tmpProjPoint.fromArray(tmpArr);
  355. // Calculate new projected length with limited minTurnAngle and get the new connect point
  356. tmpProjPoint.scaleAndAdd(dir2, d / Math.tan(Math.PI - minTurnAngle));
  357. // Limit the new calculated connect point between pt1 and pt2.
  358. var t = pt2.x !== pt1.x ? (tmpProjPoint.x - pt1.x) / (pt2.x - pt1.x) : (tmpProjPoint.y - pt1.y) / (pt2.y - pt1.y);
  359. if (isNaN(t)) {
  360. return;
  361. }
  362. if (t < 0) {
  363. Point.copy(tmpProjPoint, pt1);
  364. } else if (t > 1) {
  365. Point.copy(tmpProjPoint, pt2);
  366. }
  367. tmpProjPoint.toArray(linePoints[1]);
  368. }
  369. }
  370. /**
  371. * Limit the angle of line and the surface
  372. * @param maxSurfaceAngle Radian of minimum turn angle. 0 - 180. 0 is same direction to normal. 180 is opposite
  373. */
  374. export function limitSurfaceAngle(linePoints, surfaceNormal, maxSurfaceAngle) {
  375. if (!(maxSurfaceAngle <= 180 && maxSurfaceAngle > 0)) {
  376. return;
  377. }
  378. maxSurfaceAngle = maxSurfaceAngle / 180 * Math.PI;
  379. pt0.fromArray(linePoints[0]);
  380. pt1.fromArray(linePoints[1]);
  381. pt2.fromArray(linePoints[2]);
  382. Point.sub(dir, pt1, pt0);
  383. Point.sub(dir2, pt2, pt1);
  384. var len1 = dir.len();
  385. var len2 = dir2.len();
  386. if (len1 < 1e-3 || len2 < 1e-3) {
  387. return;
  388. }
  389. dir.scale(1 / len1);
  390. dir2.scale(1 / len2);
  391. var angleCos = dir.dot(surfaceNormal);
  392. var maxSurfaceAngleCos = Math.cos(maxSurfaceAngle);
  393. if (angleCos < maxSurfaceAngleCos) {
  394. // Calculate project point of pt0 on pt1-pt2
  395. var d = projectPointToLine(pt1.x, pt1.y, pt2.x, pt2.y, pt0.x, pt0.y, tmpArr, false);
  396. tmpProjPoint.fromArray(tmpArr);
  397. var HALF_PI = Math.PI / 2;
  398. var angle2 = Math.acos(dir2.dot(surfaceNormal));
  399. var newAngle = HALF_PI + angle2 - maxSurfaceAngle;
  400. if (newAngle >= HALF_PI) {
  401. // parallel
  402. Point.copy(tmpProjPoint, pt2);
  403. } else {
  404. // Calculate new projected length with limited minTurnAngle and get the new connect point
  405. tmpProjPoint.scaleAndAdd(dir2, d / Math.tan(Math.PI / 2 - newAngle));
  406. // Limit the new calculated connect point between pt1 and pt2.
  407. var t = pt2.x !== pt1.x ? (tmpProjPoint.x - pt1.x) / (pt2.x - pt1.x) : (tmpProjPoint.y - pt1.y) / (pt2.y - pt1.y);
  408. if (isNaN(t)) {
  409. return;
  410. }
  411. if (t < 0) {
  412. Point.copy(tmpProjPoint, pt1);
  413. } else if (t > 1) {
  414. Point.copy(tmpProjPoint, pt2);
  415. }
  416. }
  417. tmpProjPoint.toArray(linePoints[1]);
  418. }
  419. }
  420. function setLabelLineState(labelLine, ignore, stateName, stateModel) {
  421. var isNormal = stateName === 'normal';
  422. var stateObj = isNormal ? labelLine : labelLine.ensureState(stateName);
  423. // Make sure display.
  424. stateObj.ignore = ignore;
  425. // Set smooth
  426. var smooth = stateModel.get('smooth');
  427. if (smooth && smooth === true) {
  428. smooth = 0.3;
  429. }
  430. stateObj.shape = stateObj.shape || {};
  431. if (smooth > 0) {
  432. stateObj.shape.smooth = smooth;
  433. }
  434. var styleObj = stateModel.getModel('lineStyle').getLineStyle();
  435. isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj;
  436. }
  437. function buildLabelLinePath(path, shape) {
  438. var smooth = shape.smooth;
  439. var points = shape.points;
  440. if (!points) {
  441. return;
  442. }
  443. path.moveTo(points[0][0], points[0][1]);
  444. if (smooth > 0 && points.length >= 3) {
  445. var len1 = vector.dist(points[0], points[1]);
  446. var len2 = vector.dist(points[1], points[2]);
  447. if (!len1 || !len2) {
  448. path.lineTo(points[1][0], points[1][1]);
  449. path.lineTo(points[2][0], points[2][1]);
  450. return;
  451. }
  452. var moveLen = Math.min(len1, len2) * smooth;
  453. var midPoint0 = vector.lerp([], points[1], points[0], moveLen / len1);
  454. var midPoint2 = vector.lerp([], points[1], points[2], moveLen / len2);
  455. var midPoint1 = vector.lerp([], midPoint0, midPoint2, 0.5);
  456. path.bezierCurveTo(midPoint0[0], midPoint0[1], midPoint0[0], midPoint0[1], midPoint1[0], midPoint1[1]);
  457. path.bezierCurveTo(midPoint2[0], midPoint2[1], midPoint2[0], midPoint2[1], points[2][0], points[2][1]);
  458. } else {
  459. for (var i = 1; i < points.length; i++) {
  460. path.lineTo(points[i][0], points[i][1]);
  461. }
  462. }
  463. }
  464. /**
  465. * Create a label line if necessary and set it's style.
  466. */
  467. export function setLabelLineStyle(targetEl, statesModels, defaultStyle) {
  468. var labelLine = targetEl.getTextGuideLine();
  469. var label = targetEl.getTextContent();
  470. if (!label) {
  471. // Not show label line if there is no label.
  472. if (labelLine) {
  473. targetEl.removeTextGuideLine();
  474. }
  475. return;
  476. }
  477. var normalModel = statesModels.normal;
  478. var showNormal = normalModel.get('show');
  479. var labelIgnoreNormal = label.ignore;
  480. for (var i = 0; i < DISPLAY_STATES.length; i++) {
  481. var stateName = DISPLAY_STATES[i];
  482. var stateModel = statesModels[stateName];
  483. var isNormal = stateName === 'normal';
  484. if (stateModel) {
  485. var stateShow = stateModel.get('show');
  486. var isLabelIgnored = isNormal ? labelIgnoreNormal : retrieve2(label.states[stateName] && label.states[stateName].ignore, labelIgnoreNormal);
  487. if (isLabelIgnored // Not show when label is not shown in this state.
  488. || !retrieve2(stateShow, showNormal) // Use normal state by default if not set.
  489. ) {
  490. var stateObj = isNormal ? labelLine : labelLine && labelLine.states[stateName];
  491. if (stateObj) {
  492. stateObj.ignore = true;
  493. }
  494. if (!!labelLine) {
  495. setLabelLineState(labelLine, true, stateName, stateModel);
  496. }
  497. continue;
  498. }
  499. // Create labelLine if not exists
  500. if (!labelLine) {
  501. labelLine = new Polyline();
  502. targetEl.setTextGuideLine(labelLine);
  503. // Reset state of normal because it's new created.
  504. // NOTE: NORMAL should always been the first!
  505. if (!isNormal && (labelIgnoreNormal || !showNormal)) {
  506. setLabelLineState(labelLine, true, 'normal', statesModels.normal);
  507. }
  508. // Use same state proxy.
  509. if (targetEl.stateProxy) {
  510. labelLine.stateProxy = targetEl.stateProxy;
  511. }
  512. }
  513. setLabelLineState(labelLine, false, stateName, stateModel);
  514. }
  515. }
  516. if (labelLine) {
  517. defaults(labelLine.style, defaultStyle);
  518. // Not fill.
  519. labelLine.style.fill = null;
  520. var showAbove = normalModel.get('showAbove');
  521. var labelLineConfig = targetEl.textGuideLineConfig = targetEl.textGuideLineConfig || {};
  522. labelLineConfig.showAbove = showAbove || false;
  523. // Custom the buildPath.
  524. labelLine.buildPath = buildLabelLinePath;
  525. }
  526. }
  527. export function getLabelLineStatesModels(itemModel, labelLineName) {
  528. labelLineName = labelLineName || 'labelLine';
  529. var statesModels = {
  530. normal: itemModel.getModel(labelLineName)
  531. };
  532. for (var i = 0; i < SPECIAL_STATES.length; i++) {
  533. var stateName = SPECIAL_STATES[i];
  534. statesModels[stateName] = itemModel.getModel([stateName, labelLineName]);
  535. }
  536. return statesModels;
  537. }