| /* |
| * 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<number>, |
| 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<number>; |
| smooth = 0; |
| smoothConstraint = true; |
| smoothMonotone: 'x' | 'y' | 'none'; |
| connectNulls: boolean; |
| } |
| |
| interface ECPolylineProps extends PathProps { |
| shape?: Partial<ECPolylineShape> |
| } |
| |
| export class ECPolyline extends Path<ECPolylineProps> { |
| |
| 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<number>; |
| stackedOnSmooth: number; |
| } |
| |
| interface ECPolygonProps extends PathProps { |
| shape?: Partial<ECPolygonShape> |
| } |
| 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(); |
| } |
| } |
| } |