You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
627 lines
18 KiB
627 lines
18 KiB
|
|
/* |
|
* Licensed to the Apache Software Foundation (ASF) under one |
|
* or more contributor license agreements. See the NOTICE file |
|
* distributed with this work for additional information |
|
* regarding copyright ownership. The ASF licenses this file |
|
* to you under the Apache License, Version 2.0 (the |
|
* "License"); you may not use this file except in compliance |
|
* with the License. You may obtain a copy of the License at |
|
* |
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
* |
|
* Unless required by applicable law or agreed to in writing, |
|
* software distributed under the License is distributed on an |
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
|
* KIND, either express or implied. See the License for the |
|
* specific language governing permissions and limitations |
|
* under the License. |
|
*/ |
|
|
|
|
|
/** |
|
* AUTO-GENERATED FILE. DO NOT MODIFY. |
|
*/ |
|
|
|
/* |
|
* Licensed to the Apache Software Foundation (ASF) under one |
|
* or more contributor license agreements. See the NOTICE file |
|
* distributed with this work for additional information |
|
* regarding copyright ownership. The ASF licenses this file |
|
* to you under the Apache License, Version 2.0 (the |
|
* "License"); you may not use this file except in compliance |
|
* with the License. You may obtain a copy of the License at |
|
* |
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
* |
|
* Unless required by applicable law or agreed to in writing, |
|
* software distributed under the License is distributed on an |
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
|
* KIND, either express or implied. See the License for the |
|
* specific language governing permissions and limitations |
|
* under the License. |
|
*/ |
|
import { Point, Path, Polyline } from '../util/graphic.js'; |
|
import PathProxy from 'zrender/lib/core/PathProxy.js'; |
|
import { normalizeRadian } from 'zrender/lib/contain/util.js'; |
|
import { cubicProjectPoint, quadraticProjectPoint } from 'zrender/lib/core/curve.js'; |
|
import { defaults, retrieve2 } from 'zrender/lib/core/util.js'; |
|
import { invert } from 'zrender/lib/core/matrix.js'; |
|
import * as vector from 'zrender/lib/core/vector.js'; |
|
import { DISPLAY_STATES, SPECIAL_STATES } from '../util/states.js'; |
|
var PI2 = Math.PI * 2; |
|
var CMD = PathProxy.CMD; |
|
var DEFAULT_SEARCH_SPACE = ['top', 'right', 'bottom', 'left']; |
|
|
|
function getCandidateAnchor(pos, distance, rect, outPt, outDir) { |
|
var width = rect.width; |
|
var height = rect.height; |
|
|
|
switch (pos) { |
|
case 'top': |
|
outPt.set(rect.x + width / 2, rect.y - distance); |
|
outDir.set(0, -1); |
|
break; |
|
|
|
case 'bottom': |
|
outPt.set(rect.x + width / 2, rect.y + height + distance); |
|
outDir.set(0, 1); |
|
break; |
|
|
|
case 'left': |
|
outPt.set(rect.x - distance, rect.y + height / 2); |
|
outDir.set(-1, 0); |
|
break; |
|
|
|
case 'right': |
|
outPt.set(rect.x + width + distance, rect.y + height / 2); |
|
outDir.set(1, 0); |
|
break; |
|
} |
|
} |
|
|
|
function projectPointToArc(cx, cy, r, startAngle, endAngle, anticlockwise, x, y, out) { |
|
x -= cx; |
|
y -= cy; |
|
var d = Math.sqrt(x * x + y * y); |
|
x /= d; |
|
y /= d; // Intersect point. |
|
|
|
var ox = x * r + cx; |
|
var oy = y * r + cy; |
|
|
|
if (Math.abs(startAngle - endAngle) % PI2 < 1e-4) { |
|
// Is a circle |
|
out[0] = ox; |
|
out[1] = oy; |
|
return d - r; |
|
} |
|
|
|
if (anticlockwise) { |
|
var tmp = startAngle; |
|
startAngle = normalizeRadian(endAngle); |
|
endAngle = normalizeRadian(tmp); |
|
} else { |
|
startAngle = normalizeRadian(startAngle); |
|
endAngle = normalizeRadian(endAngle); |
|
} |
|
|
|
if (startAngle > endAngle) { |
|
endAngle += PI2; |
|
} |
|
|
|
var angle = Math.atan2(y, x); |
|
|
|
if (angle < 0) { |
|
angle += PI2; |
|
} |
|
|
|
if (angle >= startAngle && angle <= endAngle || angle + PI2 >= startAngle && angle + PI2 <= endAngle) { |
|
// Project point is on the arc. |
|
out[0] = ox; |
|
out[1] = oy; |
|
return d - r; |
|
} |
|
|
|
var x1 = r * Math.cos(startAngle) + cx; |
|
var y1 = r * Math.sin(startAngle) + cy; |
|
var x2 = r * Math.cos(endAngle) + cx; |
|
var y2 = r * Math.sin(endAngle) + cy; |
|
var d1 = (x1 - x) * (x1 - x) + (y1 - y) * (y1 - y); |
|
var d2 = (x2 - x) * (x2 - x) + (y2 - y) * (y2 - y); |
|
|
|
if (d1 < d2) { |
|
out[0] = x1; |
|
out[1] = y1; |
|
return Math.sqrt(d1); |
|
} else { |
|
out[0] = x2; |
|
out[1] = y2; |
|
return Math.sqrt(d2); |
|
} |
|
} |
|
|
|
function projectPointToLine(x1, y1, x2, y2, x, y, out, limitToEnds) { |
|
var dx = x - x1; |
|
var dy = y - y1; |
|
var dx1 = x2 - x1; |
|
var dy1 = y2 - y1; |
|
var lineLen = Math.sqrt(dx1 * dx1 + dy1 * dy1); |
|
dx1 /= lineLen; |
|
dy1 /= lineLen; // dot product |
|
|
|
var projectedLen = dx * dx1 + dy * dy1; |
|
var t = projectedLen / lineLen; |
|
|
|
if (limitToEnds) { |
|
t = Math.min(Math.max(t, 0), 1); |
|
} |
|
|
|
t *= lineLen; |
|
var ox = out[0] = x1 + t * dx1; |
|
var oy = out[1] = y1 + t * dy1; |
|
return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y)); |
|
} |
|
|
|
function projectPointToRect(x1, y1, width, height, x, y, out) { |
|
if (width < 0) { |
|
x1 = x1 + width; |
|
width = -width; |
|
} |
|
|
|
if (height < 0) { |
|
y1 = y1 + height; |
|
height = -height; |
|
} |
|
|
|
var x2 = x1 + width; |
|
var y2 = y1 + height; |
|
var ox = out[0] = Math.min(Math.max(x, x1), x2); |
|
var oy = out[1] = Math.min(Math.max(y, y1), y2); |
|
return Math.sqrt((ox - x) * (ox - x) + (oy - y) * (oy - y)); |
|
} |
|
|
|
var tmpPt = []; |
|
|
|
function nearestPointOnRect(pt, rect, out) { |
|
var dist = projectPointToRect(rect.x, rect.y, rect.width, rect.height, pt.x, pt.y, tmpPt); |
|
out.set(tmpPt[0], tmpPt[1]); |
|
return dist; |
|
} |
|
/** |
|
* Calculate min distance corresponding point. |
|
* This method won't evaluate if point is in the path. |
|
*/ |
|
|
|
|
|
function nearestPointOnPath(pt, path, out) { |
|
var xi = 0; |
|
var yi = 0; |
|
var x0 = 0; |
|
var y0 = 0; |
|
var x1; |
|
var y1; |
|
var minDist = Infinity; |
|
var data = path.data; |
|
var x = pt.x; |
|
var y = pt.y; |
|
|
|
for (var i = 0; i < data.length;) { |
|
var cmd = data[i++]; |
|
|
|
if (i === 1) { |
|
xi = data[i]; |
|
yi = data[i + 1]; |
|
x0 = xi; |
|
y0 = yi; |
|
} |
|
|
|
var d = minDist; |
|
|
|
switch (cmd) { |
|
case CMD.M: |
|
// moveTo 命令重新创建一个新的 subpath, 并且更新新的起点 |
|
// 在 closePath 的时候使用 |
|
x0 = data[i++]; |
|
y0 = data[i++]; |
|
xi = x0; |
|
yi = y0; |
|
break; |
|
|
|
case CMD.L: |
|
d = projectPointToLine(xi, yi, data[i], data[i + 1], x, y, tmpPt, true); |
|
xi = data[i++]; |
|
yi = data[i++]; |
|
break; |
|
|
|
case CMD.C: |
|
d = cubicProjectPoint(xi, yi, data[i++], data[i++], data[i++], data[i++], data[i], data[i + 1], x, y, tmpPt); |
|
xi = data[i++]; |
|
yi = data[i++]; |
|
break; |
|
|
|
case CMD.Q: |
|
d = quadraticProjectPoint(xi, yi, data[i++], data[i++], data[i], data[i + 1], x, y, tmpPt); |
|
xi = data[i++]; |
|
yi = data[i++]; |
|
break; |
|
|
|
case CMD.A: |
|
// TODO Arc 判断的开销比较大 |
|
var cx = data[i++]; |
|
var cy = data[i++]; |
|
var rx = data[i++]; |
|
var ry = data[i++]; |
|
var theta = data[i++]; |
|
var dTheta = data[i++]; // TODO Arc 旋转 |
|
|
|
i += 1; |
|
var anticlockwise = !!(1 - data[i++]); |
|
x1 = Math.cos(theta) * rx + cx; |
|
y1 = Math.sin(theta) * ry + cy; // 不是直接使用 arc 命令 |
|
|
|
if (i <= 1) { |
|
// 第一个命令起点还未定义 |
|
x0 = x1; |
|
y0 = y1; |
|
} // zr 使用scale来模拟椭圆, 这里也对x做一定的缩放 |
|
|
|
|
|
var _x = (x - cx) * ry / rx + cx; |
|
|
|
d = projectPointToArc(cx, cy, ry, theta, theta + dTheta, anticlockwise, _x, y, tmpPt); |
|
xi = Math.cos(theta + dTheta) * rx + cx; |
|
yi = Math.sin(theta + dTheta) * ry + cy; |
|
break; |
|
|
|
case CMD.R: |
|
x0 = xi = data[i++]; |
|
y0 = yi = data[i++]; |
|
var width = data[i++]; |
|
var height = data[i++]; |
|
d = projectPointToRect(x0, y0, width, height, x, y, tmpPt); |
|
break; |
|
|
|
case CMD.Z: |
|
d = projectPointToLine(xi, yi, x0, y0, x, y, tmpPt, true); |
|
xi = x0; |
|
yi = y0; |
|
break; |
|
} |
|
|
|
if (d < minDist) { |
|
minDist = d; |
|
out.set(tmpPt[0], tmpPt[1]); |
|
} |
|
} |
|
|
|
return minDist; |
|
} // Temporal variable for intermediate usage. |
|
|
|
|
|
var pt0 = new Point(); |
|
var pt1 = new Point(); |
|
var pt2 = new Point(); |
|
var dir = new Point(); |
|
var dir2 = new Point(); |
|
/** |
|
* Calculate a proper guide line based on the label position and graphic element definition |
|
* @param label |
|
* @param labelRect |
|
* @param target |
|
* @param targetRect |
|
*/ |
|
|
|
export function updateLabelLinePoints(target, labelLineModel) { |
|
if (!target) { |
|
return; |
|
} |
|
|
|
var labelLine = target.getTextGuideLine(); |
|
var label = target.getTextContent(); // Needs to create text guide in each charts. |
|
|
|
if (!(label && labelLine)) { |
|
return; |
|
} |
|
|
|
var labelGuideConfig = target.textGuideLineConfig || {}; |
|
var points = [[0, 0], [0, 0], [0, 0]]; |
|
var searchSpace = labelGuideConfig.candidates || DEFAULT_SEARCH_SPACE; |
|
var labelRect = label.getBoundingRect().clone(); |
|
labelRect.applyTransform(label.getComputedTransform()); |
|
var minDist = Infinity; |
|
var anchorPoint = labelGuideConfig.anchor; |
|
var targetTransform = target.getComputedTransform(); |
|
var targetInversedTransform = targetTransform && invert([], targetTransform); |
|
var len = labelLineModel.get('length2') || 0; |
|
|
|
if (anchorPoint) { |
|
pt2.copy(anchorPoint); |
|
} |
|
|
|
for (var i = 0; i < searchSpace.length; i++) { |
|
var candidate = searchSpace[i]; |
|
getCandidateAnchor(candidate, 0, labelRect, pt0, dir); |
|
Point.scaleAndAdd(pt1, pt0, dir, len); // Transform to target coord space. |
|
|
|
pt1.transform(targetInversedTransform); // Note: getBoundingRect will ensure the `path` being created. |
|
|
|
var boundingRect = target.getBoundingRect(); |
|
var dist = anchorPoint ? anchorPoint.distance(pt1) : target instanceof Path ? nearestPointOnPath(pt1, target.path, pt2) : nearestPointOnRect(pt1, boundingRect, pt2); // TODO pt2 is in the path |
|
|
|
if (dist < minDist) { |
|
minDist = dist; // Transform back to global space. |
|
|
|
pt1.transform(targetTransform); |
|
pt2.transform(targetTransform); |
|
pt2.toArray(points[0]); |
|
pt1.toArray(points[1]); |
|
pt0.toArray(points[2]); |
|
} |
|
} |
|
|
|
limitTurnAngle(points, labelLineModel.get('minTurnAngle')); |
|
labelLine.setShape({ |
|
points: points |
|
}); |
|
} // Temporal variable for the limitTurnAngle function |
|
|
|
var tmpArr = []; |
|
var tmpProjPoint = new Point(); |
|
/** |
|
* Reduce the line segment attached to the label to limit the turn angle between two segments. |
|
* @param linePoints |
|
* @param minTurnAngle Radian of minimum turn angle. 0 - 180 |
|
*/ |
|
|
|
export function limitTurnAngle(linePoints, minTurnAngle) { |
|
if (!(minTurnAngle <= 180 && minTurnAngle > 0)) { |
|
return; |
|
} |
|
|
|
minTurnAngle = minTurnAngle / 180 * Math.PI; // The line points can be |
|
// /pt1----pt2 (label) |
|
// / |
|
// pt0/ |
|
|
|
pt0.fromArray(linePoints[0]); |
|
pt1.fromArray(linePoints[1]); |
|
pt2.fromArray(linePoints[2]); |
|
Point.sub(dir, pt0, pt1); |
|
Point.sub(dir2, pt2, pt1); |
|
var len1 = dir.len(); |
|
var len2 = dir2.len(); |
|
|
|
if (len1 < 1e-3 || len2 < 1e-3) { |
|
return; |
|
} |
|
|
|
dir.scale(1 / len1); |
|
dir2.scale(1 / len2); |
|
var angleCos = dir.dot(dir2); |
|
var minTurnAngleCos = Math.cos(minTurnAngle); |
|
|
|
if (minTurnAngleCos < angleCos) { |
|
// Smaller than minTurnAngle |
|
// Calculate project point of pt0 on pt1-pt2 |
|
var d = projectPointToLine(pt1.x, pt1.y, pt2.x, pt2.y, pt0.x, pt0.y, tmpArr, false); |
|
tmpProjPoint.fromArray(tmpArr); // Calculate new projected length with limited minTurnAngle and get the new connect point |
|
|
|
tmpProjPoint.scaleAndAdd(dir2, d / Math.tan(Math.PI - minTurnAngle)); // Limit the new calculated connect point between pt1 and pt2. |
|
|
|
var t = pt2.x !== pt1.x ? (tmpProjPoint.x - pt1.x) / (pt2.x - pt1.x) : (tmpProjPoint.y - pt1.y) / (pt2.y - pt1.y); |
|
|
|
if (isNaN(t)) { |
|
return; |
|
} |
|
|
|
if (t < 0) { |
|
Point.copy(tmpProjPoint, pt1); |
|
} else if (t > 1) { |
|
Point.copy(tmpProjPoint, pt2); |
|
} |
|
|
|
tmpProjPoint.toArray(linePoints[1]); |
|
} |
|
} |
|
/** |
|
* Limit the angle of line and the surface |
|
* @param maxSurfaceAngle Radian of minimum turn angle. 0 - 180. 0 is same direction to normal. 180 is opposite |
|
*/ |
|
|
|
export function limitSurfaceAngle(linePoints, surfaceNormal, maxSurfaceAngle) { |
|
if (!(maxSurfaceAngle <= 180 && maxSurfaceAngle > 0)) { |
|
return; |
|
} |
|
|
|
maxSurfaceAngle = maxSurfaceAngle / 180 * Math.PI; |
|
pt0.fromArray(linePoints[0]); |
|
pt1.fromArray(linePoints[1]); |
|
pt2.fromArray(linePoints[2]); |
|
Point.sub(dir, pt1, pt0); |
|
Point.sub(dir2, pt2, pt1); |
|
var len1 = dir.len(); |
|
var len2 = dir2.len(); |
|
|
|
if (len1 < 1e-3 || len2 < 1e-3) { |
|
return; |
|
} |
|
|
|
dir.scale(1 / len1); |
|
dir2.scale(1 / len2); |
|
var angleCos = dir.dot(surfaceNormal); |
|
var maxSurfaceAngleCos = Math.cos(maxSurfaceAngle); |
|
|
|
if (angleCos < maxSurfaceAngleCos) { |
|
// Calculate project point of pt0 on pt1-pt2 |
|
var d = projectPointToLine(pt1.x, pt1.y, pt2.x, pt2.y, pt0.x, pt0.y, tmpArr, false); |
|
tmpProjPoint.fromArray(tmpArr); |
|
var HALF_PI = Math.PI / 2; |
|
var angle2 = Math.acos(dir2.dot(surfaceNormal)); |
|
var newAngle = HALF_PI + angle2 - maxSurfaceAngle; |
|
|
|
if (newAngle >= HALF_PI) { |
|
// parallel |
|
Point.copy(tmpProjPoint, pt2); |
|
} else { |
|
// Calculate new projected length with limited minTurnAngle and get the new connect point |
|
tmpProjPoint.scaleAndAdd(dir2, d / Math.tan(Math.PI / 2 - newAngle)); // Limit the new calculated connect point between pt1 and pt2. |
|
|
|
var t = pt2.x !== pt1.x ? (tmpProjPoint.x - pt1.x) / (pt2.x - pt1.x) : (tmpProjPoint.y - pt1.y) / (pt2.y - pt1.y); |
|
|
|
if (isNaN(t)) { |
|
return; |
|
} |
|
|
|
if (t < 0) { |
|
Point.copy(tmpProjPoint, pt1); |
|
} else if (t > 1) { |
|
Point.copy(tmpProjPoint, pt2); |
|
} |
|
} |
|
|
|
tmpProjPoint.toArray(linePoints[1]); |
|
} |
|
} |
|
|
|
function setLabelLineState(labelLine, ignore, stateName, stateModel) { |
|
var isNormal = stateName === 'normal'; |
|
var stateObj = isNormal ? labelLine : labelLine.ensureState(stateName); // Make sure display. |
|
|
|
stateObj.ignore = ignore; // Set smooth |
|
|
|
var smooth = stateModel.get('smooth'); |
|
|
|
if (smooth && smooth === true) { |
|
smooth = 0.3; |
|
} |
|
|
|
stateObj.shape = stateObj.shape || {}; |
|
|
|
if (smooth > 0) { |
|
stateObj.shape.smooth = smooth; |
|
} |
|
|
|
var styleObj = stateModel.getModel('lineStyle').getLineStyle(); |
|
isNormal ? labelLine.useStyle(styleObj) : stateObj.style = styleObj; |
|
} |
|
|
|
function buildLabelLinePath(path, shape) { |
|
var smooth = shape.smooth; |
|
var points = shape.points; |
|
|
|
if (!points) { |
|
return; |
|
} |
|
|
|
path.moveTo(points[0][0], points[0][1]); |
|
|
|
if (smooth > 0 && points.length >= 3) { |
|
var len1 = vector.dist(points[0], points[1]); |
|
var len2 = vector.dist(points[1], points[2]); |
|
|
|
if (!len1 || !len2) { |
|
path.lineTo(points[1][0], points[1][1]); |
|
path.lineTo(points[2][0], points[2][1]); |
|
return; |
|
} |
|
|
|
var moveLen = Math.min(len1, len2) * smooth; |
|
var midPoint0 = vector.lerp([], points[1], points[0], moveLen / len1); |
|
var midPoint2 = vector.lerp([], points[1], points[2], moveLen / len2); |
|
var midPoint1 = vector.lerp([], midPoint0, midPoint2, 0.5); |
|
path.bezierCurveTo(midPoint0[0], midPoint0[1], midPoint0[0], midPoint0[1], midPoint1[0], midPoint1[1]); |
|
path.bezierCurveTo(midPoint2[0], midPoint2[1], midPoint2[0], midPoint2[1], points[2][0], points[2][1]); |
|
} else { |
|
for (var i = 1; i < points.length; i++) { |
|
path.lineTo(points[i][0], points[i][1]); |
|
} |
|
} |
|
} |
|
/** |
|
* Create a label line if necessary and set it's style. |
|
*/ |
|
|
|
|
|
export function setLabelLineStyle(targetEl, statesModels, defaultStyle) { |
|
var labelLine = targetEl.getTextGuideLine(); |
|
var label = targetEl.getTextContent(); |
|
|
|
if (!label) { |
|
// Not show label line if there is no label. |
|
if (labelLine) { |
|
targetEl.removeTextGuideLine(); |
|
} |
|
|
|
return; |
|
} |
|
|
|
var normalModel = statesModels.normal; |
|
var showNormal = normalModel.get('show'); |
|
var labelIgnoreNormal = label.ignore; |
|
|
|
for (var i = 0; i < DISPLAY_STATES.length; i++) { |
|
var stateName = DISPLAY_STATES[i]; |
|
var stateModel = statesModels[stateName]; |
|
var isNormal = stateName === 'normal'; |
|
|
|
if (stateModel) { |
|
var stateShow = stateModel.get('show'); |
|
var isLabelIgnored = isNormal ? labelIgnoreNormal : retrieve2(label.states[stateName] && label.states[stateName].ignore, labelIgnoreNormal); |
|
|
|
if (isLabelIgnored // Not show when label is not shown in this state. |
|
|| !retrieve2(stateShow, showNormal) // Use normal state by default if not set. |
|
) { |
|
var stateObj = isNormal ? labelLine : labelLine && labelLine.states[stateName]; |
|
|
|
if (stateObj) { |
|
stateObj.ignore = true; |
|
} |
|
|
|
continue; |
|
} // Create labelLine if not exists |
|
|
|
|
|
if (!labelLine) { |
|
labelLine = new Polyline(); |
|
targetEl.setTextGuideLine(labelLine); // Reset state of normal because it's new created. |
|
// NOTE: NORMAL should always been the first! |
|
|
|
if (!isNormal && (labelIgnoreNormal || !showNormal)) { |
|
setLabelLineState(labelLine, true, 'normal', statesModels.normal); |
|
} // Use same state proxy. |
|
|
|
|
|
if (targetEl.stateProxy) { |
|
labelLine.stateProxy = targetEl.stateProxy; |
|
} |
|
} |
|
|
|
setLabelLineState(labelLine, false, stateName, stateModel); |
|
} |
|
} |
|
|
|
if (labelLine) { |
|
defaults(labelLine.style, defaultStyle); // Not fill. |
|
|
|
labelLine.style.fill = null; |
|
var showAbove = normalModel.get('showAbove'); |
|
var labelLineConfig = targetEl.textGuideLineConfig = targetEl.textGuideLineConfig || {}; |
|
labelLineConfig.showAbove = showAbove || false; // Custom the buildPath. |
|
|
|
labelLine.buildPath = buildLabelLinePath; |
|
} |
|
} |
|
export function getLabelLineStatesModels(itemModel, labelLineName) { |
|
labelLineName = labelLineName || 'labelLine'; |
|
var statesModels = { |
|
normal: itemModel.getModel(labelLineName) |
|
}; |
|
|
|
for (var i = 0; i < SPECIAL_STATES.length; i++) { |
|
var stateName = SPECIAL_STATES[i]; |
|
statesModels[stateName] = itemModel.getModel([stateName, labelLineName]); |
|
} |
|
|
|
return statesModels; |
|
} |