UNPKG

devexpress-diagram

Version:

DevExpress Diagram Control

776 lines (739 loc) 37.5 kB
import "es6-object-assign/auto"; import { Size } from "@devexpress/utils/lib/geometry/size"; import { Point } from "@devexpress/utils/lib/geometry/point"; import { Rectangle } from "@devexpress/utils/lib/geometry/rectangle"; import { SearchUtils } from "@devexpress/utils/lib/utils/search"; import { Metrics } from "@devexpress/utils/lib/geometry/metrics"; import { MathUtils } from "@devexpress/utils/lib/utils/math"; import { Vector } from "@devexpress/utils/lib/geometry/vector"; import { Segment } from "@devexpress/utils/lib/geometry/segment"; import { TextAlignment } from "./Model/Style"; import { Browser } from "@devexpress/utils/lib/browser"; import { EvtUtils } from "@devexpress/utils/lib/utils/evt"; import { ConnectorRenderPoint } from "./Model/Connectors/ConnectorRenderPoint"; export class LineEquation { static fromPoints(pointA: Point, pointB: Point, accuracy: number = 0.00001): LineEquation { return !GeometryUtils.areDuplicatedPoints(pointA, pointB, accuracy) ? new LineEquation( pointB.y - pointA.y, pointA.x - pointB.x, pointB.x * pointA.y - pointA.x * pointB.y) : undefined; } constructor(private aParam: number, private bParam: number, private cParam: number) { } getPointIntersection(other: LineEquation, accuracy: number = 0.00001): Point | null { const A1: number = this.aParam; const B1: number = this.bParam; const C1: number = this.cParam; const A2: number = other.aParam; const B2: number = other.bParam; const C2: number = other.cParam; const v: number = A2 * B1 - A1 * B2; if(MathUtils.numberCloseTo(v, 0, accuracy)) return null; if(A1 === 0) { const x: number = (B2 * C1 - C2 * B1) / (B1 * A2); return this.createPoint(x, -C1 / B1); } const y: number = (C2 * A1 - C1 * A2) / v; return this.createPoint((-B1 * y - C1) / A1, y); } containsPoint(point: Point, accuracy: number = 0.00001) : boolean { return MathUtils.numberCloseTo(this.aParam * point.x + this.bParam * point.y + this.cParam, 0, accuracy); } private createPoint(x: number, y: number, accuracy: number = 0.00001): Point { return new Point( MathUtils.numberCloseTo(x, 0, accuracy) ? 0 : x, MathUtils.numberCloseTo(y, 0, accuracy) ? 0 : y); } } export class Range { public to: number; constructor(public from: number, to?: number) { this.to = to !== undefined ? to : from; } get length(): number { return Math.abs(this.to - this.from); } extend(range: Range): void { this.from = Math.min(range.from, this.from); this.to = Math.max(range.to, this.to); } includes(value: number): boolean { return value >= this.from && value <= this.to; } static fromLength(from: number, length: number): Range { return new Range(from, from + length); } } export class EventDispatcher<T extends IEventListener> { listeners: T[] = []; public add(listener: T): void { if(!listener) throw new Error("Not Implemented"); if(!this.hasEventListener(listener)) this.listeners.push(listener); } public remove(listener: T): void { for(let i = 0, currentListener; currentListener = this.listeners[i]; i++) if(currentListener === listener) { this.listeners.splice(i, 1); break; } } public raise(funcName: keyof T, ...args: any[]): void { for(let i = 0, listener: IEventListener; listener = this.listeners[i]; i++) { const func = listener[<string>funcName]; func && func.apply(listener, args); } } public raise1(action: (listener: T) => void) { for(let i = 0, listener: T; listener = this.listeners[i]; i++) action(listener); } hasEventListener(listener: IEventListener): boolean { for(let i = 0, l = this.listeners.length; i < l; i++) if(this.listeners[i] === listener) return true; return false; } } export interface IEventListener { } export class Utils { static flatten<T>(arr: T[][]): T[] { return [].concat(...arr); } } export class GeometryUtils { static arePointsOfOrthogonalLine(point1: ConnectorRenderPoint, point2: ConnectorRenderPoint, isHorizontal: boolean): boolean { return isHorizontal ? (point1.y === point2.y) : (point1.x === point2.x); } static getCommonRectangle(rects: Rectangle[]) { if(!rects.length) return new Rectangle(0, 0, 0, 0); let minX = Number.MAX_VALUE; let maxX = -Number.MAX_VALUE; let minY = Number.MAX_VALUE; let maxY = -Number.MAX_VALUE; rects.forEach(rect => { minX = Math.min(minX, rect.x); maxX = Math.max(maxX, rect.right); minY = Math.min(minY, rect.y); maxY = Math.max(maxY, rect.bottom); }); return new Rectangle(minX, minY, maxX - minX, maxY - minY); } static findFreeSpace(rects: Rectangle[], size: Size, exact: boolean, targetRect?: Rectangle): Point { let xs: number[] = [targetRect ? targetRect.x : 0]; let ys: number[] = [targetRect ? targetRect.y : 0]; rects.forEach(r => { xs.push(r.x); xs.push(r.right); ys.push(r.y); ys.push(r.bottom); }); xs = xs.sort((a, b) => a - b).reduce((acc, v, index) => (xs[index - 1] !== v && acc.push(v) && acc) || acc, []); ys = ys.sort((a, b) => a - b).reduce((acc, v, index) => (ys[index - 1] !== v && acc.push(v) && acc) || acc, []); const matrix: number[][] = ys.map(y => xs.map((x, i) => xs[i + 1] - x)); for(let i = 0, rect: Rectangle; rect = rects[i]; i++) { const xi0 = SearchUtils.binaryIndexOf(xs, a => a - rect.x); const xi1 = SearchUtils.binaryIndexOf(xs, a => a - rect.right); const yi0 = SearchUtils.binaryIndexOf(ys, a => a - rect.y); const yi1 = SearchUtils.binaryIndexOf(ys, a => a - rect.bottom); for(let y = yi0; y < yi1; y++) for(let x = xi0; x < xi1; x++) matrix[y][x] *= -1; } for(let yi = 0; yi < ys.length; yi++) for(let xi = 0; xi < xs.length - 1; xi++) { const checkResult = this.checkRect(matrix, ys, xs, yi, xi, size, exact); if(checkResult > 0) xi = checkResult; else if(checkResult === 0) return new Point(xs[xi], ys[yi]); } return null; } private static checkRect(matrix: number[][], ys: number[], xs: number[], yimin: number, ximin: number, size: Size, exact: boolean): number { let height = 0; let width = 0; let ximax = xs.length - 2; for(let yi = yimin; yi < ys.length; yi++) { height = ys[yi + 1] - ys[yimin]; for(let xi = ximin; xi <= ximax; xi++) { if(matrix[yi][xi] < 0) return xi === 0 ? -1 : xi; width = xs[xi + 1] - xs[ximin]; if(size.width <= width || (!exact && xi === xs.length - 2 && size.width / 2 <= width)) { if(size.height <= height || (!exact && yi === ys.length - 2 && size.height / 2 <= height)) return 0; ximax = xi; } } } } static getArrowPoints(point: Point, directionPoint: Point, arrowHeight: number, arrowWidth: number): { point1: Point, point2: Point, point3: Point } { if(point.x === directionPoint.x && point.y === directionPoint.y) return { point1: point.clone(), point2: point.clone(), point3: point.clone() }; const catX = directionPoint.x - point.x; const catY = directionPoint.y - point.y; const hypotenuse = Math.sqrt(Math.pow(catX, 2) + Math.pow(catY, 2)); const cos = catX / hypotenuse; const sin = catY / hypotenuse; const x1 = point.x + arrowHeight * cos + arrowWidth * sin; const y1 = point.y + arrowHeight * sin - arrowWidth * cos; const x2 = point.x + arrowHeight * cos - arrowWidth * sin; const y2 = point.y + arrowHeight * sin + arrowWidth * cos; const x3 = point.x + arrowHeight * cos; const y3 = point.y + arrowHeight * sin; return { point1: new Point(x1, y1), point2: new Point(x2, y2), point3: new Point(x3, y3) }; } static createSegments<TPoint extends Point>(points: TPoint[]) : Segment<TPoint>[] { const result = []; for(let i = 1; i < points.length; i++) result.push(new Segment(points[i - 1], points[i])); return result; } static createRectagle<T extends Point>(points: T[]) : Rectangle { const xarr = points.map(p => p.x); const yarr = points.map(p => p.y); const minX = xarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE); const maxX = xarr.reduce((prev, cur) => Math.max(prev, cur), -Number.MAX_VALUE); const minY = yarr.reduce((prev, cur) => Math.min(prev, cur), Number.MAX_VALUE); const maxY = yarr.reduce((prev, cur) => Math.max(prev, cur), -Number.MAX_VALUE); return new Rectangle(minX, minY, maxX - minX, maxY - minY); } static createSegmentsFromRectangle(rect: Rectangle): Segment<Point>[] { const result : Segment<Point>[] = []; const topLeft = new Point(rect.x, rect.y); const topRight = new Point(rect.right, rect.y); const bottomRight = new Point(rect.right, rect.bottom); const bottomLeft = new Point(rect.x, rect.bottom); result.push(new Segment(topLeft, topRight)); result.push(new Segment(topRight, bottomRight)); result.push(new Segment(bottomRight, bottomLeft)); result.push(new Segment(bottomLeft, topLeft)); return result; } static areSegmentsCutRectangle<T extends Point>(segments: Segment<T>[], rect: Rectangle) : boolean { if(!rect) return false; const rectanlePolygonalChain = GeometryUtils.createSegmentsFromRectangle(rect); let hasSegmentIn = false; let hasSegmentOut = false; for(let i = 0; i < segments.length; i++) { if(hasSegmentIn && hasSegmentOut) return true; const segment = segments[i]; if(segment.isIntersectedByRect(rect)) { const startPoint = segment.startPoint; const endPoint = segment.endPoint; const currentContainsStart = rect.containsPoint(startPoint); const currentContainsEnd = rect.containsPoint(endPoint); if(!currentContainsStart && !currentContainsEnd) return true; if(currentContainsStart && !currentContainsEnd) { const rectLinesContainsStart = rectanlePolygonalChain.filter(s => s.containsPoint(startPoint)); if(rectLinesContainsStart.length > 0) { const otherRectSegments = rectanlePolygonalChain.filter(s => { if(rectLinesContainsStart.length === 1) return !s.containsPoint(rectLinesContainsStart[0].startPoint) && !s.containsPoint(rectLinesContainsStart[0].endPoint); return s !== rectLinesContainsStart[0] && s !== rectLinesContainsStart[1]; }); if(otherRectSegments.some(s => segment.isIntersected(s)) && !hasSegmentIn) hasSegmentIn = true; } if(!hasSegmentOut) hasSegmentOut = true; continue; } if(!currentContainsStart && currentContainsEnd) { if(!hasSegmentIn) { hasSegmentIn = true; if(hasSegmentOut) hasSegmentOut = false; } const rectLinesContainsEnd = rectanlePolygonalChain.filter(s => s.containsPoint(endPoint)); if(rectLinesContainsEnd.length > 0) { const otherRectSegments = rectanlePolygonalChain.filter(s => { if(rectLinesContainsEnd.length === 1) return !s.containsPoint(rectLinesContainsEnd[0].startPoint) && !s.containsPoint(rectLinesContainsEnd[0].endPoint); return s !== rectLinesContainsEnd[0] && s !== rectLinesContainsEnd[1]; }); if(otherRectSegments.some(s => segment.isIntersected(s)) && !hasSegmentOut) hasSegmentOut = true; } continue; } const rectLinesContainsStart = rectanlePolygonalChain.filter(s => s.containsPoint(startPoint)); const rectLinesContainsEnd = rectanlePolygonalChain.filter(s => s.containsPoint(endPoint)); if(rectLinesContainsStart.length === 2 && rectLinesContainsEnd.length === 2) return true; if(rectLinesContainsStart.length === 1 && rectLinesContainsEnd.length === 1 && rectLinesContainsStart[0] !== rectLinesContainsEnd[0]) return true; if(!hasSegmentOut && rectLinesContainsEnd.length === 1 && !rectLinesContainsStart.length) hasSegmentOut = true; if(!hasSegmentIn && rectLinesContainsStart.length === 1 && !rectLinesContainsEnd.length) { hasSegmentIn = true; if(hasSegmentOut) hasSegmentOut = false; } } } return hasSegmentIn && hasSegmentOut; } static areIntersectedSegments<T extends Point>(segments: Segment<T>[], otherSegments: Segment<T>[]) : boolean { if(!otherSegments) return false; let segmentIndex = 0; let segment; while(segment = segments[segmentIndex]) { let otherSegmentIndex = 0; let otherSegment; while(otherSegment = otherSegments[otherSegmentIndex]) { if(otherSegment.isIntersected(segment)) return true; otherSegmentIndex++; } segmentIndex++; } return false; } static isLineIntersected<T extends Point>(beginLinePoint: T, endLinePoint: T, segment : Segment<T>, excludeBeginPoint? : boolean, excludeEndPoint?: boolean) : boolean { const line = LineEquation.fromPoints(beginLinePoint, endLinePoint); const segmentStartPoint = segment.startPoint; const segmentEndPoint = segment.endPoint; if(line.containsPoint(segmentStartPoint) && line.containsPoint(segmentEndPoint)) return !excludeBeginPoint && !excludeEndPoint; const segmentLine = LineEquation.fromPoints(segmentStartPoint, segmentEndPoint); const intersection = segmentLine.getPointIntersection(line); if(!intersection || !segment.containsPoint(intersection)) return false; if(excludeBeginPoint) return !GeometryUtils.areDuplicatedPoints(segmentStartPoint, intersection); if(excludeEndPoint) return !GeometryUtils.areDuplicatedPoints(segmentEndPoint, intersection); return true; } static removeUnnecessaryPoints<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy); this.removeBackwardPoints(points, removeCallback, checkCallback, accuracy); this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy); } static removeUnnecessaryRightAnglePoints<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy); this.removeBackwardPoints(points, removeCallback, checkCallback, accuracy); this.removeNotRightAnglePoints(points, removeCallback, checkCallback, accuracy); this.removeUnnecessaryPointsCore(points, removeCallback, checkCallback, accuracy); } private static removeUnnecessaryPointsCore<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { this.removeDuplicatedPoints(points, removeCallback, checkCallback, accuracy); this.removeNotCornersPoints(points, removeCallback, checkCallback, accuracy); } static removeNotRightAnglePoints<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { let index = 0; let point: T; while((point = points[index]) && points.length > 2) { const nextPoint = this.getNextPoint(points, index, 1, checkCallback); const prevPoint = this.getNextPoint(points, index, -1, checkCallback); if(!prevPoint || !nextPoint || GeometryUtils.isRightAngleCorner(prevPoint, point, nextPoint, accuracy) || !removeCallback(point, index)) index++; } } static removeDuplicatedPoints<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { let index = 0; let point: T; while((point = points[index]) && points.length > 2) { const nextPoint = this.getNextPoint(points, index, 1, checkCallback); if(nextPoint && GeometryUtils.areDuplicatedPoints(point, nextPoint, accuracy)) { const actualIndex = index === points.length - 2 ? index : index + 1; if(removeCallback(points[actualIndex], actualIndex)) continue; } index++; } } static removeNotCornersPoints<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { let index = 0; let point: T; while((point = points[index]) && points.length > 2) { const nextPoint = this.getNextPoint(points, index, 1, checkCallback); const prevPoint = this.getNextPoint(points, index, -1, checkCallback); if(!prevPoint || !nextPoint || GeometryUtils.isCorner(prevPoint, point, nextPoint, accuracy)) index++; else if(!removeCallback(point, index)) index++; } } static removeBackwardPoints<T extends Point>(points: T[], removeCallback: (p: T, index: number) => boolean, checkCallback: (p: T) => boolean = p => p !== undefined, accuracy: number = 0.00001) { let index = 0; let point: T; while((point = points[index]) && points.length > 2) { const nextPoint = this.getNextPoint(points, index, 1, checkCallback); const prevPoint = this.getNextPoint(points, index, -1, checkCallback); if(!prevPoint || !nextPoint || !GeometryUtils.isBackwardPoint(prevPoint, point, nextPoint, accuracy) || !removeCallback(point, index)) index++; } } static isRightAngleCorner<T extends Point>(prev: T, current: T, next: T, accuracy: number = 0.00001): boolean { return MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), Math.PI / 2.0, accuracy) || MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), Math.PI, accuracy) || MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), 3.0 * Math.PI / 2.0, accuracy); } static isCorner<T extends Point>(prev: T, current: T, next: T, accuracy: number = 0.00001): boolean { return !MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), 0, accuracy); } static areDuplicatedPoints<T extends Point>(current: T, next: T, accuracy: number = 0.00001) : boolean { return (MathUtils.numberCloseTo(current.x, next.x, accuracy) && MathUtils.numberCloseTo(current.y, next.y, accuracy)); } static isBackwardPoint<T extends Point>(prev: T, current: T, next: T, accuracy: number = 0.00001): boolean { return MathUtils.numberCloseTo(GeometryUtils.createAngle(prev, current, next), Math.PI, accuracy); } static createAngle<T extends Point>(prev: T, current: T, next: T): number { const vector1 = Vector.fromPoints(current, next); const vector2 = Vector.fromPoints(prev, current); const vector1X = vector1.x; const vector1Y = vector1.y; const vector2X = vector2.x; const vector2Y = vector2.y; const atan = Math.atan2(vector1X * vector2Y - vector2X * vector1Y, vector1X * vector2X + vector1Y * vector2Y); return atan < 0 ? 2 * Math.PI + atan : atan; } static getNextPoint<T extends Point>(points: T[], index: number, step: number, checkCallback: (pt: T) => boolean): T { let result: T; let newIndex = index + step; while(result = points[newIndex]) { if(checkCallback(result)) return result; newIndex = newIndex + step; } } static addSelectedLinesTo(prevPt: Point, pt: Point, nextPt: Point, offsetX : number, offsetY : number, offsetXNegative: number, offsetYNegative: number, nextOffsetX : number, nextOffsetY : number, nextOffsetXNegative : number, nextOffsetYNegative : number, addSelectedLine : (x: number, y: number) => void, addSelectedLineWB : (x: number, y: number) => void, accuracy: number = 0.00001) : void { const a1 = pt.y - prevPt.y; const a2 = nextPt.y - pt.y; const b1 = prevPt.x - pt.x; const b2 = pt.x - nextPt.x; const det = a1 * b2 - a2 * b1; if(!MathUtils.numberCloseTo(det, 0, accuracy)) { const c1 = a1 * (prevPt.x + offsetX) + b1 * (prevPt.y + offsetY); const c2 = a2 * (pt.x + nextOffsetX) + b2 * (pt.y + nextOffsetY); addSelectedLine((b2 * c1 - b1 * c2) / det, (a1 * c2 - a2 * c1) / det); const c1WB = a1 * (prevPt.x + offsetXNegative) + b1 * (prevPt.y + offsetYNegative); const c2WB = a2 * (pt.x + nextOffsetXNegative) + b2 * (pt.y + nextOffsetYNegative); addSelectedLineWB((b2 * c1WB - b1 * c2WB) / det, (a1 * c2WB - a2 * c1WB) / det); } } static getSelectionOffsetPoint(prev: Point, current: Point, distance: number): Point { return new Point((prev.y - current.y) / distance, (current.x - prev.x) / distance); } static getSelectionTextStartEndPoints(prev: Point, current: Point, distance: number, center: Point, size: Size, align: TextAlignment): [Point, Point] { const cos = (current.x - prev.x) / distance; const sin = (current.y - prev.y) / distance; const width = size.width * cos + size.height * sin; switch(align) { case TextAlignment.Left: return [center, new Point(center.x + cos * width, center.y + sin * width)]; case TextAlignment.Right: return [new Point(center.x - cos * width, center.y - sin * width), center]; default: return [ new Point(center.x - 0.5 * cos * width, center.y - 0.5 * sin * width), new Point(center.x + 0.5 * cos * width, center.y + 0.5 * sin * width) ]; } } static getPathLength(points: Point[]): number { let length = 0; let prevPt; points.forEach(pt => { if(prevPt !== undefined) length += Metrics.euclideanDistance(pt, prevPt); prevPt = pt; }); return length; } static getPathPointByPosition(points: Point[], relativePosition: number): [Point, number] { if(!points.length) throw new Error("Invalid points"); if(0 > relativePosition || relativePosition > 1) throw new Error("Invalid relative position"); const length = this.getPathLength(points); if(points.length <= 2 && length === 0 || relativePosition === 0) return [points[0], 0]; const targetLength = length * relativePosition; let currentLength = 0; for(let i = 1; i < points.length; i++) { const lineLength = Metrics.euclideanDistance(points[i], points[i - 1]); if(currentLength + lineLength >= targetLength) { const delta = targetLength - currentLength; const cos = (points[i].x - points[i - 1].x) / lineLength; const sin = (points[i].y - points[i - 1].y) / lineLength; return [new Point(points[i - 1].x + cos * delta, points[i - 1].y + sin * delta), i]; } currentLength += lineLength; } return [points[points.length - 1], points.length - 1]; } static getLineAngle(beginPoint: Point, endPoint: Point): number { return Math.atan2(endPoint.y - beginPoint.y, endPoint.x - beginPoint.x); } static getTriangleBeginAngle(beginPoint: Point, endPoint: Point, point: Point) { const lineAngle = this.getLineAngle(beginPoint, endPoint); const beginPointAngle = this.getLineAngle(beginPoint, point); return Math.abs(beginPointAngle - lineAngle); } static getTriangleEndAngle(beginPoint: Point, endPoint: Point, point: Point) { const lineAngle = this.getLineAngle(beginPoint, endPoint); const endPointAngle = this.getLineAngle(point, endPoint); return Math.abs(lineAngle - endPointAngle); } static getPathPointByPoint(points: Point[], point: Point): Point { if(!points.length) throw new Error("Invalid points"); if(points.length === 1) return points[0]; let distance = Number.MAX_VALUE; let result: Point; for(let i = 1; i < points.length; i++) { const beginPoint = points[i - 1]; const endPoint = points[i]; if(point.equals(beginPoint)) { result = beginPoint.clone(); break; } if(point.equals(endPoint)) { result = endPoint.clone(); break; } const beginAngle = this.getTriangleBeginAngle(beginPoint, endPoint, point); const endAngle = this.getTriangleEndAngle(beginPoint, endPoint, point); const beginDistance = Metrics.euclideanDistance(point, beginPoint); const endDistance = Metrics.euclideanDistance(point, endPoint); const orthOffset = beginDistance * Math.sin(beginAngle); let currentDistance; if(Math.PI / 2 <= beginAngle && beginAngle <= Math.PI * 3 / 2) currentDistance = beginDistance; else if(Math.PI / 2 <= endAngle && endAngle <= Math.PI * 3 / 2) currentDistance = endDistance; else currentDistance = Math.abs(orthOffset); if(currentDistance < distance) { distance = currentDistance; if(Math.PI / 2 <= beginAngle && beginAngle <= Math.PI * 3 / 2) result = beginPoint.clone(); else if(Math.PI / 2 <= endAngle && endAngle <= Math.PI * 3 / 2) result = endPoint.clone(); else { const round = Math.fround || Math.round; const lineAngle = this.getLineAngle(beginPoint, endPoint); let offsetX = round(Math.abs(orthOffset * Math.sin(lineAngle))); let offsetY = round(Math.abs(orthOffset * Math.cos(lineAngle))); const isAbove = point.y - beginPoint.y < round((point.x - beginPoint.x) * Math.tan(lineAngle)); if(0 <= lineAngle && lineAngle <= Math.PI / 2) { offsetX *= isAbove ? -1 : 1; offsetY *= isAbove ? 1 : -1; } else if(Math.PI / 2 <= lineAngle && lineAngle <= Math.PI) { offsetX *= isAbove ? 1 : -1; offsetY *= isAbove ? 1 : -1; } else if(0 >= lineAngle && lineAngle >= -Math.PI / 2) { offsetX *= isAbove ? 1 : -1; offsetY *= isAbove ? 1 : -1; } else if(-Math.PI / 2 >= lineAngle && lineAngle >= -Math.PI) { offsetX *= isAbove ? -1 : 1; offsetY *= isAbove ? 1 : -1; } result = point.clone().offset(offsetX, offsetY); } } } return result; } static getPathPositionByPoint(points: Point[], point: Point, maxPositionCount: number = 100): number { point = this.getPathPointByPoint(points, point); const length = this.getPathLength(points); let currentLength = 0; for(let i = 1; i < points.length; i++) { const beginPoint = points[i - 1]; const endPoint = points[i]; const lineLength = Metrics.euclideanDistance(endPoint, beginPoint); const angle = Math.atan((endPoint.y - beginPoint.y) / (endPoint.x - beginPoint.x)); const round = Math.fround || Math.round; if((point.x === endPoint.x && point.x === beginPoint.x) || (point.y === endPoint.y && point.y === beginPoint.y) || round(point.y - beginPoint.y) === round((point.x - beginPoint.x) * Math.tan(angle))) { if(Math.sin(angle) !== 0) currentLength += Math.abs((point.y - beginPoint.y) / Math.sin(angle)); else currentLength += Math.abs(point.x - beginPoint.x); return Math.round(currentLength * maxPositionCount / length) / maxPositionCount; } currentLength += lineLength; } return 1; } static arePointsEqual(points1: Point[], points2: Point[]): boolean { const count1 = points1.length; const count2 = points2.length; if(count1 !== count2) return false; for(let i = 0; i < count1; i++) if(!points1[i].equals(points2[i])) return false; return true; } static getMaxRectangleEnscribedInEllipse(ellipseSize: Size): Size { const dx = ellipseSize.width * Math.sqrt(2) / 2; const dy = ellipseSize.height * Math.sqrt(2) / 2; return new Size(dx, dy); } static getEllipseByEnscribedRectangle(rectSize: Size): Size { return new Size(2 * rectSize.width / Math.sqrt(2), 2 * rectSize.height / Math.sqrt(2)); } } export class ObjectUtils { static cloneObject(source: any): any { return source && Object.assign({}, source); } static compareObjects(obj1: any, obj2: any): boolean { if(obj1 === obj2) return true; if(typeof obj1 === "object" && typeof obj2 === "object") return this.isDeepEqual(obj1, obj2); return false; } private static isDeepEqual(obj1: any, obj2: any): boolean { const props1 = obj1 ? Object.getOwnPropertyNames(obj1) : []; const props2 = obj2 ? Object.getOwnPropertyNames(obj2) : []; if(props1.length !== props2.length) return false; for(let i = 0; i < props1.length; i++) { const property = props1[i]; switch(typeof obj1[property]) { case "object": { if(!this.isDeepEqual(obj1[property], obj2[property])) return false; break; } case "number": { if(!isNaN(obj1[property]) || !isNaN(obj2[property])) if(obj1[property] !== obj2[property]) return false; break; } default: { if(obj1[property] !== obj2[property]) return false; } } } return true; } } export class HtmlFocusUtils { static focusWithPreventScroll(element: HTMLElement) { try { const isPreventScrollNotSupported = Browser.Safari; const savedDocumentScrollPosition = isPreventScrollNotSupported && this.getHtmlScrollPosition(); if(isPreventScrollNotSupported) { const parentPos = element.parentElement && element.parentElement.getBoundingClientRect(); if(parentPos) { let left = parentPos.left < 0 ? -parentPos.left + 1 : 0; let top = parentPos.top < 0 ? -parentPos.top + 1 : 0; const iframePos = window.frameElement && window.frameElement.getBoundingClientRect(); if(iframePos) { if(iframePos.top < 0 && (-iframePos.top > parentPos.top)) top = -iframePos.top - parentPos.top + 1; if(iframePos.left < 0 && (-iframePos.left > parentPos.left)) left = -iframePos.left - parentPos.left + 1; } element.style.setProperty("left", left + "px", "important"); element.style.setProperty("top", top + "px", "important"); } } element.focus({ preventScroll: true }); if(isPreventScrollNotSupported) { const newDocumentScrollPosition = this.getHtmlScrollPosition(); if(!ObjectUtils.compareObjects(savedDocumentScrollPosition, newDocumentScrollPosition)) this.setHtmlScrollPosition(savedDocumentScrollPosition); element.style.setProperty("left", "-1000px", "important"); element.style.setProperty("top", "-1000px", "important"); } } catch(e) { } } private static getHtmlScrollPosition() { return { pos: this.getDocumentScrollPosition(window, document), iframePos: window.top !== window && this.getDocumentScrollPosition(window.top, window.top.document) }; } private static getDocumentScrollPosition(win: any, doc: any) { return { left: win.pageXOffset || doc.documentElement.scrollLeft || doc.body.scrollLeft, top: win.pageYOffset || doc.documentElement.scrollTop || doc.body.scrollTop }; } private static setHtmlScrollPosition(position: any) { this.setDocumentScrollPosition(document, position.pos); if(window.top !== window && position.iframePos) this.setDocumentScrollPosition(window.top.document, position.iframePos); } private static setDocumentScrollPosition(doc: any, pos: any) { doc.documentElement.scrollTop = pos.top; doc.documentElement.scrollLeft = pos.left; doc.body.scrollTop = pos.top; doc.body.scrollLeft = pos.left; } } export class EventUtils { static isLeftButtonPressed(evt: Event) { return EvtUtils.isLeftButtonPressed(evt); } static isPointerEvents() { return window.PointerEvent; } static isMousePointer(evt: any): boolean { return this.isPointerEvents() && ((evt.pointerType && evt.pointerType === "mouse") || (Browser.Firefox && evt.type === "click")); } static isTouchMode(): boolean { return Browser.TouchUI || (window.navigator && window.navigator.maxTouchPoints > 0); } static isTouchEvent(evt: any): boolean { return Browser.TouchUI || !EventUtils.isMousePointer(evt); } }