@maplat/tin
Version:
JavaScript library which performs homeomorphic conversion mutually between the coordinate systems of two planes based on the control points.
1,040 lines (958 loc) • 26 kB
text/typescript
import type { Feature, Point, Position } from "geojson";
import { point } from "@turf/helpers";
import type { IntersectionPoint } from "./types/kinks.d.ts";
/**
* 線分の交差点を検出するメインの関数
*
* @param coords - 線分群の座標配列。各線分は始点と終点の座標で表現
* @returns 検出された交差点のFeature配列
*/
export default function findIntersections(coords: Position[][]) {
const arcs = new ArcCollection(coords);
const xy = arcs.findSegmentIntersections();
return dedupIntersections(xy).reduce(
(
prev: Record<string, IntersectionPoint> | Feature<Point>[],
apoint: IntersectionPoint,
index: number,
array: IntersectionPoint[],
) => {
if (Array.isArray(prev)) return prev;
if (!prev) prev = {};
prev[`${apoint.x}:${apoint.y}`] = apoint;
if (index != array.length - 1) return prev;
return Object.keys(prev).map((key) => point([prev[key].x, prev[key].y]));
},
{} as Record<string, IntersectionPoint>,
) as Feature<Point>[];
}
/**
* 線分群を管理するクラス
* 効率的な交差判定のためのデータ構造と機能を提供
*/
class ArcCollection {
/**
* 座標データの配列
* _xx, _yy: Float64Array形式で座標を保持
* _ii: 各線分の開始インデックス
* _nn: 各線分の頂点数
*/
_xx?: Float64Array;
_yy?: Float64Array; // coordinates data
_ii?: Uint32Array;
_nn?: Uint32Array; // indexes, sizes
_zz: any;
_zlimit = 0; // simplification
_bb: any;
_allBounds: any; // bounding boxes
_arcIter: any;
_filteredArcIter: any; // path iterators
buf?: ArrayBuffer;
/**
* 線分群からArcCollectionを初期化
* @param coords - 線分群の座標配列
*/
constructor(coords: Position[][]) {
this.initArcs(coords);
}
initArcs(arcs: Position[][]) {
const xx: number[] = [],
yy: number[] = [];
const nn = arcs.map((points) => {
const n = points ? points.length : 0;
for (let i = 0; i < n; i++) {
xx.push(points[i][0]);
yy.push(points[i][1]);
}
return n;
});
this.initXYData(nn, xx, yy);
}
initXYData(nn: number[], xx: number[], yy: number[]) {
const size = nn.length;
this._xx = new Float64Array(xx);
this._yy = new Float64Array(yy);
this._nn = new Uint32Array(nn);
this._zz = null;
this._zlimit = 0;
this._filteredArcIter = null;
// generate array of starting idxs of each arc
this._ii = new Uint32Array(size);
let idx = 0;
for (let j = 0; j < size; j++) {
this._ii[j] = idx;
idx += nn[j];
}
if (idx != this._xx.length || this._xx.length != this._yy.length) {
error("ArcCollection#initXYData() Counting error");
}
this.initBounds();
// Pre-allocate some path iterators for repeated use.
this._arcIter = new ArcIter(this._xx, this._yy);
}
initBounds() {
const data = this.calcArcBounds_(this._xx!, this._yy!, this._nn!);
this._bb = data.bb;
this._allBounds = data.bounds;
}
/**
* データの境界を計算
* @returns バウンディングボックス情報
*/
calcArcBounds_(xx: Float64Array, yy: Float64Array, nn: Uint32Array) {
const numArcs = nn.length,
bb = new Float64Array(numArcs * 4),
bounds = new Bounds();
let arcOffs = 0,
arcLen: number,
j: number,
b: number[];
for (let i = 0; i < numArcs; i++) {
arcLen = nn[i];
if (arcLen > 0) {
j = i * 4;
b = calcArcBounds(xx, yy, arcOffs, arcLen) as number[];
bb[j++] = b[0];
bb[j++] = b[1];
bb[j++] = b[2];
bb[j] = b[3];
arcOffs += arcLen;
bounds.mergeBounds(b);
}
}
return {
bb,
bounds,
};
}
getBounds() {
return this._allBounds.clone();
}
// @cb function(i, j, xx, yy)
forEachSegment(cb: any) {
let count = 0;
for (let i = 0, n = this.size(); i < n; i++) {
count += this.forEachArcSegment(i, cb);
}
return count;
}
size() {
return (this._ii && this._ii.length) || 0;
}
// @cb function(i, j, xx, yy)
forEachArcSegment(arcId: any, cb: any): any {
const fw = arcId >= 0,
absId = fw ? arcId : ~arcId,
zlim = this.getRetainedInterval(),
n = this._nn![absId],
step = fw ? 1 : -1;
let v1 = fw ? this._ii![absId] : this._ii![absId] + n - 1,
v2 = v1,
count = 0;
for (let j = 1; j < n; j++) {
v2 += step;
if (zlim === 0 || this._zz![v2] >= zlim) {
cb(v1, v2, this._xx!, this._yy!);
v1 = v2;
count++;
}
}
return count;
}
getRetainedInterval() {
return this._zlimit;
}
// Give access to raw data arrays...
getVertexData() {
return {
xx: this._xx,
yy: this._yy,
zz: this._zz,
bb: this._bb,
nn: this._nn,
ii: this._ii,
};
}
getUint32Array(count: number) {
const bytes = count * 4;
if (!this.buf || this.buf.byteLength < bytes) {
this.buf = new ArrayBuffer(bytes);
}
return new Uint32Array(this.buf, 0, count);
}
// Return average magnitudes of dx, dy (with simplification)
getAvgSegment2() {
let dx = 0,
dy = 0;
const count = this.forEachSegment(
(i: number, j: number, xx: number[], yy: number[]) => {
dx += Math.abs(xx[i] - xx[j]);
dy += Math.abs(yy[i] - yy[j]);
},
);
return [dx / count || 0, dy / count || 0];
}
/**
* 交差判定のためのストライプ数を計算
* 線分の平均長さに基づいて最適な分割数を決定
*/
calcSegmentIntersectionStripeCount() {
const yrange = this.getBounds().height(),
segLen = this.getAvgSegment2()[1];
let count = 1;
if (segLen > 0 && yrange > 0) {
count = Math.ceil(yrange / segLen / 20);
}
return count || 1;
}
/**
* 線分の交差を検出
* ストライプ分割による効率的な判定を実装
*
* @returns 検出された交差点の配列
*/
findSegmentIntersections() {
const bounds = this.getBounds(),
ymin = bounds.ymin,
yrange = bounds.ymax - ymin,
stripeCount = this.calcSegmentIntersectionStripeCount(),
stripeSizes = new Uint32Array(stripeCount),
stripeId = stripeCount > 1
? (y: number) => Math.floor(((stripeCount - 1) * (y - ymin)) / yrange)
: () => 0;
let i, j;
// Count segments in each stripe
this.forEachSegment(
(id1: number, id2: number, _xx: number[], yy: number[]) => {
let s1 = stripeId(yy[id1]);
const s2 = stripeId(yy[id2]);
while (true) {
stripeSizes[s1] = stripeSizes[s1] + 2;
if (s1 == s2) break;
s1 += s2 > s1 ? 1 : -1;
}
},
);
// Allocate arrays for segments in each stripe
const stripeData = this.getUint32Array(utilsSum(stripeSizes));
let offs = 0;
const stripes: any[] = [];
utilsForEach(stripeSizes, (stripeSize: number) => {
const start = offs;
offs += stripeSize;
stripes.push(stripeData.subarray(start, offs));
});
// Assign segment ids to each stripe
initializeArray(stripeSizes, 0);
this.forEachSegment(
(id1: number, id2: number, _xx: number[], yy: number[]) => {
let s1 = stripeId(yy[id1]);
const s2 = stripeId(yy[id2]);
let count, stripe;
while (true) {
count = stripeSizes[s1];
stripeSizes[s1] = count + 2;
stripe = stripes[s1];
stripe[count] = id1;
stripe[count + 1] = id2;
if (s1 == s2) break;
s1 += s2 > s1 ? 1 : -1;
}
},
);
// Detect intersections among segments in each stripe.
const raw = this.getVertexData(),
intersections = [];
let arr;
for (i = 0; i < stripeCount; i++) {
arr = intersectSegments(stripes[i], raw.xx, raw.yy);
for (j = 0; j < arr.length; j++) {
intersections.push(arr[j]);
}
}
return dedupIntersections(intersections);
}
}
function error(...args: any[]) {
const msg = args.join(" ");
throw new Error(msg);
}
function isArrayLike(obj: any) {
if (!obj) return false;
if (isArray(obj)) return true;
if (isString(obj)) return false;
if (obj.length === 0) return true;
return obj.length > 0;
}
function isString(obj: any) {
return obj != null && obj.toString === String.prototype.toString;
}
function isArray(obj: any) {
return Array.isArray(obj);
}
// Calc sum, skip falsy and NaN values
// Assumes: no other non-numeric objects in array
//
function utilsSum(arr: Uint32Array, info?: any) {
if (!isArrayLike(arr)) error("utils.sum() expects an array, received:", arr);
let tot = 0,
nan = 0,
val;
for (let i = 0, n = arr.length; i < n; i++) {
val = arr[i];
if (val) {
tot += val;
} else if (isNaN(val)) {
nan++;
}
}
if (info) {
info.nan = nan;
}
return tot;
}
// Support for iterating over array-like objects, like typed arrays
function utilsForEach(arr: any, func: any, ctx?: any) {
if (!isArrayLike(arr)) {
throw new Error(`#forEach() takes an array-like argument. ${arr}`);
}
for (let i = 0, n = arr.length; i < n; i++) {
func.call(ctx, arr[i], i);
}
}
function initializeArray(arr: any, init: any) {
for (let i = 0, len = arr.length; i < len; i++) {
arr[i] = init;
}
return arr;
}
// Find intersections among a group of line segments
//
// TODO: handle case where a segment starts and ends at the same point (i.e. duplicate coords);
//
// @ids: Array of indexes: [s0p0, s0p1, s1p0, s1p1, ...] where xx[sip0] <= xx[sip1]
// @xx, @yy: Arrays of x- and y-coordinates
//
function intersectSegments(ids: any, xx: any, yy: any) {
const lim = ids.length - 2,
intersections = [];
let s1p1,
s1p2,
s2p1,
s2p2,
s1p1x,
s1p2x,
s2p1x,
s2p2x,
s1p1y,
s1p2y,
s2p1y,
s2p2y,
hit,
seg1,
seg2,
i,
j;
// Sort segments by xmin, to allow efficient exclusion of segments with
// non-overlapping x extents.
sortSegmentIds(xx, ids); // sort by ascending xmin
i = 0;
while (i < lim) {
s1p1 = ids[i];
s1p2 = ids[i + 1];
s1p1x = xx[s1p1];
s1p2x = xx[s1p2];
s1p1y = yy[s1p1];
s1p2y = yy[s1p2];
// count++;
j = i;
while (j < lim) {
j += 2;
s2p1 = ids[j];
s2p1x = xx[s2p1];
if (s1p2x < s2p1x) break; // x extent of seg 2 is greater than seg 1: done with seg 1
//if (s1p2x <= s2p1x) break; // this misses point-segment intersections when s1 or s2 is vertical
s2p1y = yy[s2p1];
s2p2 = ids[j + 1];
s2p2x = xx[s2p2];
s2p2y = yy[s2p2];
// skip segments with non-overlapping y ranges
if (s1p1y >= s2p1y) {
if (s1p1y > s2p2y && s1p2y > s2p1y && s1p2y > s2p2y) continue;
} else {
if (s1p1y < s2p2y && s1p2y < s2p1y && s1p2y < s2p2y) continue;
}
// skip segments that are adjacent in a path (optimization)
// TODO: consider if this eliminates some cases that should
// be detected, e.g. spikes formed by unequal segments
if (s1p1 == s2p1 || s1p1 == s2p2 || s1p2 == s2p1 || s1p2 == s2p2) {
continue;
}
// test two candidate segments for intersection
hit = segmentIntersection(
s1p1x,
s1p1y,
s1p2x,
s1p2y,
s2p1x,
s2p1y,
s2p2x,
s2p2y,
);
if (hit) {
seg1 = [s1p1, s1p2];
seg2 = [s2p1, s2p2];
intersections.push(formatIntersection(hit, seg1, seg2, xx, yy));
if (hit.length == 4) {
// two collinear segments may have two endpoint intersections
intersections.push(
formatIntersection(hit.slice(2), seg1, seg2, xx, yy),
);
}
}
}
i += 2;
}
return intersections;
}
/**
* 2つの2D線分間の交差を判定し、交差点を計算します
*
* @param ax, ay - 第1線分の始点座標
* @param bx, by - 第1線分の終点座標
* @param cx, cy - 第2線分の始点座標
* @param dx, dy - 第2線分の終点座標
*
* @returns 以下のいずれか:
* - null: 交差なし
* - [x, y]: 1点での交差
* - [x1, y1, x2, y2]: 線分が重なる場合の2点での交差
*
* 特殊ケースの扱い:
* 1. 両線分の端点で接触する場合 → 交差としない
* 2. T字型に接触する場合 → 交差として扱う
* 3. 線分が同一直線上で部分的に重なる場合 → 重なる部分の端点を交差点として扱う(1または2点)
* - 例: 線分ABとCDが重なる場合、重なり部分の両端点が交差点となる
* - [x1, y1, x2, y2] の形式で返される
*/
function segmentIntersection(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
const hit = segmentHit(ax, ay, bx, by, cx, cy, dx, dy);
let p = null;
if (hit) {
p = crossIntersection(ax, ay, bx, by, cx, cy, dx, dy);
if (!p) {
// collinear if p is null
p = collinearIntersection(ax, ay, bx, by, cx, cy, dx, dy);
} else if (endpointHit(ax, ay, bx, by, cx, cy, dx, dy)) {
p = null; // filter out segments that only intersect at an endpoint
}
}
return p;
}
// Source: Sedgewick, _Algorithms in C_
// (Tried various other functions that failed owing to floating point errors)
function segmentHit(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
return (
orient2D(ax, ay, bx, by, cx, cy) * orient2D(ax, ay, bx, by, dx, dy) <= 0 &&
orient2D(cx, cy, dx, dy, ax, ay) * orient2D(cx, cy, dx, dy, bx, by) <= 0
);
}
// returns a positive value if the points a, b, and c are arranged in
// counterclockwise order, a negative value if the points are in clockwise
// order, and zero if the points are collinear.
// Source: Jonathan Shewchuk http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf
function orient2D(ax: any, ay: any, bx: any, by: any, cx: any, cy: any) {
return determinant2D(ax - cx, ay - cy, bx - cx, by - cy);
}
/**
* 2次元の行列式を計算
*
* @param a, b - 行列の第1行
* @param c, d - 行列の第2行
* @returns 行列式の値
*/
function determinant2D(a: any, b: any, c: any, d: any) {
return a * d - b * c;
}
// Get intersection point if segments are non-collinear, else return null
// Assumes that segments have been intersect
function crossIntersection(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
let p = lineIntersection(ax, ay, bx, by, cx, cy, dx, dy);
let nearest;
if (p) {
// Re-order operands so intersection point is closest to a (better precision)
// Source: Jonathan Shewchuk http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf
nearest = nearestPoint(p[0], p[1], ax, ay, bx, by, cx, cy, dx, dy);
if (nearest == 1) {
p = lineIntersection(bx, by, ax, ay, cx, cy, dx, dy);
} else if (nearest == 2) {
p = lineIntersection(cx, cy, dx, dy, ax, ay, bx, by);
} else if (nearest == 3) {
p = lineIntersection(dx, dy, cx, cy, ax, ay, bx, by);
}
}
if (p) {
clampIntersectionPoint(p, ax, ay, bx, by, cx, cy, dx, dy);
}
return p;
}
function lineIntersection(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
const den = determinant2D(bx - ax, by - ay, dx - cx, dy - cy);
const eps = 1e-18;
let p;
if (den === 0) return null;
const m = orient2D(cx, cy, dx, dy, ax, ay) / den;
if (den <= eps && den >= -eps) {
// tiny denominator = low precision; using one of the endpoints as intersection
p = findEndpointInRange(ax, ay, bx, by, cx, cy, dx, dy);
/*if (!p) {
debug("[lineIntersection()]");
debugSegmentIntersection([], ax, ay, bx, by, cx, cy, dx, dy);
}*/
} else {
p = [ax + m * (bx - ax), ay + m * (by - ay)];
}
return p;
}
function findEndpointInRange(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
let p = null;
if (!outsideRange(ax, cx, dx) && !outsideRange(ay, cy, dy)) {
p = [ax, ay];
} else if (!outsideRange(bx, cx, dx) && !outsideRange(by, cy, dy)) {
p = [bx, by];
} else if (!outsideRange(cx, ax, bx) && !outsideRange(cy, ay, by)) {
p = [cx, cy];
} else if (!outsideRange(dx, ax, bx) && !outsideRange(dy, ay, by)) {
p = [dx, dy];
}
return p;
}
/**
* 点が線分の範囲外にあるかを判定
*
* @param a - 判定する点の座標
* @param b - 線分の一方の端点
* @param c - 線分のもう一方の端点
*/
function outsideRange(a: any, b: any, c: any) {
let out;
if (b < c) {
out = a < b || a > c;
} else if (b > c) {
out = a > b || a < c;
} else {
out = a != b;
}
return out;
}
// Return id of nearest point to x, y, among x0, y0, x1, y1, ...
function nearestPoint(x: any, y: any, ...args: any[]) {
let minIdx = -1,
minDist = Infinity,
dist;
for (let i = 0, j = 0, n = args.length; j < n; i++, j += 2) {
dist = distanceSq(x, y, args[j], args[j + 1]);
if (dist < minDist) {
minDist = dist;
minIdx = i;
}
}
return minIdx;
}
function distanceSq(ax: any, ay: any, bx: any, by: any) {
const dx = ax - bx,
dy = ay - by;
return dx * dx + dy * dy;
}
function clampIntersectionPoint(
p: any,
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
// Handle intersection points that fall outside the x-y range of either
// segment by snapping to nearest endpoint coordinate. Out-of-range
// intersection points can be caused by floating point rounding errors
// when a segment is vertical or horizontal. This has caused problems when
// repeatedly applying bbox clipping along the same segment
let x = p[0],
y = p[1];
// assumes that segment ranges intersect
x = clampToCloseRange(x, ax, bx);
x = clampToCloseRange(x, cx, dx);
y = clampToCloseRange(y, ay, by);
y = clampToCloseRange(y, cy, dy);
p[0] = x;
p[1] = y;
}
function clampToCloseRange(a: any, b: any, c: any) {
let lim;
if (outsideRange(a, b, c)) {
lim = Math.abs(a - b) < Math.abs(a - c) ? b : c;
if (Math.abs(a - lim) > 1e-15) {
//debug("[clampToCloseRange()] large clamping interval", a, b, c);
}
a = lim;
}
return a;
}
// Assume segments s1 and s2 are collinear and overlap; find one or two internal endpoints
function collinearIntersection(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
const minX = Math.min(ax, bx, cx, dx),
maxX = Math.max(ax, bx, cx, dx),
minY = Math.min(ay, by, cy, dy),
maxY = Math.max(ay, by, cy, dy),
useY = maxY - minY > maxX - minX;
let coords: any = [];
if (useY ? inside(ay, minY, maxY) : inside(ax, minX, maxX)) {
coords.push(ax, ay);
}
if (useY ? inside(by, minY, maxY) : inside(bx, minX, maxX)) {
coords.push(bx, by);
}
if (useY ? inside(cy, minY, maxY) : inside(cx, minX, maxX)) {
coords.push(cx, cy);
}
if (useY ? inside(dy, minY, maxY) : inside(dx, minX, maxX)) {
coords.push(dx, dy);
}
if (coords.length != 2 && coords.length != 4) {
coords = null;
//debug("Invalid collinear segment intersection", coords);
} else if (
coords.length == 4 &&
coords[0] == coords[2] &&
coords[1] == coords[3]
) {
// segs that meet in the middle don't count
coords = null;
}
return coords;
}
function endpointHit(
ax: any,
ay: any,
bx: any,
by: any,
cx: any,
cy: any,
dx: any,
dy: any,
) {
return (
(ax == cx && ay == cy) ||
(ax == dx && ay == dy) ||
(bx == cx && by == cy) ||
(bx == dx && by == dy)
);
}
function inside(x: any, minX: any, maxX: any) {
return x > minX && x < maxX;
}
// @xx array of x coords
// @ids an array of segment endpoint ids [a0, b0, a1, b1, ...]
// Sort @ids in place so that xx[a(n)] <= xx[b(n)] and xx[a(n)] <= xx[a(n+1)]
function sortSegmentIds(xx: any, ids: any) {
orderSegmentIds(xx, ids);
quicksortSegmentIds(xx, ids, 0, ids.length - 2);
}
function orderSegmentIds(xx: any, ids: any) {
for (let i = 0, n = ids.length; i < n; i += 2) {
if (xx[ids[i]] > xx[ids[i + 1]]) {
swap(ids, i, i + 1);
}
}
}
function swap(ids: any, i: number, j: number) {
const tmp = ids[i];
ids[i] = ids[j];
ids[j] = tmp;
}
function quicksortSegmentIds(a: any, ids: any, lo: any, hi: any) {
let i = lo,
j = hi,
pivot,
tmp;
while (i < hi) {
pivot = a[ids[((lo + hi) >> 2) << 1]]; // avoid n^2 performance on sorted arrays
while (i <= j) {
while (a[ids[i]] < pivot) i += 2;
while (a[ids[j]] > pivot) j -= 2;
if (i <= j) {
tmp = ids[i];
ids[i] = ids[j];
ids[j] = tmp;
tmp = ids[i + 1];
ids[i + 1] = ids[j + 1];
ids[j + 1] = tmp;
i += 2;
j -= 2;
}
}
if (j - lo < 40) insertionSortSegmentIds(a, ids, lo, j);
else quicksortSegmentIds(a, ids, lo, j);
if (hi - i < 40) {
insertionSortSegmentIds(a, ids, i, hi);
return;
}
lo = i;
j = hi;
}
}
function insertionSortSegmentIds(arr: any, ids: any, start: any, end: any) {
let id, id2;
for (let j = start + 2; j <= end; j += 2) {
id = ids[j];
id2 = ids[j + 1];
let i;
for (i = j - 2; i >= start && arr[id] < arr[ids[i]]; i -= 2) {
ids[i + 2] = ids[i];
ids[i + 3] = ids[i + 1];
}
ids[i + 2] = id;
ids[i + 3] = id2;
}
}
function formatIntersection(xy: any, s1: any, s2: any, xx: any, yy: any) {
const x = xy[0],
y = xy[1];
s1 = formatIntersectingSegment(x, y, s1[0], s1[1], xx, yy);
s2 = formatIntersectingSegment(x, y, s2[0], s2[1], xx, yy);
const a = s1[0] < s2[0] ? s1 : s2;
const b = a == s1 ? s2 : s1;
return { x, y, a, b };
}
function formatIntersectingSegment(
x: any,
y: any,
id1: any,
id2: any,
xx: any,
yy: any,
) {
let i = id1 < id2 ? id1 : id2,
j = i === id1 ? id2 : id1;
if (xx[i] == x && yy[i] == y) {
j = i;
} else if (xx[j] == x && yy[j] == y) {
i = j;
}
return [i, j];
}
/**
* 交差判定結果から重複を除去
*
* @param arr - 交差点情報の配列
* @returns 重複を除去した交差点情報の配列
*/
function dedupIntersections(arr: any) {
const index: any = {};
return arr.filter((o: any) => {
const key = getIntersectionKey(o);
if (key in index) {
return false;
}
index[key] = true;
return true;
});
}
// Get an indexable key from an intersection object
// Assumes that vertex ids of o.a and o.b are sorted
function getIntersectionKey(o: any) {
return `${o.a.join(",")};${o.b.join(",")}`;
}
// Constructor takes arrays of coords: xx, yy, zz (optional)
//
class ArcIter {
_i = 0;
_n = 0;
_inc = 1;
_xx: Float64Array;
_yy: Float64Array;
i = 0;
x = 0;
y = 0;
constructor(xx: Float64Array, yy: Float64Array) {
this._xx = xx;
this._yy = yy;
}
}
function calcArcBounds(
xx: Float64Array,
yy: Float64Array,
start: number,
len: number,
) {
let i = start | 0;
const n = isNaN(len) ? xx.length - i : len + i;
let x: number,
y: number,
xmin: number,
ymin: number,
xmax: number,
ymax: number;
if (n > 0) {
xmin = xmax = xx[i];
ymin = ymax = yy[i];
} else return [undefined, undefined, undefined, undefined];
for (i++; i < n; i++) {
x = xx[i];
y = yy[i];
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
return [xmin, ymin, xmax, ymax];
}
class Bounds {
xmin?: number;
ymin?: number;
xmax?: number;
ymax?: number;
constructor(...args: any[]) {
if (args.length > 0) {
this.setBounds(args);
}
}
// Return a bounding box with the same extent as this one.
cloneBounds() {
return this.clone();
}
clone() {
// alias so child classes can override clone()
return new Bounds(this.xmin, this.ymin, this.xmax, this.ymax);
}
width() {
return this.xmax! - this.xmin! || 0;
}
height() {
return this.ymax! - this.ymin! || 0;
}
setBounds(a: any, b?: number, c?: number, d?: number) {
if (arguments.length == 1) {
// assume first arg is a Bounds or array
if (isArrayLike(a)) {
b = a[1];
c = a[2];
d = a[3];
a = a[0];
} else {
b = a.ymin;
c = a.xmax;
d = a.ymax;
a = a.xmin;
}
}
this.xmin = a;
this.ymin = b;
this.xmax = c;
this.ymax = d;
if (a > c! || b! > d!) this.update();
// error("Bounds#setBounds() min/max reversed:", a, b, c, d);
return this;
}
update() {
let tmp: number;
if (this.xmin! > this.xmax!) {
tmp = this.xmin!;
this.xmin = this.xmax;
this.xmax = tmp;
}
if (this.ymin! > this.ymax!) {
tmp = this.ymin!;
this.ymin = this.ymax;
this.ymax = tmp;
}
}
mergeBounds(bb: number | number[] | Bounds, ...args: number[]) {
let a: number, b: number, c: number, d: number;
if (bb instanceof Bounds) {
a = bb.xmin!;
b = bb.ymin!;
c = bb.xmax!;
d = bb.ymax!;
} else if (args.length == 3) {
a = bb as number;
b = args[0];
c = args[1];
d = args[2];
} else if ((bb as number[]).length == 4) {
// assume array: [xmin, ymin, xmax, ymax]
a = (bb as number[])[0];
b = (bb as number[])[1];
c = (bb as number[])[2];
d = (bb as number[])[3];
} else {
error("Bounds#mergeBounds() invalid argument:", bb);
}
if (this.xmin === void 0) {
this.setBounds(a!, b!, c!, d!);
} else {
if (a! < this.xmin) this.xmin = a!;
if (b! < this.ymin!) this.ymin = b!;
if (c! > this.xmax!) this.xmax = c!;
if (d! > this.ymax!) this.ymax = d!;
}
return this;
}
}