/* * 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. */ // Poly path support NaN point import Path, { PathProps } from 'zrender/src/graphic/Path'; import PathProxy from 'zrender/src/core/PathProxy'; import { cubicRootAt, cubicAt } from 'zrender/src/core/curve'; const mathMin = Math.min; const mathMax = Math.max; function isPointNull(x: number, y: number) { return isNaN(x) || isNaN(y); } /** * Draw smoothed line in non-monotone, in may cause undesired curve in extreme * situations. This should be used when points are non-monotone neither in x or * y dimension. */ function drawSegment( ctx: PathProxy, points: ArrayLike, start: number, segLen: number, allLen: number, dir: number, smooth: number, smoothMonotone: 'x' | 'y' | 'none', connectNulls: boolean ) { let prevX: number; let prevY: number; let cpx0: number; let cpy0: number; let cpx1: number; let cpy1: number; let idx = start; let k = 0; for (; k < segLen; k++) { let x = points[idx * 2]; let y = points[idx * 2 + 1]; if (idx >= allLen || idx < 0) { break; } if (isPointNull(x, y)) { if (connectNulls) { idx += dir; continue; } break; } if (idx === start) { ctx[dir > 0 ? 'moveTo' : 'lineTo'](x, y); cpx0 = x; cpy0 = y; } else { let dx = x - prevX; let dy = y - prevY; // Ignore tiny segment. if ((dx * dx + dy * dy) < 0.5) { idx += dir; continue; } if (smooth > 0) { let nextIdx = idx + dir; let nextX = points[nextIdx * 2]; let nextY = points[nextIdx * 2 + 1]; // Ignore duplicate point while (nextX === x && nextY === y && k < segLen) { k++; nextIdx += dir; idx += dir; nextX = points[nextIdx * 2]; nextY = points[nextIdx * 2 + 1]; x = points[idx * 2]; y = points[idx * 2 + 1]; dx = x - prevX; dy = y - prevY; } let tmpK = k + 1; if (connectNulls) { // Find next point not null while (isPointNull(nextX, nextY) && tmpK < segLen) { tmpK++; nextIdx += dir; nextX = points[nextIdx * 2]; nextY = points[nextIdx * 2 + 1]; } } let ratioNextSeg = 0.5; let vx: number = 0; let vy: number = 0; let nextCpx0; let nextCpy0; // Is last point if (tmpK >= segLen || isPointNull(nextX, nextY)) { cpx1 = x; cpy1 = y; } else { vx = nextX - prevX; vy = nextY - prevY; const dx0 = x - prevX; const dx1 = nextX - x; const dy0 = y - prevY; const dy1 = nextY - y; let lenPrevSeg; let lenNextSeg; if (smoothMonotone === 'x') { lenPrevSeg = Math.abs(dx0); lenNextSeg = Math.abs(dx1); const dir = vx > 0 ? 1 : -1; cpx1 = x - dir * lenPrevSeg * smooth; cpy1 = y; nextCpx0 = x + dir * lenNextSeg * smooth; nextCpy0 = y; } else if (smoothMonotone === 'y') { lenPrevSeg = Math.abs(dy0); lenNextSeg = Math.abs(dy1); const dir = vy > 0 ? 1 : -1; cpx1 = x; cpy1 = y - dir * lenPrevSeg * smooth; nextCpx0 = x; nextCpy0 = y + dir * lenNextSeg * smooth; } else { lenPrevSeg = Math.sqrt(dx0 * dx0 + dy0 * dy0); lenNextSeg = Math.sqrt(dx1 * dx1 + dy1 * dy1); // Use ratio of seg length ratioNextSeg = lenNextSeg / (lenNextSeg + lenPrevSeg); cpx1 = x - vx * smooth * (1 - ratioNextSeg); cpy1 = y - vy * smooth * (1 - ratioNextSeg); // cp0 of next segment nextCpx0 = x + vx * smooth * ratioNextSeg; nextCpy0 = y + vy * smooth * ratioNextSeg; // Smooth constraint between point and next point. // Avoid exceeding extreme after smoothing. nextCpx0 = mathMin(nextCpx0, mathMax(nextX, x)); nextCpy0 = mathMin(nextCpy0, mathMax(nextY, y)); nextCpx0 = mathMax(nextCpx0, mathMin(nextX, x)); nextCpy0 = mathMax(nextCpy0, mathMin(nextY, y)); // Reclaculate cp1 based on the adjusted cp0 of next seg. vx = nextCpx0 - x; vy = nextCpy0 - y; cpx1 = x - vx * lenPrevSeg / lenNextSeg; cpy1 = y - vy * lenPrevSeg / lenNextSeg; // Smooth constraint between point and prev point. // Avoid exceeding extreme after smoothing. cpx1 = mathMin(cpx1, mathMax(prevX, x)); cpy1 = mathMin(cpy1, mathMax(prevY, y)); cpx1 = mathMax(cpx1, mathMin(prevX, x)); cpy1 = mathMax(cpy1, mathMin(prevY, y)); // Adjust next cp0 again. vx = x - cpx1; vy = y - cpy1; nextCpx0 = x + vx * lenNextSeg / lenPrevSeg; nextCpy0 = y + vy * lenNextSeg / lenPrevSeg; } } ctx.bezierCurveTo(cpx0, cpy0, cpx1, cpy1, x, y); cpx0 = nextCpx0; cpy0 = nextCpy0; } else { ctx.lineTo(x, y); } } prevX = x; prevY = y; idx += dir; } return k; } class ECPolylineShape { points: ArrayLike; smooth = 0; smoothConstraint = true; smoothMonotone: 'x' | 'y' | 'none'; connectNulls: boolean; } interface ECPolylineProps extends PathProps { shape?: Partial } export class ECPolyline extends Path { readonly type = 'ec-polyline'; shape: ECPolylineShape; constructor(opts?: ECPolylineProps) { super(opts); } getDefaultStyle() { return { stroke: '#000', fill: null as string }; } getDefaultShape() { return new ECPolylineShape(); } buildPath(ctx: PathProxy, shape: ECPolylineShape) { const points = shape.points; let i = 0; let len = points.length / 2; // const result = getBoundingBox(points, shape.smoothConstraint); if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon for (; len > 0; len--) { if (!isPointNull(points[len * 2 - 2], points[len * 2 - 1])) { break; } } for (; i < len; i++) { if (!isPointNull(points[i * 2], points[i * 2 + 1])) { break; } } } while (i < len) { i += drawSegment( ctx, points, i, len, len, 1, shape.smooth, shape.smoothMonotone, shape.connectNulls ) + 1; } } getPointOn(xOrY: number, dim: 'x' | 'y'): number[] { if (!this.path) { this.createPathProxy(); this.buildPath(this.path, this.shape); } const path = this.path; const data = path.data; const CMD = PathProxy.CMD; let x0; let y0; const isDimX = dim === 'x'; const roots: number[] = []; for (let i = 0; i < data.length;) { const cmd = data[i++]; let x; let y; let x2; let y2; let x3; let y3; let t; switch (cmd) { case CMD.M: x0 = data[i++]; y0 = data[i++]; break; case CMD.L: x = data[i++]; y = data[i++]; t = isDimX ? (xOrY - x0) / (x - x0) : (xOrY - y0) / (y - y0); if (t <= 1 && t >= 0) { const val = isDimX ? (y - y0) * t + y0 : (x - x0) * t + x0; return isDimX ? [xOrY, val] : [val, xOrY]; } x0 = x; y0 = y; break; case CMD.C: x = data[i++]; y = data[i++]; x2 = data[i++]; y2 = data[i++]; x3 = data[i++]; y3 = data[i++]; const nRoot = isDimX ? cubicRootAt(x0, x, x2, x3, xOrY, roots) : cubicRootAt(y0, y, y2, y3, xOrY, roots); if (nRoot > 0) { for (let i = 0; i < nRoot; i++) { const t = roots[i]; if (t <= 1 && t >= 0) { const val = isDimX ? cubicAt(y0, y, y2, y3, t) : cubicAt(x0, x, x2, x3, t); return isDimX ? [xOrY, val] : [val, xOrY]; } } } x0 = x3; y0 = y3; break; } } } } class ECPolygonShape extends ECPolylineShape { // Offset between stacked base points and points stackedOnPoints: ArrayLike; stackedOnSmooth: number; } interface ECPolygonProps extends PathProps { shape?: Partial } export class ECPolygon extends Path { readonly type = 'ec-polygon'; shape: ECPolygonShape; constructor(opts?: ECPolygonProps) { super(opts); } getDefaultShape() { return new ECPolygonShape(); } buildPath(ctx: PathProxy, shape: ECPolygonShape) { const points = shape.points; const stackedOnPoints = shape.stackedOnPoints; let i = 0; let len = points.length / 2; const smoothMonotone = shape.smoothMonotone; if (shape.connectNulls) { // Must remove first and last null values avoid draw error in polygon for (; len > 0; len--) { if (!isPointNull(points[len * 2 - 2], points[len * 2 - 1])) { break; } } for (; i < len; i++) { if (!isPointNull(points[i * 2], points[i * 2 + 1])) { break; } } } while (i < len) { const k = drawSegment( ctx, points, i, len, len, 1, shape.smooth, smoothMonotone, shape.connectNulls ); drawSegment( ctx, stackedOnPoints, i + k - 1, k, len, -1, shape.stackedOnSmooth, smoothMonotone, shape.connectNulls ); i += k + 1; ctx.closePath(); } } }