graph-explorer
Version:
Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.
247 lines (223 loc) • 6.38 kB
text/typescript
import { Element } from "./elements";
export interface Vector {
readonly x: number;
readonly y: number;
}
export const Vector = {
equals(a: Vector, b: Vector) {
return a.x === b.x && a.y === b.y;
},
length({ x, y }: Vector) {
return Math.sqrt(x * x + y * y);
},
normalize({ x, y }: Vector) {
if (x === 0 && y === 0) {
return { x, y };
}
const inverseLength = 1 / Math.sqrt(x * x + y * y);
return { x: x * inverseLength, y: y * inverseLength };
},
dot({ x: x1, y: y1 }: Vector, { x: x2, y: y2 }: Vector): number {
return x1 * x2 + y1 * y2;
},
cross2D({ x: x1, y: y1 }: Vector, { x: x2, y: y2 }: Vector) {
return x1 * y2 - y1 * x2;
},
};
export interface Size {
readonly width: number;
readonly height: number;
}
export interface Rect {
readonly x: number;
readonly y: number;
readonly width: number;
readonly height: number;
}
export const Rect = {
center({ x, y, width, height }: Rect) {
return { x: x + width / 2, y: y + height / 2 };
},
};
export function boundsOf(element: Element): Rect {
const { x, y } = element.position;
const { width, height } = element.size;
return { x, y, width, height };
}
function intersectRayFromRectangleCenter(sourceRect: Rect, rayTarget: Vector) {
const isTargetInsideRect =
sourceRect.width === 0 ||
sourceRect.height === 0 ||
(rayTarget.x > sourceRect.x &&
rayTarget.x < sourceRect.x + sourceRect.width &&
rayTarget.y > sourceRect.y &&
rayTarget.y < sourceRect.y + sourceRect.height);
const halfWidth = sourceRect.width / 2;
const halfHeight = sourceRect.height / 2;
const center = {
x: sourceRect.x + halfWidth,
y: sourceRect.y + halfHeight,
};
if (isTargetInsideRect) {
return center;
}
const direction = Vector.normalize({
x: rayTarget.x - center.x,
y: rayTarget.y - center.y,
});
const rightDirection = { x: Math.abs(direction.x), y: direction.y };
const isHorizontal =
Vector.cross2D({ x: halfWidth, y: -halfHeight }, rightDirection) > 0 &&
Vector.cross2D({ x: halfWidth, y: halfHeight }, rightDirection) < 0;
if (isHorizontal) {
return {
x: center.x + halfWidth * Math.sign(direction.x),
y: center.y + (halfWidth * direction.y) / Math.abs(direction.x),
};
} else {
return {
x: center.x + (halfHeight * direction.x) / Math.abs(direction.y),
y: center.y + halfHeight * Math.sign(direction.y),
};
}
}
export function isPolylineEqual(
left: readonly Vector[],
right: readonly Vector[]
) {
if (left === right) {
return true;
}
if (left.length !== right.length) {
return false;
}
for (let i = 0; i < left.length; i++) {
const a = left[i];
const b = right[i];
if (!(a.x === b.x && a.y === b.y)) {
return false;
}
}
return true;
}
export function computePolyline(
source: Element,
target: Element,
vertices: readonly Vector[]
): Vector[] {
const sourceRect = boundsOf(source);
const targetRect = boundsOf(target);
const startPoint = intersectRayFromRectangleCenter(
sourceRect,
vertices.length > 0 ? vertices[0] : Rect.center(targetRect)
);
const endPoint = intersectRayFromRectangleCenter(
targetRect,
vertices.length > 0
? vertices[vertices.length - 1]
: Rect.center(sourceRect)
);
return [startPoint, ...vertices, endPoint];
}
export function computePolylineLength(polyline: readonly Vector[]): number {
let previous: Vector;
return polyline.reduce((acc, point) => {
const segmentLength = previous
? Vector.length({ x: point.x - previous.x, y: point.y - previous.y })
: 0;
previous = point;
return acc + segmentLength;
}, 0);
}
export function getPointAlongPolyline(
polyline: readonly Vector[],
offset: number
): Vector {
if (polyline.length === 0) {
throw new Error("Cannot compute a point for empty polyline");
}
if (offset < 0) {
return polyline[0];
}
let currentOffset = 0;
for (let i = 1; i < polyline.length; i++) {
const previous = polyline[i - 1];
const point = polyline[i];
const segment = { x: point.x - previous.x, y: point.y - previous.y };
const segmentLength = Vector.length(segment);
const newOffset = currentOffset + segmentLength;
if (offset < newOffset) {
const leftover = (offset - currentOffset) / segmentLength;
return {
x: previous.x + leftover * segment.x,
y: previous.y + leftover * segment.y,
};
} else {
currentOffset = newOffset;
}
}
return polyline[polyline.length - 1];
}
export function findNearestSegmentIndex(
polyline: readonly Vector[],
location: Vector
): number {
let minDistance = Infinity;
let foundIndex = 0;
for (let i = 0; i < polyline.length - 1; i++) {
const pivot = polyline[i];
const next = polyline[i + 1];
const target = { x: location.x - pivot.x, y: location.y - pivot.y };
const segment = { x: next.x - pivot.x, y: next.y - pivot.y };
const segmentLength = Vector.length(segment);
const projectionToSegment = Vector.dot(target, segment) / segmentLength;
if (projectionToSegment < 0 || projectionToSegment > segmentLength) {
continue;
}
const distanceToSegment =
Math.abs(Vector.cross2D(target, segment)) / segmentLength;
if (distanceToSegment < minDistance) {
minDistance = distanceToSegment;
foundIndex = i;
}
}
return foundIndex;
}
export function findElementAtPoint(
elements: readonly Element[],
point: Vector
): Element | undefined {
for (let i = elements.length - 1; i >= 0; i--) {
const element = elements[i];
const { x, y, width, height } = boundsOf(element);
if (element.temporary) {
continue;
}
if (
point.x >= x &&
point.x <= x + width &&
point.y >= y &&
point.y <= y + height
) {
return element;
}
}
return undefined;
}
export function computeGrouping(
elements: readonly Element[]
): Map<string, Element[]> {
const grouping = new Map<string, Element[]>();
for (const element of elements) {
const group = element.group;
if (typeof group === "string") {
let children = grouping.get(group);
if (!children) {
children = [];
grouping.set(group, children);
}
children.push(element);
}
}
return grouping;
}