devexpress-diagram
Version:
DevExpress Diagram Control
776 lines (739 loc) • 37.5 kB
text/typescript
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);
}
}