UNPKG

84.4 kBJavaScriptView Raw
1/*
2* Copyright (C) 1998-2021 by Northwoods Software Corporation. All Rights Reserved.
3*/
4/*
5* This is an extension and not part of the main GoJS library.
6* Note that the API for this class may change with any version, even point releases.
7* If you intend to use an extension in production, you should copy the code to your own source directory.
8* Extensions can be found in the GoJS kit under the extensions or extensionsTS folders.
9* See the Extensions intro page (https://gojs.net/latest/intro/extensions.html) for more information.
10*/
11import * as go from '../release/go-module.js';
12import { Quadtree } from './Quadtree.js';
13/**
14 * @hidden @internal
15 * Used to represent the perimeter of the currently packed
16 * shape when packing rectangles. Segments are always assumed
17 * to be either horizontal or vertical, and store whether or
18 * not their first point is concave (this makes sense in the
19 * context of representing a perimeter, as the next segment
20 * will always be connected to the last).
21 */
22class Segment {
23 /**
24 * @hidden @internal
25 * Constructs a new Segment. Segments are assumed to be either
26 * horizontal or vertical, and the given coordinates should
27 * reflect that.
28 * @param x1 the x coordinate of the first point
29 * @param y1 the y coordinate of the first point
30 * @param x2 the x coordinate of the second point
31 * @param y2 the y coordinate of the second point
32 * @param p1Concave whether or not the first point is concave
33 */
34 constructor(x1, y1, x2, y2, p1Concave) {
35 this.x1 = x1;
36 this.y1 = y1;
37 this.x2 = x2;
38 this.y2 = y2;
39 this.p1Concave = p1Concave;
40 this.isHorizontal = Math.abs(y2 - y1) < 1e-7;
41 }
42}
43/**
44 * @hidden @internal
45 * Defines the possible orientations that two adjacent
46 * horizontal/vertical segments can form.
47 */
48var Orientation;
49(function (Orientation) {
50 Orientation[Orientation["NE"] = 0] = "NE";
51 Orientation[Orientation["NW"] = 1] = "NW";
52 Orientation[Orientation["SW"] = 2] = "SW";
53 Orientation[Orientation["SE"] = 3] = "SE";
54})(Orientation || (Orientation = {}));
55/**
56 * @hidden @internal
57 * Structure for storing possible placements when packing
58 * rectangles. Fits have a cost associated with them (lower
59 * cost placements are preferred), and can be placed relative
60 * to either one or two segments. If the fit is only placed
61 * relative to one segment, s2 will be undefined. Fits placed
62 * relative to multiple segments will hereafter be referred to
63 * as "skip fits".
64 */
65class Fit {
66 /**
67 * @hidden @internal
68 * Constructs a new Fit.
69 * @param bounds the boundaries of the placement, including defined x and y coordinates
70 * @param cost the cost of the placement, lower cost fits will be preferred
71 * @param s1 the segment that the placement was made relative to
72 * @param s2 the second segment that the placement was made relative to, if the fit is a skip fit
73 */
74 constructor(bounds, cost, s1, s2) {
75 this.bounds = bounds;
76 this.cost = cost;
77 this.s1 = s1;
78 this.s2 = s2;
79 }
80}
81/**
82 * Custom layout which attempts to pack nodes as close together as possible
83 * without overlap. Each node is assumed to be either rectangular or
84 * circular (dictated by the {@link #hasCircularNodes} property). This layout
85 * supports packing nodes into either a rectangle or an ellipse, with the
86 * shape determined by the packShape property and the aspect ratio determined
87 * by either the aspectRatio property or the specified width and height
88 * (depending on the packMode).
89 *
90 * Nodes with 0 width or height cannot be packed, so they are treated by this
91 * layout as having a width or height of 0.1 instead.
92 * @category Layout Extension
93 */
94export class PackedLayout extends go.Layout {
95 constructor() {
96 super(...arguments);
97 // configuration defaults
98 /** @hidden @internal */ this._packShape = PackedLayout.Elliptical;
99 /** @hidden @internal */ this._packMode = PackedLayout.AspectOnly;
100 /** @hidden @internal */ this._sortMode = PackedLayout.None;
101 /** @hidden @internal */ this._sortOrder = PackedLayout.Descending;
102 /** @hidden @internal */ this._comparer = undefined;
103 /** @hidden @internal */ this._aspectRatio = 1;
104 /** @hidden @internal */ this._size = new go.Size(500, 500);
105 /** @hidden @internal */ this._defaultSize = this._size.copy();
106 /** @hidden @internal */ this._fillViewport = false; // true if size is (NaN, NaN)
107 /** @hidden @internal */ this._spacing = 0;
108 /** @hidden @internal */ this._hasCircularNodes = false;
109 /** @hidden @internal */ this._arrangesToOrigin = true;
110 /**
111 * @hidden @internal
112 * The forced spacing value applied in the {@link PackedLayout.Fit}
113 * and {@link PackedLayout.ExpandToFit} modes.
114 */
115 this._fixedSizeModeSpacing = 0;
116 /**
117 * @hidden @internal
118 * The actual target aspect ratio, set from either {@link #aspectRatio}
119 * or from the {@link #size}, depending on the {@link #packMode}.
120 */
121 this._eAspectRatio = this._aspectRatio;
122 // layout state
123 /** @hidden @internal */ this._center = new go.Point();
124 /** @hidden @internal */ this._bounds = new go.Rect();
125 /** @hidden @internal */ this._actualBounds = new go.Rect();
126 /** @hidden @internal */ this._enclosingCircle = null;
127 /** @hidden @internal */ this._minXSegment = null;
128 /** @hidden @internal */ this._minYSegment = null;
129 /** @hidden @internal */ this._maxXSegment = null;
130 /** @hidden @internal */ this._maxYSegment = null;
131 /** @hidden @internal */ this._tree = new Quadtree();
132 // saved node bounds and segment list to use to calculate enclosing circle in the enclosingCircle getter
133 /** @hidden @internal */ this._nodeBounds = [];
134 /** @hidden @internal */ this._segments = new CircularDoublyLinkedList();
135 }
136 /**
137 * Gets or sets the shape that nodes will be packed into. Valid values are
138 * {@link PackedLayout.Elliptical}, {@link PackedLayout.Rectangular}, and
139 * {@link PackedLayout.Spiral}.
140 *
141 * In {@link PackedLayout.Spiral} mode, nodes are not packed into a particular
142 * shape, but rather packed consecutively one after another in a spiral fashion.
143 * The {@link #aspectRatio} property is ignored in this mode, and
144 * the {@link #size} property (if provided) is expected to be square.
145 * If it is not square, the largest dimension given will be used. This mode
146 * currently only works with circular nodes, so setting it cause the assume that
147 * layout to assume that {@link #hasCircularNodes} is true.
148 *
149 * Note that this property sets only the shape, not the aspect ratio. The aspect
150 * ratio of this shape is determined by either {@link #aspectRatio}
151 * or {@link #size}, depending on the {@link #packMode}.
152 *
153 * When the {@link #packMode} is {@link PackedLayout.Fit} or
154 * {@link PackedLayout.ExpandToFit} and this property is set to true, the
155 * layout will attempt to make the diameter of the enclosing circle of the
156 * layout approximately equal to the greater dimension of the given
157 * {@link #size} property.
158 *
159 * The default value is {@link PackedLayout.Elliptical}.
160 */
161 get packShape() { return this._packShape; }
162 set packShape(value) {
163 if (this._packShape !== value && (value === PackedLayout.Elliptical || value === PackedLayout.Rectangular || value === PackedLayout.Spiral)) {
164 this._packShape = value;
165 this.invalidateLayout();
166 }
167 }
168 /**
169 * Gets or sets the mode that the layout will use to determine its size. Valid values
170 * are {@link PackedLayout.AspectOnly}, {@link PackedLayout.Fit}, and {@link PackedLayout.ExpandToFit}.
171 *
172 * The default value is {@link PackedLayout.AspectOnly}. In this mode, the layout will simply
173 * grow as needed, attempting to keep the aspect ratio defined by {@link #aspectRatio}.
174 */
175 get packMode() { return this._packMode; }
176 set packMode(value) {
177 if (value === PackedLayout.AspectOnly || value === PackedLayout.Fit || value === PackedLayout.ExpandToFit) {
178 this._packMode = value;
179 this.invalidateLayout();
180 }
181 }
182 /**
183 * Gets or sets the method by which nodes will be sorted before being packed. To change
184 * the order, see {@link #sortOrder}.
185 *
186 * The default value is {@link PackedLayout.None}, in which nodes will not be sorted at all.
187 */
188 get sortMode() { return this._sortMode; }
189 set sortMode(value) {
190 if (value === PackedLayout.None || value === PackedLayout.MaxSide || value === PackedLayout.Area) {
191 this._sortMode = value;
192 this.invalidateLayout();
193 }
194 }
195 /**
196 * Gets or sets the order that nodes will be sorted in before being packed. To change
197 * the sort method, see {@link #sortMode}.
198 *
199 * The default value is {@link PackedLayout.Descending}
200 */
201 get sortOrder() { return this._sortOrder; }
202 set sortOrder(value) {
203 if (value === PackedLayout.Descending || value === PackedLayout.Ascending) {
204 this._sortOrder = value;
205 this.invalidateLayout();
206 }
207 }
208 /**
209 * Gets or sets the comparison function used for sorting nodes.
210 *
211 * By default, the comparison function is set according to the values of {@link #sortMode}
212 * and {@link #sortOrder}.
213 *
214 * Whether this comparison function is used is determined by the value of {@link #sortMode}.
215 * Any value except {@link PackedLayout.None} will result in the comparison function being used.
216 * ```js
217 * $(PackedLayout,
218 * {
219 * sortMode: PackedLayout.Area,
220 * comparer: function(na, nb) {
221 * var na = na.data;
222 * var nb = nb.data;
223 * if (da.someProperty < db.someProperty) return -1;
224 * if (da.someProperty > db.someProperty) return 1;
225 * return 0;
226 * }
227 * }
228 * )
229 * ```
230 */
231 get comparer() { return this._comparer; }
232 set comparer(value) {
233 if (typeof value === 'function') {
234 this._comparer = value;
235 }
236 }
237 /**
238 * Gets or sets the aspect ratio for the shape that nodes will be packed into.
239 * The provided aspect ratio should be a nonzero postive number.
240 *
241 * Note that this only applies if the {@link #packMode} is
242 * {@link PackedLayout.AspectOnly}. Otherwise, the {@link #size}
243 * will determine the aspect ratio of the packed shape.
244 *
245 * The default value is 1.
246 */
247 get aspectRatio() { return this._aspectRatio; }
248 set aspectRatio(value) {
249 if (this.isNumeric(value) && isFinite(value) && value > 0) {
250 this._aspectRatio = value;
251 this.invalidateLayout();
252 }
253 }
254 /**
255 * Gets or sets the size for the shape that nodes will be packed into.
256 * To fill the viewport, set a size with a width and height of NaN. Size
257 * values of 0 are considered for layout purposes to instead be 1.
258 *
259 * If the width and height are set to NaN (to fill the viewport), but this
260 * layout has no diagram associated with it, the default value of size will
261 * be used instead.
262 *
263 * Note that this only applies if the {@link #packMode} is
264 * {@link PackedLayout.Fit} or {@link PackedLayout.ExpandToFit}.
265 *
266 * The default value is 500x500.
267 */
268 get size() { return this._size; }
269 set size(value) {
270 // check if both width and height are NaN, as per https://stackoverflow.com/a/16988441
271 if (value.width !== value.width && value.height !== value.height) {
272 this._size = value;
273 this._fillViewport = true;
274 this.invalidateLayout();
275 }
276 else if (this.isNumeric(value.width) && isFinite(value.width) && value.width >= 0
277 && this.isNumeric(value.height) && isFinite(value.height) && value.height >= 0) {
278 this._size = value;
279 this.invalidateLayout();
280 }
281 }
282 /**
283 * Gets or sets the spacing between nodes. This value can be set to any
284 * real number (a negative spacing will compress nodes together, and a
285 * positive spacing will leave space between them).
286 *
287 * Note that the spacing value is only respected in the {@link PackedLayout.Fit}
288 * {@link #packMode} if it does not cause the layout to grow outside
289 * of the specified bounds. In the {@link PackedLayout.ExpandToFit}
290 * {@link #packMode}, this property does not do anything.
291 *
292 * The default value is 0.
293 */
294 get spacing() { return this._spacing; }
295 set spacing(value) {
296 if (this.isNumeric(value) && isFinite(value)) {
297 this._spacing = value;
298 this.invalidateLayout();
299 }
300 }
301 /**
302 * Gets or sets whether or not to assume that nodes are circular. This changes
303 * the packing algorithm to one that is much more efficient for circular nodes.
304 *
305 * As this algorithm expects circles, it is assumed that if this property is set
306 * to true that the given nodes will all have the same height and width. All
307 * calculations are done using the width of the given nodes, so unexpected results
308 * may occur if the height differs from the width.
309 *
310 * The default value is false.
311 */
312 get hasCircularNodes() { return this._hasCircularNodes; }
313 set hasCircularNodes(value) {
314 if (typeof (value) === typeof (true) && value !== this._hasCircularNodes) {
315 this._hasCircularNodes = value;
316 this.invalidateLayout();
317 }
318 }
319 /**
320 * This read-only property is the effective spacing calculated after {@link PackedLayout#doLayout}.
321 *
322 * If the {@link #packMode} is {@link PackedLayout.AspectOnly}, this will simply be the
323 * {@link #spacing} property. However, in the {@link PackedLayout.Fit} and
324 * {@link PackedLayout.ExpandToFit} modes, this property will include the forced spacing added by
325 * the modes themselves.
326 *
327 * Note that this property will only return a valid value after a layout has been performed. Before
328 * then, its behavior is undefined.
329 */
330 get actualSpacing() { return this.spacing + this._fixedSizeModeSpacing; }
331 /**
332 * This read-only property returns the actual rectangular bounds occupied by the packed nodes.
333 * This property does not take into account any kind of spacing around the packed nodes.
334 *
335 * Note that this property will only return a valid value after a layout has been performed. Before
336 * then, its behavior is undefined.
337 */
338 get actualBounds() { return this._actualBounds; }
339 /**
340 * This read-only property returns the smallest enclosing circle around the packed nodes. It makes
341 * use of the {@link #hasCircularNodes} property to determine whether or not to make
342 * enclosing circle calculations for rectangles or for circles. This property does not take into
343 * account any kind of spacing around the packed nodes. The enclosing circle calculation is
344 * performed the first time this property is retrieved, and then cached to prevent slow accesses
345 * in the future.
346 *
347 * Note that this property will only return a valid value after a layout has been performed. Before
348 * then, its behavior is undefined.
349 *
350 * This property is included as it may be useful for some data visualizations.
351 */
352 get enclosingCircle() {
353 if (this._enclosingCircle === null) {
354 if (this.hasCircularNodes || this.packShape === PackedLayout.Spiral) { // remember, spiral mode assumes hasCircularNodes
355 const circles = new Array(this._nodeBounds.length);
356 for (let i = 0; i < circles.length; i++) {
357 const bounds = this._nodeBounds[i];
358 const r = bounds.width / 2;
359 circles[i] = new Circle(bounds.x + r, bounds.y + r, r);
360 }
361 this._enclosingCircle = enclose(circles);
362 }
363 else {
364 const points = new Array(); // TODO: make this work with segments, not the whole nodeboudns list
365 let segment = this._segments.start;
366 if (segment !== null) {
367 do {
368 points.push(new go.Point(segment.data.x1, segment.data.y1));
369 segment = segment.next;
370 } while (segment !== this._segments.start);
371 }
372 this._enclosingCircle = enclose(points);
373 }
374 }
375 return this._enclosingCircle;
376 }
377 /**
378 * Gets or sets whether or not to use the {@link Layout#arrangementOrigin}
379 * property when placing nodes.
380 *
381 * The default value is true.
382 */
383 get arrangesToOrigin() { return this._arrangesToOrigin; }
384 set arrangesToOrigin(value) {
385 if (typeof (value) === typeof (true) && value !== this._arrangesToOrigin) {
386 this._arrangesToOrigin = value;
387 this.invalidateLayout();
388 }
389 }
390 /**
391 * Performs the PackedLayout.
392 * @this {PackedLayout}
393 * @param {Diagram|Group|Iterable.<Part>} coll A {@link Diagram} or a {@link Group} or a collection of {@link Part}s.
394 */
395 doLayout(coll) {
396 const diagram = this.diagram;
397 if (diagram !== null)
398 diagram.startTransaction('Layout');
399 this._bounds = new go.Rect();
400 this._enclosingCircle = null;
401 // push all nodes in parts iterator to an array for easy sorting
402 const it = this.collectParts(coll).iterator;
403 const nodes = [];
404 let averageSize = 0;
405 let maxSize = 0;
406 while (it.next()) {
407 const node = it.value;
408 if (node instanceof go.Node) {
409 nodes.push(node);
410 averageSize += node.actualBounds.width + node.actualBounds.height;
411 if (node.actualBounds.width > maxSize) {
412 maxSize = node.actualBounds.width;
413 }
414 else if (node.actualBounds.height > maxSize) {
415 maxSize = node.actualBounds.height;
416 }
417 }
418 }
419 averageSize = averageSize / (nodes.length * 2);
420 if (averageSize < 1) {
421 averageSize = 1;
422 }
423 this.arrangementOrigin = this.initialOrigin(this.arrangementOrigin);
424 if (this.sortMode !== PackedLayout.None) {
425 if (!this.comparer) {
426 const sortOrder = this.sortOrder;
427 const sortMode = this.sortMode;
428 this.comparer = (a, b) => {
429 const sortVal = sortOrder === PackedLayout.Ascending ? 1 : -1;
430 if (sortMode === PackedLayout.MaxSide) {
431 const aMax = Math.max(a.actualBounds.width, a.actualBounds.height);
432 const bMax = Math.max(b.actualBounds.width, b.actualBounds.height);
433 if (aMax > bMax) {
434 return sortVal;
435 }
436 else if (bMax > aMax) {
437 return -sortVal;
438 }
439 return 0;
440 }
441 else if (sortMode === PackedLayout.Area) {
442 const area1 = a.actualBounds.width * a.actualBounds.height;
443 const area2 = b.actualBounds.width * b.actualBounds.height;
444 if (area1 > area2) {
445 return sortVal;
446 }
447 else if (area2 > area1) {
448 return -sortVal;
449 }
450 return 0;
451 }
452 return 0;
453 };
454 }
455 nodes.sort(this.comparer);
456 }
457 let targetWidth = this.size.width !== 0 ? this.size.width : 1;
458 let targetHeight = this.size.height !== 0 ? this.size.height : 1;
459 if (this._fillViewport && this.diagram !== null) {
460 targetWidth = this.diagram.viewportBounds.width !== 0 ? this.diagram.viewportBounds.width : 1;
461 targetHeight = this.diagram.viewportBounds.height !== 0 ? this.diagram.viewportBounds.height : 1;
462 }
463 else if (this._fillViewport) {
464 targetWidth = this._defaultSize.width !== 0 ? this._defaultSize.width : 1;
465 targetHeight = this._defaultSize.height !== 0 ? this._defaultSize.height : 1;
466 }
467 // set the target aspect ratio using the given bounds if necessary
468 if (this.packMode === PackedLayout.Fit || this.packMode === PackedLayout.ExpandToFit) {
469 this._eAspectRatio = targetWidth / targetHeight;
470 }
471 else {
472 this._eAspectRatio = this.aspectRatio;
473 }
474 let fits = this.hasCircularNodes || this.packShape === PackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes);
475 // in the Fit and ExpandToFit modes, we need to run the packing another time to figure out what the correct
476 // _fixedModeSpacing should be. Then the layout is run a final time with the correct spacing.
477 if (this.packMode === PackedLayout.Fit || this.packMode === PackedLayout.ExpandToFit) {
478 const bounds0 = this._bounds.copy();
479 this._bounds = new go.Rect();
480 this._fixedSizeModeSpacing = Math.floor(averageSize);
481 fits = this.hasCircularNodes || this.packShape === PackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes);
482 if ((this.hasCircularNodes || this.packShape === PackedLayout.Spiral) && this.packShape === PackedLayout.Spiral) {
483 const targetDiameter = Math.max(targetWidth, targetHeight);
484 const oldDiameter = targetDiameter === targetWidth ? bounds0.width : bounds0.height;
485 const newDiameter = targetDiameter === targetWidth ? this._bounds.width : this._bounds.height;
486 const diff = (newDiameter - oldDiameter) / this._fixedSizeModeSpacing;
487 this._fixedSizeModeSpacing = (targetDiameter - oldDiameter) / diff;
488 }
489 else {
490 const dx = (this._bounds.width - bounds0.width) / this._fixedSizeModeSpacing;
491 const dy = (this._bounds.height - bounds0.height) / this._fixedSizeModeSpacing;
492 const paddingX = (targetWidth - bounds0.width) / dx;
493 const paddingY = (targetHeight - bounds0.height) / dy;
494 this._fixedSizeModeSpacing = Math.abs(paddingX) > Math.abs(paddingY) ? paddingX : paddingY;
495 }
496 if (this.packMode === PackedLayout.Fit) {
497 // make sure that the spacing is not positive in this mode
498 this._fixedSizeModeSpacing = Math.min(this._fixedSizeModeSpacing, 0);
499 }
500 if (this._fixedSizeModeSpacing === Infinity) {
501 this._fixedSizeModeSpacing = -maxSize;
502 }
503 this._bounds = new go.Rect();
504 fits = this.hasCircularNodes || this.packShape === PackedLayout.Spiral ? this.fitCircles(nodes) : this.fitRects(nodes);
505 }
506 // move the nodes and calculate the actualBounds property
507 if (this.arrangesToOrigin) {
508 this._actualBounds = new go.Rect(this.arrangementOrigin.x, this.arrangementOrigin.y, 0, 0);
509 }
510 const nodeBounds = new Array(nodes.length);
511 for (let i = 0; i < nodes.length; i++) {
512 const fit = fits[i];
513 const node = nodes[i];
514 if (this.arrangesToOrigin) {
515 // translate coordinates to respect this.arrangementOrigin
516 // this.arrangementOrigin should be the top left corner of the bounding box around the layout
517 fit.x = fit.x - this._bounds.x + this.arrangementOrigin.x;
518 fit.y = fit.y - this._bounds.y + this.arrangementOrigin.y;
519 }
520 node.moveTo(fit.x, fit.y);
521 nodeBounds[i] = node.actualBounds;
522 this._actualBounds.unionRect(node.actualBounds);
523 }
524 this._nodeBounds = nodeBounds; // save node bounds in case we want to calculate the smallest enclosing circle later
525 // can be overriden to change layout behavior, doesn't do anything by default
526 this.commitLayout();
527 if (diagram !== null)
528 diagram.commitTransaction('Layout');
529 this.isValidLayout = true;
530 }
531 /**
532 * This method is called at the end of {@link #doLayout}, but
533 * before the layout transaction is committed. It can be overriden and
534 * used to customize layout behavior. By default, the method does nothing.
535 * @expose
536 * @this {PackedLayout}
537 */
538 commitLayout() { }
539 /**
540 * @hidden @internal
541 * Runs a circle packing algorithm on the given array of nodes. The
542 * algorithm used is a slightly modified version of the one proposed
543 * by Wang et al. in "Visualization of large hierarchical data by
544 * circle packing", 2006.
545 * @this {PackedLayout}
546 * @param nodes the array of Nodes to pack
547 * @return {Array<Rect>} an array of positioned rectangles corresponding to the nodes argument
548 */
549 fitCircles(nodes) {
550 function place(a, b, c) {
551 const ax = a.centerX;
552 const ay = a.centerY;
553 let da = (b.width + c.width) / 2;
554 let db = (a.width + c.width) / 2;
555 const dx = b.centerX - ax;
556 const dy = b.centerY - ay;
557 const dc = dx * dx + dy * dy;
558 if (dc) {
559 const x = 0.5 + ((db *= db) - (da *= da)) / (2 * dc);
560 const y = Math.sqrt(Math.max(0, 2 * da * (db + dc) - (db -= dc) * db - da * da)) / (2 * dc);
561 c.x = (ax + x * dx + y * dy) - (c.width / 2);
562 c.y = (ay + x * dy - y * dx) - (c.height / 2);
563 }
564 else {
565 c.x = ax + db;
566 c.y = ay;
567 }
568 return c;
569 }
570 function intersects(a, b) {
571 const ar = a.height / 2;
572 const br = b.height / 2;
573 const dist = Math.sqrt(a.center.distanceSquaredPoint(b.center));
574 const difference = dist - (ar + br);
575 return difference < -0.0000001;
576 }
577 const aspect = this._eAspectRatio;
578 const shape = this.packShape;
579 const placementCost = this.placementCost;
580 function score(n) {
581 const a = n.data;
582 const b = n.next.data;
583 const ar = a.width / 2;
584 const br = b.width / 2;
585 const ab = ar + br;
586 const dx = (a.centerX * br + b.centerX * ar) / ab;
587 const dy = (a.centerY * br + b.centerY * ar) / ab * aspect;
588 return shape === PackedLayout.Elliptical ? dx * dx + dy * dy : Math.max(dx * dx, dy * dy);
589 }
590 const sideSpacing = (this.spacing + this._fixedSizeModeSpacing) / 2;
591 const fits = [];
592 const frontChain = new CircularDoublyLinkedList();
593 if (!nodes.length)
594 return fits;
595 let r1 = nodes[0].actualBounds.copy().inflate(sideSpacing, sideSpacing);
596 r1.setTo(0, 0, r1.width === 0 ? 0.1 : r1.width, r1.height === 0 ? 0.1 : r1.height);
597 fits.push(r1.setTo(0, 0, r1.width, r1.height));
598 this._bounds.unionRect(r1);
599 if (nodes.length < 2)
600 return fits;
601 let r2 = nodes[1].actualBounds.copy().inflate(sideSpacing, sideSpacing);
602 r2.setTo(0, 0, r2.width === 0 ? 0.1 : r2.width, r2.height === 0 ? 0.1 : r2.height);
603 fits.push(r2.setTo(-r2.width, r1.centerY - r2.width / 2, r2.width, r2.height));
604 this._bounds.unionRect(r2);
605 if (nodes.length < 3)
606 return fits;
607 let r3 = nodes[2].actualBounds.copy().inflate(sideSpacing, sideSpacing);
608 r3.setTo(0, 0, r3.width === 0 ? 0.1 : r3.width, r3.height === 0 ? 0.1 : r3.height);
609 fits.push(place(r2, r1, r3));
610 this._bounds.unionRect(r3);
611 let n2 = frontChain.push(r2);
612 let n3 = frontChain.push(r3);
613 let n1 = frontChain.push(r1);
614 pack: for (let i = 3; i < nodes.length; i++) {
615 r3 = nodes[i].actualBounds.copy().inflate(sideSpacing, sideSpacing);
616 r3.setTo(0, 0, r3.width === 0 ? 0.1 : r3.width, r3.height === 0 ? 0.1 : r3.height);
617 place(n1.data, n2.data, r3);
618 let j = n2.next;
619 let k = n1.prev;
620 let sj = n2.data.width / 2;
621 let sk = n1.data.width / 2;
622 do {
623 if (sj <= sk) {
624 if (intersects(j.data, r3)) {
625 n2 = frontChain.removeBetween(n1, j), i--;
626 continue pack;
627 }
628 sj += j.data.width / 2, j = j.next;
629 }
630 else {
631 if (intersects(k.data, r3)) {
632 frontChain.removeBetween(k, n2);
633 n1 = k, i--;
634 continue pack;
635 }
636 sk += k.data.width / 2, k = k.prev;
637 }
638 } while (j !== k.next);
639 fits.push(r3);
640 this._bounds.unionRect(r3);
641 n2 = n3 = frontChain.insertAfter(r3, n1);
642 if (this.packShape !== PackedLayout.Spiral) {
643 let aa = score(n1);
644 while ((n3 = n3.next) !== n2) {
645 const ca = score(n3);
646 if (ca < aa) {
647 n1 = n3, aa = ca;
648 }
649 }
650 n2 = n1.next;
651 }
652 }
653 return fits;
654 }
655 /**
656 * @hidden @internal
657 * Runs a rectangle packing algorithm on the given array of nodes.
658 * The algorithm presented is original, and operates by maintaining
659 * a representation (with segments) of the perimeter of the already
660 * packed shape. The perimeter of segments is stored in both a linked
661 * list (for ordered iteration) and a quadtree (for fast intersection
662 * detection). Similar to the circle packing algorithm presented
663 * above, this is a greedy algorithm.
664 *
665 * For each node, a large list of possible placements is created,
666 * each one relative to a segment on the perimeter. These placements
667 * are sorted according to a cost function, and then the lowest cost
668 * placement with no intersections is picked. The perimeter
669 * representation is then updated according to the new placement.
670 *
671 * However, in addition to placements made relative to a single segment
672 * on the perimeter, the algorithm also attempts to make placements
673 * between two nonsequential segments ("skip fits"), closing gaps in the
674 * packed shape. If a placement made in this way has no intersections
675 * and a lower cost than any of the original placements, it is picked
676 * instead. This step occurs simultaneously to checking intersections on
677 * the original placement list.
678 *
679 * Intersections for new placements are checked only against the current
680 * perimeter of segments, rather than the entire packed shape.
681 * Additionally, before the quadtree is queried at all, a few closely
682 * surrounding segments to the placement are checked in case an
683 * intersection can be found more quickly. The combination of these two
684 * strategies enables intersection checking to take place extremely
685 * quickly, when it would normally be the slowest part of the entire
686 * algorithm.
687 *
688 * @this {PackedLayout}
689 * @param nodes the array of Nodes to pack
690 * @return {Array<Rect>} an array of positioned rectangles corresponding to the nodes argument
691 */
692 fitRects(nodes) {
693 const sideSpacing = (this.spacing + this._fixedSizeModeSpacing) / 2;
694 const fits = [];
695 const segments = new CircularDoublyLinkedList();
696 // reset layout state
697 this._tree.clear();
698 this._minXSegment = null;
699 this._maxXSegment = null;
700 this._minYSegment = null;
701 this._maxYSegment = null;
702 if (nodes.length < 1) {
703 return fits;
704 }
705 // place first node at 0, 0
706 const bounds0 = nodes[0].actualBounds;
707 fits.push(new go.Rect(sideSpacing, sideSpacing, bounds0.width, bounds0.height));
708 fits[0].inflate(sideSpacing, sideSpacing);
709 fits[0].setTo(0, 0, fits[0].width === 0 ? 0.1 : fits[0].width, fits[0].height === 0 ? 0.1 : fits[0].height);
710 this._bounds.unionRect(fits[0]);
711 this._center = fits[0].center;
712 const s1 = new Segment(0, 0, fits[0].width, 0, false);
713 const s2 = new Segment(fits[0].width, 0, fits[0].width, fits[0].height, false);
714 const s3 = new Segment(fits[0].width, fits[0].height, 0, fits[0].height, false);
715 const s4 = new Segment(0, fits[0].height, 0, 0, false);
716 this._tree.add(s1, this.rectFromSegment(s1));
717 this._tree.add(s2, this.rectFromSegment(s2));
718 this._tree.add(s3, this.rectFromSegment(s3));
719 this._tree.add(s4, this.rectFromSegment(s4));
720 segments.push(s1, s2, s3, s4);
721 this.fixMissingMinMaxSegments(true);
722 for (let i = 1; i < nodes.length; i++) {
723 const node = nodes[i];
724 const bounds = node.actualBounds.copy().inflate(sideSpacing, sideSpacing);
725 bounds.setTo(0, 0, bounds.width === 0 ? 0.1 : bounds.width, bounds.height === 0 ? 0.1 : bounds.height);
726 const possibleFits = new Array(segments.length);
727 let j = 0;
728 let s = segments.start;
729 do {
730 // make sure segment is perfectly straight (fixing some floating point error)
731 const sdata = s.data;
732 sdata.x1 = s.prev.data.x2;
733 sdata.y1 = s.prev.data.y2;
734 if (sdata.isHorizontal) {
735 sdata.y2 = sdata.y1;
736 }
737 else {
738 sdata.x2 = sdata.x1;
739 }
740 const fitBounds = this.getBestFitRect(s, bounds.width, bounds.height);
741 const cost = this.placementCost(fitBounds);
742 possibleFits[j] = new Fit(fitBounds, cost, s);
743 s = s.next;
744 j++;
745 } while (s !== segments.start);
746 possibleFits.sort((a, b) => {
747 return a.cost - b.cost;
748 });
749 /* scales the cost of skip fits. a number below
750 * one makes skip fits more likely to appear,
751 * which is preferable because they are more
752 * aesthetically pleasing and reduce the total
753 * number of segments.
754 */
755 const skipFitScaleFactor = 0.98;
756 let bestFit = null;
757 let onlyCheckSkipFits = false;
758 for (const fit of possibleFits) {
759 if (bestFit && bestFit.cost <= fit.cost) {
760 onlyCheckSkipFits = true;
761 }
762 let hasIntersections = true; // set initially to true to make skip fit checking work when onlyCheckSkipFits = true
763 if (!onlyCheckSkipFits) {
764 hasIntersections = this.fastFitHasIntersections(fit) || this.fitHasIntersections(fit);
765 if (!hasIntersections) {
766 bestFit = fit;
767 continue;
768 }
769 }
770 // check skip fits
771 if (hasIntersections && !fit.s1.data.p1Concave && (fit.s1.next.data.p1Concave || fit.s1.next.next.data.p1Concave)) {
772 let [nextSegment, usePreviousSegment] = this.findNextOrientedSegment(fit, fit.s1.next);
773 let nextSegmentTouchesFit = false;
774 while (hasIntersections && nextSegment !== null) {
775 fit.bounds = this.rectAgainstMultiSegment(fit.s1, nextSegment, bounds.width, bounds.height);
776 hasIntersections = this.fastFitHasIntersections(fit) || this.fitHasIntersections(fit);
777 nextSegmentTouchesFit = this.segmentIsOnFitPerimeter(nextSegment.data, fit.bounds);
778 if (hasIntersections || !nextSegmentTouchesFit) {
779 [nextSegment, usePreviousSegment] = this.findNextOrientedSegment(fit, nextSegment);
780 }
781 }
782 if (!hasIntersections && nextSegment !== null && nextSegmentTouchesFit) {
783 fit.cost = this.placementCost(fit.bounds) * skipFitScaleFactor;
784 if (bestFit === null || fit.cost <= bestFit.cost) {
785 bestFit = fit;
786 bestFit.s2 = nextSegment;
787 if (usePreviousSegment) {
788 bestFit.s1 = bestFit.s1.prev;
789 }
790 }
791 }
792 }
793 }
794 if (bestFit !== null) {
795 this.updateSegments(bestFit, segments);
796 fits.push(bestFit.bounds);
797 this._bounds.unionRect(bestFit.bounds);
798 }
799 }
800 // save segments in case we want to calculate the enclosing circle later
801 this._segments = segments;
802 return fits;
803 }
804 /**
805 * @hidden @internal
806 * Attempts to find a segment which can be used to create a new skip fit
807 * between fit.s1 and the found segment. A number of conditions are checked
808 * before returning a segment, ensuring that if the skip fit *does* intersect
809 * with the already packed shape, it will do so along the perimeter (so that it
810 * can be detected with only knowledge about the perimeter). Multiple oriented
811 * segments can be found for a given fit, so this function starts searching at
812 * the segment after the given lastSegment parameter.
813 *
814 * Oriented segments can be oriented with either fit.s1, or fit.s1.prev. The
815 * second return value (usePreviousSegment) indicates which the found segment is.
816 *
817 * @this {PackedLayout}
818 * @param fit the fit to search for a new segment for
819 * @param lastSegment the last segment found.
820 */
821 findNextOrientedSegment(fit, lastSegment) {
822 lastSegment = lastSegment.next;
823 const orientation = this.segmentOrientation(fit.s1.prev.data, fit.s1.data);
824 const targetOrientation = (orientation + 1) % 4;
825 while (!this.segmentIsMinOrMax(lastSegment.data)) {
826 const usePreviousSegment = lastSegment.data.isHorizontal === fit.s1.data.isHorizontal;
827 let lastOrientation;
828 if (usePreviousSegment) {
829 lastOrientation = this.segmentOrientation(lastSegment.data, lastSegment.next.data);
830 if (lastSegment.next.data.p1Concave) {
831 lastOrientation = (lastOrientation + 1) % 4;
832 }
833 }
834 else {
835 lastOrientation = this.segmentOrientation(lastSegment.prev.data, lastSegment.data);
836 if (lastSegment.data.p1Concave) {
837 lastOrientation = (lastOrientation + 1) % 4;
838 }
839 }
840 const validLastOrientation = lastOrientation === targetOrientation;
841 const exceededPrimaryDimension = fit.s1.data.isHorizontal ?
842 Math.abs(lastSegment.data.y1 - fit.s1.data.y1) + 1e-7 > fit.bounds.height :
843 Math.abs(lastSegment.data.x1 - fit.s1.data.x1) + 1e-7 > fit.bounds.width;
844 let validCornerPlacement;
845 let exceededSecondaryDimension;
846 switch (orientation) {
847 case Orientation.NE:
848 validCornerPlacement = fit.s1.data.x1 < lastSegment.data.x1;
849 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.y1 - fit.bounds.height >= lastSegment.data.y1 : fit.s1.data.y2 + fit.bounds.height <= lastSegment.data.y1;
850 break;
851 case Orientation.NW:
852 validCornerPlacement = fit.s1.data.y1 > lastSegment.data.y1;
853 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.x1 - fit.bounds.width >= lastSegment.data.x1 : fit.s1.data.x2 + fit.bounds.width <= lastSegment.data.x1;
854 break;
855 case Orientation.SW:
856 validCornerPlacement = fit.s1.data.x1 > lastSegment.data.x1;
857 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.y1 + fit.bounds.height <= lastSegment.data.y1 : fit.s1.data.y2 - fit.bounds.height >= lastSegment.data.y1;
858 break;
859 case Orientation.SE:
860 validCornerPlacement = fit.s1.data.y1 < lastSegment.data.y1;
861 exceededSecondaryDimension = usePreviousSegment ? fit.s1.data.x1 + fit.bounds.width <= lastSegment.data.x1 : fit.s1.data.x2 - fit.bounds.width >= lastSegment.data.x1;
862 break;
863 default:
864 throw new Error('Unknown orientation ' + orientation);
865 }
866 if (!exceededPrimaryDimension && !exceededSecondaryDimension && validCornerPlacement && validLastOrientation) {
867 return [lastSegment, usePreviousSegment];
868 }
869 lastSegment = lastSegment.next;
870 }
871 return [null, false];
872 }
873 /**
874 * @hidden @internal
875 * Returns the orientation of two adjacent segments. s2
876 * is assumed to start at the end of s1.
877 * @this {PackedLayout}
878 * @param s1 the first segment
879 * @param s2 the second segment
880 */
881 segmentOrientation(s1, s2) {
882 if (s1.isHorizontal) {
883 if (s1.x1 < s2.x1) {
884 return s2.p1Concave ? Orientation.SE : Orientation.NE;
885 }
886 else {
887 return s2.p1Concave ? Orientation.NW : Orientation.SW;
888 }
889 }
890 else {
891 if (s1.y1 < s2.y1) {
892 return s2.p1Concave ? Orientation.SW : Orientation.SE;
893 }
894 else {
895 return s2.p1Concave ? Orientation.NE : Orientation.NW;
896 }
897 }
898 }
899 /**
900 * @hidden @internal
901 * Fits a rectangle between two segments (used for skip fits). This is an operation
902 * related more to corners than segments, so fit.s1 should always be supplied for
903 * segment a (even if usePreviousSegment was true in the return value for
904 * {@link #findNextOrientedSegment}).
905 *
906 * @this {PackedLayout}
907 * @param a the first segment to fit between, should always be fit.s1
908 * @param b the second segment to fit between, found with {@link #findNextOrientedSegment}
909 * @param width the width of the rectangle, should be fit.width
910 * @param height the height of the rectangle, should be fit.height
911 */
912 rectAgainstMultiSegment(a, b, width, height) {
913 switch (this.segmentOrientation(a.prev.data, a.data)) {
914 case Orientation.NE:
915 if (a.data.y1 > b.data.y2) {
916 return new go.Rect(b.data.x1 - width, a.data.y1 - height, width, height);
917 }
918 else {
919 return new go.Rect(a.data.x1, b.data.y1 - height, width, height);
920 }
921 case Orientation.NW:
922 if (a.data.x1 > b.data.x2) {
923 return new go.Rect(a.data.x1 - width, b.data.y1, width, height);
924 }
925 else {
926 return new go.Rect(b.data.x1 - width, a.data.y1 - height, width, height);
927 }
928 case Orientation.SW:
929 if (a.data.y1 < b.data.y2) {
930 return new go.Rect(b.data.x1, a.data.y1, width, height);
931 }
932 else {
933 return new go.Rect(a.data.x1 - width, b.data.y1, width, height);
934 }
935 case Orientation.SE:
936 if (a.data.x1 < b.data.x2) {
937 return new go.Rect(a.data.x1, b.data.y1 - height, width, height);
938 }
939 else {
940 return new go.Rect(b.data.x1, a.data.y1, width, height);
941 }
942 }
943 }
944 /**
945 * @hidden @internal
946 * Gets the rectangle placed against the given segment with the lowest
947 * placement cost. Rectangles can be placed against a segment either at
948 * the top/left side, the bottom/right side, or at the center coordinate
949 * of the entire packed shape (if the segment goes through either the x
950 * or y coordinate of the center).
951 * @this {PackedLayout}
952 * @param s the segment to place against
953 * @param width the width of the fit, fit.width
954 * @param height the height of the fit, fit.height
955 */
956 getBestFitRect(s, width, height) {
957 let x1 = s.data.x1;
958 let y1 = s.data.y1;
959 let x2 = s.data.x2;
960 let y2 = s.data.y2;
961 let dir = this.segmentOrientation(s.prev.data, s.data);
962 if (s.data.p1Concave) {
963 dir = (dir + 3) % 4;
964 }
965 const coordIsX = dir === Orientation.NW || dir === Orientation.SE;
966 if (dir === Orientation.NE) {
967 y2 -= height;
968 }
969 else if (dir === Orientation.SE) {
970 x1 -= width;
971 }
972 else if (dir === Orientation.SW) {
973 x1 -= width;
974 y1 -= height;
975 x2 -= width;
976 }
977 else if (dir === Orientation.NW) {
978 y1 -= height;
979 x2 -= width;
980 y2 -= height;
981 }
982 const r = new go.Rect(x1, y1, width, height);
983 const cost1 = this.placementCost(r);
984 const cost2 = this.placementCost(r.setTo(x2, y2, width, height));
985 let cost3 = Infinity;
986 if (coordIsX && (this._center.x - (x1 + width / 2)) * (this._center.x - (x2 + width / 2)) < 0) {
987 cost3 = this.placementCost(r.setTo(this._center.x - width / 2, y1, width, height));
988 }
989 else if (!coordIsX && (this._center.y - (y1 + height / 2)) * (this._center.y - (y2 + height / 2)) < 0) {
990 cost3 = this.placementCost(r.setTo(x1, this._center.y - height / 2, width, height));
991 }
992 return cost3 < cost2 && cost3 < cost1 ? r
993 : (cost2 < cost1 ? r.setTo(x2, y2, width, height)
994 : r.setTo(x1, y1, width, height));
995 }
996 /**
997 * @hidden @internal
998 * Checks if a segment is on the perimeter of the given fit bounds.
999 * Also returns true if the segment is within the rect, but that
1000 * shouldn't matter for any of the cases where this function is used.
1001 * @this {PackedLayout}
1002 * @param s the segment to test
1003 * @param bounds the fit bounds
1004 */
1005 segmentIsOnFitPerimeter(s, bounds) {
1006 const xCoordinatesTogether = this.numberIsBetween(s.x1, bounds.left, bounds.right)
1007 || this.numberIsBetween(s.x2, bounds.left, bounds.right)
1008 || this.numberIsBetween(bounds.left, s.x1, s.x2)
1009 || this.numberIsBetween(bounds.right, s.x1, s.x2);
1010 const yCoordinatesTogether = this.numberIsBetween(s.y1, bounds.top, bounds.bottom)
1011 || this.numberIsBetween(s.y2, bounds.top, bounds.bottom)
1012 || this.numberIsBetween(bounds.top, s.y1, s.y2)
1013 || this.numberIsBetween(bounds.bottom, s.y1, s.y2);
1014 return (s.isHorizontal && (this.approxEqual(s.y1, bounds.top) || this.approxEqual(s.y1, bounds.bottom)) && xCoordinatesTogether)
1015 || (!s.isHorizontal && (this.approxEqual(s.x1, bounds.left) || this.approxEqual(s.x1, bounds.right)) && yCoordinatesTogether);
1016 }
1017 /**
1018 * @hidden @internal
1019 * Checks if a point is on the perimeter of the given fit bounds.
1020 * Also returns true if the point is within the rect, but that
1021 * shouldn't matter for any of the cases where this function is used.
1022 * @this {PackedLayout}
1023 * @param x the x coordinate of the point to test
1024 * @param y the y coordinate of the point to test
1025 * @param bounds the fit bounds
1026 */
1027 pointIsOnFitPerimeter(x, y, bounds) {
1028 return (x >= bounds.left - 1e-7 && x <= bounds.right + 1e-7 && y >= bounds.top - 1e-7 && y <= bounds.bottom + 1e-7);
1029 }
1030 /**
1031 * @hidden @internal
1032 * Checks if a point is on the corner of the given fit bounds.
1033 * @this {PackedLayout}
1034 * @param x the x coordinate of the point to test
1035 * @param y the y coordinate of the point to test
1036 * @param bounds the fit bounds
1037 */
1038 pointIsFitCorner(x, y, bounds) {
1039 return (this.approxEqual(x, bounds.left) && this.approxEqual(y, bounds.top)) ||
1040 (this.approxEqual(x, bounds.right) && this.approxEqual(y, bounds.top)) ||
1041 (this.approxEqual(x, bounds.left) && this.approxEqual(y, bounds.bottom)) ||
1042 (this.approxEqual(x, bounds.right) && this.approxEqual(y, bounds.bottom));
1043 }
1044 /**
1045 * @hidden @internal
1046 * Updates the representation of the perimeter of segments after
1047 * a new placement is made. This modifies the given segments list,
1048 * as well as the quadtree class variable {@link #_tree}.
1049 * Also updates the minimum/maximum segments if they have changed as
1050 * a result of the new placement.
1051 * @this {PackedLayout}
1052 * @param fit the fit to add
1053 * @param segments the list of segments to update
1054 */
1055 updateSegments(fit, segments) {
1056 let s0 = fit.s1;
1057 while (this.pointIsOnFitPerimeter(s0.data.x1, s0.data.y1, fit.bounds)) {
1058 s0 = s0.prev;
1059 }
1060 if (!this.segmentIsMinOrMax(s0.data) || !this.segmentIsMinOrMax(s0.prev.data)) {
1061 let sTest = s0.prev.prev; // test for conflicting segments
1062 let sFound = null;
1063 let minMaxCount = 0;
1064 while (minMaxCount < 2) {
1065 if (this.segmentIsOnFitPerimeter(sTest.data, fit.bounds)) {
1066 sFound = sTest;
1067 }
1068 sTest = sTest.prev;
1069 if (this.segmentIsMinOrMax(sTest.next.data)) {
1070 minMaxCount++;
1071 }
1072 }
1073 if (sFound !== null) {
1074 while (this.pointIsOnFitPerimeter(sFound.data.x1, sFound.data.y1, fit.bounds)) {
1075 sFound = sFound.prev;
1076 }
1077 this.removeBetween(segments, sFound, s0);
1078 s0 = sFound;
1079 }
1080 }
1081 let nextConvexCornerOrientation;
1082 let lastConvexCornerOrientation;
1083 let testOrientation = this.segmentOrientation(s0.prev.data, s0.data);
1084 if (s0.data.p1Concave) {
1085 testOrientation = (testOrientation + 3) % 4;
1086 }
1087 let [cornerX, cornerY] = this.cornerFromRect(testOrientation, fit.bounds);
1088 const extended0 = this.approxEqual(cornerX, s0.data.x2) && this.approxEqual(cornerY, s0.data.y2);
1089 let shortened0Precond;
1090 let [cornerX2, cornerY2] = this.cornerFromRect((testOrientation + 1) % 4, fit.bounds);
1091 if (s0.data.isHorizontal) {
1092 shortened0Precond = this.numberIsBetween(cornerX2, s0.data.x1, s0.data.x2) && this.approxEqual(cornerY2, s0.data.y1);
1093 }
1094 else {
1095 shortened0Precond = this.numberIsBetween(cornerY2, s0.data.y1, s0.data.y2) && this.approxEqual(cornerX2, s0.data.x1);
1096 }
1097 const shortened0 = !extended0 && this.pointIsFitCorner(s0.data.x2, s0.data.y2, fit.bounds)
1098 || !this.pointIsOnFitPerimeter(s0.data.x2, s0.data.y2, fit.bounds)
1099 || (this.pointIsOnFitPerimeter(s0.data.x2, s0.data.y2, fit.bounds)
1100 && !this.pointIsOnFitPerimeter(s0.data.x1, s0.data.y1, fit.bounds)
1101 && shortened0Precond);
1102 if (extended0) {
1103 // extend s0
1104 [s0.data.x2, s0.data.y2] = this.cornerFromRect((testOrientation + 3) % 4, fit.bounds);
1105 this._tree.setTo(s0.data, this.rectFromSegment(s0.data));
1106 nextConvexCornerOrientation = (testOrientation + 3) % 4;
1107 this.updateMinMaxSegments(s0.data);
1108 }
1109 else {
1110 if (shortened0) {
1111 [s0.data.x2, s0.data.y2] = this.cornerFromRect((testOrientation + 1) % 4, fit.bounds);
1112 this._tree.setTo(s0.data, this.rectFromSegment(s0.data));
1113 }
1114 const newSegment = new Segment(s0.data.x2, s0.data.y2, cornerX, cornerY, true);
1115 s0 = segments.insertAfter(newSegment, s0);
1116 this._tree.add(newSegment, this.rectFromSegment(newSegment));
1117 nextConvexCornerOrientation = testOrientation;
1118 this.updateMinMaxSegments(newSegment);
1119 }
1120 let sNext = fit.s2 ? fit.s2 : s0;
1121 while (this.pointIsOnFitPerimeter(sNext.data.x2, sNext.data.y2, fit.bounds)) {
1122 sNext = sNext.next;
1123 }
1124 if (!this.segmentIsMinOrMax(sNext.data) || !this.segmentIsMinOrMax(sNext.next.data)) {
1125 let sTest = sNext.next.next; // test for conflicting segments
1126 let sFound = null;
1127 let minMaxCount = 0;
1128 while (minMaxCount < 2) {
1129 if (this.segmentIsOnFitPerimeter(sTest.data, fit.bounds)) {
1130 sFound = sTest;
1131 }
1132 sTest = sTest.next;
1133 if (this.segmentIsMinOrMax(sTest.prev.data)) {
1134 minMaxCount++;
1135 }
1136 }
1137 if (sFound !== null) {
1138 sNext = sFound;
1139 while (this.pointIsOnFitPerimeter(sNext.data.x2, sNext.data.y2, fit.bounds)) {
1140 sNext = sNext.next;
1141 }
1142 }
1143 }
1144 testOrientation = this.segmentOrientation(sNext.data, sNext.next.data);
1145 if (sNext.data.p1Concave) {
1146 testOrientation = (testOrientation + 2) % 4;
1147 }
1148 if (sNext.next.data.p1Concave) {
1149 testOrientation = (testOrientation + 1) % 4;
1150 }
1151 [cornerX2, cornerY2] = this.cornerFromRect((testOrientation + 3) % 4, fit.bounds);
1152 if (sNext.data.isHorizontal && this.numberIsBetween(cornerX2, sNext.data.x1, sNext.data.x2) && this.approxEqual(cornerY2, sNext.data.y1)
1153 || (!sNext.data.isHorizontal && this.numberIsBetween(cornerY2, sNext.data.y1, sNext.data.y2) && this.approxEqual(cornerX2, sNext.data.x1))
1154 || (sNext.data.isHorizontal && this.numberIsBetween(fit.bounds.left, sNext.data.x1, sNext.data.x2) && this.numberIsBetween(fit.bounds.right, sNext.data.x1, sNext.data.x2)
1155 && (this.approxEqual(fit.bounds.top, sNext.data.y1) || this.approxEqual(fit.bounds.bottom, sNext.data.y1)))
1156 || (!sNext.data.isHorizontal && this.numberIsBetween(fit.bounds.top, sNext.data.y1, sNext.data.y2) && this.numberIsBetween(fit.bounds.bottom, sNext.data.y1, sNext.data.y2)
1157 && (this.approxEqual(fit.bounds.left, sNext.data.x1) || this.approxEqual(fit.bounds.right, sNext.data.x1)))) {
1158 sNext = sNext.next;
1159 testOrientation = this.segmentOrientation(sNext.data, sNext.next.data);
1160 if (sNext.data.p1Concave) {
1161 testOrientation = (testOrientation + 2) % 4;
1162 }
1163 if (sNext.next.data.p1Concave) {
1164 testOrientation = (testOrientation + 1) % 4;
1165 }
1166 }
1167 this.removeBetween(segments, s0, sNext);
1168 [cornerX, cornerY] = this.cornerFromRect(testOrientation, fit.bounds);
1169 if (this.approxEqual(cornerX, sNext.data.x1) && this.approxEqual(cornerY, sNext.data.y1)) {
1170 // extend sNext
1171 if (s0.data.isHorizontal === sNext.data.isHorizontal && (s0.data.isHorizontal ? this.approxEqual(s0.data.y1, sNext.data.y1) : this.approxEqual(s0.data.x1, sNext.data.x1))) {
1172 s0.data.x2 = sNext.data.x2;
1173 s0.data.y2 = sNext.data.y2;
1174 this.removeSegmentFromLayoutState(sNext);
1175 segments.remove(sNext);
1176 this._tree.setTo(s0.data, this.rectFromSegment(s0.data));
1177 lastConvexCornerOrientation = nextConvexCornerOrientation; // no additional segments need to be added
1178 this.updateMinMaxSegments(s0.data);
1179 }
1180 else {
1181 [sNext.data.x1, sNext.data.y1] = this.cornerFromRect((testOrientation + 1) % 4, fit.bounds);
1182 this._tree.setTo(sNext.data, this.rectFromSegment(sNext.data));
1183 lastConvexCornerOrientation = (testOrientation + 1) % 4;
1184 this.updateMinMaxSegments(sNext.data);
1185 }
1186 }
1187 else if (extended0 && (s0.data.isHorizontal ?
1188 this.approxEqual(s0.data.y1, sNext.data.y1) && this.numberIsBetween(sNext.data.x1, s0.data.x1, s0.data.x2) :
1189 this.approxEqual(s0.data.x1, sNext.data.x1) && this.numberIsBetween(sNext.data.y1, s0.data.y1, s0.data.y2))) {
1190 if (s0.data.isHorizontal) {
1191 s0.data.x2 = sNext.data.x1;
1192 }
1193 else {
1194 s0.data.y2 = sNext.data.y1;
1195 }
1196 this._tree.setTo(s0.data, this.rectFromSegment(s0.data));
1197 lastConvexCornerOrientation = nextConvexCornerOrientation;
1198 sNext.data.p1Concave = true;
1199 this.updateMinMaxSegments(s0.data);
1200 }
1201 else if (!this.pointIsFitCorner(sNext.data.x1, sNext.data.y1, fit.bounds)) {
1202 // add concave segment
1203 const newSegment = new Segment(cornerX, cornerY, sNext.data.x1, sNext.data.y1, false);
1204 if (this.pointIsOnFitPerimeter(sNext.data.x1, sNext.data.y1, fit.bounds)) {
1205 sNext.data.p1Concave = true;
1206 }
1207 else {
1208 newSegment.p1Concave = true;
1209 }
1210 if (this.approxEqual(sNext.prev.data.x1, cornerX) && this.approxEqual(sNext.prev.data.y1, cornerY) && newSegment.isHorizontal === sNext.prev.data.isHorizontal) {
1211 sNext.prev.data.x2 = sNext.data.x1;
1212 sNext.prev.data.y2 = sNext.data.y1;
1213 this._tree.setTo(sNext.prev.data, this.rectFromSegment(sNext.prev.data));
1214 lastConvexCornerOrientation = nextConvexCornerOrientation;
1215 }
1216 else {
1217 segments.insertAfter(newSegment, sNext.prev);
1218 this._tree.add(newSegment, this.rectFromSegment(newSegment));
1219 lastConvexCornerOrientation = testOrientation;
1220 this.updateMinMaxSegments(newSegment);
1221 }
1222 }
1223 else { // if (this.pointIsOnFitPerimeter(sNext.data.x1, sNext.data.y1, fit.bounds))
1224 // shorten existing segment
1225 [sNext.data.x1, sNext.data.y1] = this.cornerFromRect((testOrientation + 3) % 4, fit.bounds);
1226 sNext.data.p1Concave = true;
1227 this._tree.setTo(sNext.data, this.rectFromSegment(sNext.data));
1228 lastConvexCornerOrientation = (testOrientation + 3) % 4;
1229 }
1230 while (nextConvexCornerOrientation !== lastConvexCornerOrientation) {
1231 [cornerX, cornerY] = this.cornerFromRect((nextConvexCornerOrientation + 3) % 4, fit.bounds);
1232 const newSegment = new Segment(s0.data.x2, s0.data.y2, cornerX, cornerY, false);
1233 s0 = segments.insertAfter(newSegment, s0);
1234 this._tree.add(newSegment, this.rectFromSegment(newSegment));
1235 nextConvexCornerOrientation = (nextConvexCornerOrientation + 3) % 4;
1236 this.updateMinMaxSegments(newSegment);
1237 }
1238 this.fixMissingMinMaxSegments();
1239 }
1240 /**
1241 * @hidden @internal
1242 * Finds the new minimum and maximum segments in the packed shape if
1243 * any of them have been deleted. To do this quickly, the quadtree
1244 * is used.
1245 * @this{PackedLayout}
1246 * @param force whether or not to force an update based on the quadtree even if none of the segments were deleted
1247 */
1248 fixMissingMinMaxSegments(force = false) {
1249 if (!this._minXSegment || !this._maxXSegment || !this._minYSegment || !this._maxYSegment || force) {
1250 [this._minXSegment, this._maxXSegment, this._minYSegment, this._maxYSegment] = this._tree.findExtremeObjects();
1251 }
1252 }
1253 /**
1254 * @hidden @internal
1255 * Updates the minimum or maximum segments with a new segment if that
1256 * segment is a new minimum or maximum.
1257 * @this {PackedLayout}
1258 * @param s the new segment to test
1259 */
1260 updateMinMaxSegments(s) {
1261 const centerX = (s.x1 + s.x2) / 2;
1262 const centerY = (s.y1 + s.y2) / 2;
1263 if (this._minXSegment && centerX < (this._minXSegment.x1 + this._minXSegment.x2) / 2) {
1264 this._minXSegment = s;
1265 }
1266 if (this._minYSegment && centerY < (this._minYSegment.y1 + this._minYSegment.y2) / 2) {
1267 this._minYSegment = s;
1268 }
1269 if (this._maxXSegment && centerX > (this._maxXSegment.x1 + this._maxXSegment.x2) / 2) {
1270 this._maxXSegment = s;
1271 }
1272 if (this._maxYSegment && centerY > (this._maxYSegment.y1 + this._maxYSegment.y2) / 2) {
1273 this._maxYSegment = s;
1274 }
1275 }
1276 /**
1277 * @hidden @internal
1278 * Gets the x and y coordinates of a corner of a given rectangle.
1279 * @this {PackedLayout}
1280 * @param orientation the orientation of the corner to get
1281 * @param bounds the bounds of the rectangle to get the corner from
1282 */
1283 cornerFromRect(orientation, bounds) {
1284 let x = bounds.x;
1285 let y = bounds.y;
1286 if (orientation === Orientation.NE || orientation === Orientation.SE) {
1287 x = bounds.right;
1288 }
1289 if (orientation === Orientation.SW || orientation === Orientation.SE) {
1290 y = bounds.bottom;
1291 }
1292 return [x, y];
1293 }
1294 /**
1295 * @hidden @internal
1296 * Gets a rectangle representing the bounds of a given segment.
1297 * Used to supply bounds of segments to the quadtree.
1298 * @this {PackedLayout}
1299 * @param segment the segment to get a rectangle for
1300 */
1301 rectFromSegment(segment) {
1302 if (this.approxEqual(segment.x1, segment.x2)) {
1303 return new go.Rect(segment.x1, Math.min(segment.y1, segment.y2), 0, Math.abs(segment.y1 - segment.y2));
1304 }
1305 return new go.Rect(Math.min(segment.x1, segment.x2), segment.y1, Math.abs(segment.x1 - segment.x2), 0);
1306 }
1307 /**
1308 * @hidden @internal
1309 * Tests if a number is in between two other numbers, with included
1310 * allowance for some floating point error with the supplied values.
1311 * The order of the given boundaries does not matter.
1312 * @this {PackedLayout}
1313 * @param n the number to test
1314 * @param b1 the first boundary
1315 * @param b2 the second boundary
1316 */
1317 numberIsBetween(n, b1, b2) {
1318 const tmp = b1;
1319 b1 = Math.min(b1, b2);
1320 b2 = Math.max(tmp, b2);
1321 return n + 1e-7 >= b1 && n - 1e-7 <= b2;
1322 }
1323 /**
1324 * @hidden @internal
1325 * Tests whether or not a given segment is a minimum or maximum segment.
1326 * @this {PackedLayout}
1327 * @param s the segment to test
1328 */
1329 segmentIsMinOrMax(s) {
1330 return s === this._minXSegment || s === this._minYSegment || s === this._maxXSegment || s === this._maxYSegment;
1331 }
1332 /**
1333 * @hidden @internal
1334 * Removes a segment from the layout state. This includes removing it
1335 * from the quadtree, as well as setting the corresponding minimum or
1336 * maximum segment to null if the given segment is a minimum or
1337 * maximum.
1338 * @this {PackedLayout}
1339 * @param s the segment to remove
1340 */
1341 removeSegmentFromLayoutState(s) {
1342 if (s.data === this._minXSegment) {
1343 this._minXSegment = null;
1344 }
1345 if (s.data === this._maxXSegment) {
1346 this._maxXSegment = null;
1347 }
1348 if (s.data === this._minYSegment) {
1349 this._minYSegment = null;
1350 }
1351 if (s.data === this._maxYSegment) {
1352 this._maxYSegment = null;
1353 }
1354 this._tree.remove(s.data);
1355 }
1356 /**
1357 * @hidden @internal
1358 * Removes all segments between the two given segments (exclusive).
1359 * This includes removing them from the layout state, as well as
1360 * the given segment list.
1361 * @this {PackedLayout}
1362 * @param segments the full list of segments
1363 * @param s1 the first segment
1364 * @param s2 the second segment
1365 */
1366 removeBetween(segments, s1, s2) {
1367 if (s1 === s2)
1368 return;
1369 let last = s1.next;
1370 let count = 0;
1371 while (last !== s2) {
1372 if (last === segments.start) {
1373 segments.start = s2;
1374 }
1375 this.removeSegmentFromLayoutState(last);
1376 count++;
1377 last = last.next;
1378 }
1379 s1.next = s2;
1380 s2.prev = s1;
1381 segments.length -= count;
1382 }
1383 /**
1384 * @hidden @internal
1385 * Calculates the cost of a given fit placement, depending on the
1386 * {@link #packShape} and {@link #_eAspectRatio}.
1387 * @this {PackedLayout}
1388 * @param fit the fit to calculate the cost of
1389 */
1390 placementCost(fit) {
1391 if (this.packShape === PackedLayout.Rectangular) {
1392 if (this._bounds.containsRect(fit)) {
1393 return 0;
1394 }
1395 return Math.max(Math.abs(this._center.x - fit.center.x), Math.abs(this._center.y - fit.center.y) * this._eAspectRatio);
1396 }
1397 else { // if (this.packShape === PackedLayout.Elliptical)
1398 return Math.pow((fit.center.x - this._center.x) / this._eAspectRatio, 2) + Math.pow(fit.center.y - this._center.y, 2);
1399 }
1400 }
1401 /**
1402 * @hidden @internal
1403 * Uses the quadtree to determine if the given fit has any
1404 * intersections anywhere along the perimeter.
1405 * @this {PackedLayout}
1406 * @param fit the fit to check
1407 */
1408 fitHasIntersections(fit) {
1409 return this._tree.intersecting(fit.bounds).length > 0;
1410 }
1411 /**
1412 * @hidden @internal
1413 * Checks if a few nearby segments intersect with the given fit,
1414 * producing faster interesection detection than a complete check
1415 * with the quadtree in many cases. However, since it doesn't check
1416 * the entire perimeter, this function is susceptible to false
1417 * negatives and should only be used with a more comprehensive check.
1418 * @this {PackedLayout}
1419 * @param fit the fit to check
1420 */
1421 fastFitHasIntersections(fit) {
1422 let sNext = fit.s1.next;
1423 let sPrev = fit.s1.prev;
1424 for (let i = 0; i < 2; i++) {
1425 if (this.segmentIntersectsRect(sNext.data, fit.bounds)) {
1426 return true;
1427 }
1428 if (this.segmentIntersectsRect(sPrev.data, fit.bounds)) {
1429 return true;
1430 }
1431 sNext = sNext.next;
1432 sPrev = sPrev.prev;
1433 }
1434 return false;
1435 }
1436 /**
1437 * @hidden @internal
1438 * Checks whether or not a segment intersects with a given rect.
1439 * Used for {@link #fastFitHasIntersections}.
1440 * @this {PackedLayout}
1441 * @param s the segment to test
1442 * @param r the rectangle to test
1443 */
1444 segmentIntersectsRect(s, r) {
1445 const left = Math.min(s.x1, s.x2);
1446 const right = Math.max(s.x1, s.x2);
1447 const top = Math.min(s.y1, s.y2);
1448 const bottom = Math.min(s.y1, s.y2);
1449 return !(left + 1e-7 >= r.right || right - 1e-7 <= r.left || top + 1e-7 >= r.bottom || bottom - 1e-7 <= r.top);
1450 }
1451 /**
1452 * @hidden @internal
1453 * Checks if two numbers are approximately equal, used for
1454 * eliminating mistakes caused by floating point error.
1455 * @this {PackedLayout}
1456 * @param x the first number
1457 * @param y the second number
1458 */
1459 approxEqual(x, y) {
1460 return Math.abs(x - y) < 1e-7;
1461 }
1462 /**
1463 * @hidden @internal
1464 * Checks if a value is a number, used for parameter validation
1465 * @this {PackedLayout}
1466 * @param value the value to check
1467 */
1468 isNumeric(value) {
1469 return !isNaN(Number(value.toString()));
1470 }
1471 /**
1472 * @hidden @internal
1473 * Copies properties to a cloned Layout.
1474 * @this {PackedLayout}
1475 * @param {?} copy
1476 */
1477 cloneProtected(copy) {
1478 copy._packShape = this._packShape;
1479 copy._packMode = this._packMode;
1480 copy._sortMode = this._sortMode;
1481 copy._sortOrder = this._sortOrder;
1482 copy._comparer = this._comparer;
1483 copy._aspectRatio = this._aspectRatio;
1484 copy._size = this._size;
1485 copy._spacing = this._spacing;
1486 copy._hasCircularNodes = this._hasCircularNodes;
1487 copy._arrangesToOrigin = this._arrangesToOrigin;
1488 }
1489}
1490/********************** Configuration constants **********************/
1491// These values determine the shape of the final layout
1492/**
1493 * This value for {@link #packShape} causes nodes to be packed
1494 * into an ellipse.
1495 *
1496 * The aspect ratio of this ellipse is determined by either
1497 * {@link #aspectRatio} or {@link #size}.
1498 * @constant
1499 */
1500PackedLayout.Elliptical = 0;
1501/**
1502 * Causes nodes to be packed into a rectangle; this value is used for
1503 * {@link #packShape}.
1504 *
1505 * The aspect ratio of this rectangle is determined by either
1506 * {@link #aspectRatio} or {@link #size}.
1507 * @constant
1508 */
1509PackedLayout.Rectangular = 1;
1510/**
1511 * Causes nodes to be packed into a spiral shape; this value is used
1512 * for {@link #packShape}.
1513 *
1514 * The {@link #aspectRatio} property is ignored in this mode, the
1515 * {@link #size} is expected to be square, and {@link #hasCircularNodes}
1516 * will be assumed 'true'. Please see {@link #packShape} for more details.
1517 */
1518PackedLayout.Spiral = 2;
1519// These values determine the size of the layout
1520/**
1521 * Nodes will be packed using the {@link #aspectRatio} property, with
1522 * no size considerations; this value is used for {@link #packMode}.
1523 *
1524 * The {@link #spacing} property will be respected in this mode.
1525 * @constant
1526 */
1527PackedLayout.AspectOnly = 10;
1528/**
1529 * Nodes will be compressed if necessary (using negative spacing) to fit the given
1530 * {@link #size}. However, if the {@link #size} is bigger
1531 * than the packed shape (with 0 spacing), it will not expand to fit it. This value
1532 * is used for {@link #packMode}.
1533 *
1534 * The {@link #spacing} property will be respected in this mode, but only
1535 * if it does not cause the layout to grow larger than the {@link #size}.
1536 * @constant
1537 */
1538PackedLayout.Fit = 11;
1539/**
1540 * Nodes will be either compressed or spaced evenly to fit the given
1541 * {@link #size}; this value is used for {@link #packMode}.
1542 *
1543 * The {@link #spacing} property will not be respected in this mode, and
1544 * will not do anything if set.
1545 * @constant
1546 */
1547PackedLayout.ExpandToFit = 12;
1548// These values specify an optional method by which to sort nodes before packing
1549/**
1550 * Nodes will not be sorted before packing; this value is used for {@link #sortMode}.
1551 * @constant
1552 */
1553PackedLayout.None = 20;
1554/**
1555 * Nodes will be sorted by their maximum side length before packing; this value is
1556 * used for {@link #sortMode}.
1557 * @constant
1558 */
1559PackedLayout.MaxSide = 21;
1560/**
1561 * Nodes will be sorted by their area; this value is used for {@link #sortMode}.
1562 * @constant
1563 */
1564PackedLayout.Area = 22;
1565// These values specify the order that nodes will be sorted, if applicable
1566/**
1567 * Nodes will be sorted in descending order; this value is used for {@link #sortOrder}.
1568 *
1569 * Does nothing if {@link #sortMode} is set to {@link PackedLayout.None}.
1570 * @constant
1571 */
1572PackedLayout.Descending = 30;
1573/**
1574 * Nodes will be sorted in ascending order; this value is used for {@link #sortOrder}.
1575 *
1576 * Does nothing if {@link #sortMode} is set to {@link PackedLayout.None}.
1577 * @constant
1578 */
1579PackedLayout.Ascending = 31;
1580/**
1581 * @hidden @internal
1582 * Class for a node in a {{@link CircularDoublyLinkedList}.
1583 * Stores a pointer to the previous and next node.
1584 */
1585class ListNode {
1586 constructor(data, prev, next) {
1587 this.data = data;
1588 this.prev = prev !== undefined ? prev : this;
1589 this.next = next !== undefined ? next : this;
1590 }
1591}
1592/**
1593 * @hidden @internal
1594 * A Circular doubly linked list, used by {@link PackedLayout} to
1595 * efficiently store {@link Segment}s with fast insertion and
1596 * deletion time.
1597 */
1598class CircularDoublyLinkedList {
1599 /**
1600 * Constructs a new list with an optional list of values
1601 * @param vals values to create the list with
1602 */
1603 constructor(...vals) {
1604 /**
1605 * The start of the list, null when the list is empty.
1606 */
1607 this.start = null;
1608 /**
1609 * The length of the list.
1610 */
1611 this.length = 0;
1612 if (vals.length > 0) {
1613 this.push(...vals);
1614 }
1615 }
1616 /**
1617 * Inserts the given value directly after the given node
1618 * @this {CircularDoublyLinkedList}
1619 * @param val the value to insert
1620 * @param node the node to insert after
1621 * @return {ListNode<T>} the new node
1622 */
1623 insertAfter(val, node) {
1624 if (node === null) {
1625 const newnode = new ListNode(val);
1626 newnode.prev = newnode;
1627 newnode.next = newnode;
1628 this.length = 1;
1629 return this.start = newnode;
1630 }
1631 const tmp = node.next;
1632 node.next = new ListNode(val, node, tmp);
1633 tmp.prev = node.next;
1634 this.length++;
1635 return node.next;
1636 }
1637 /**
1638 * Inserts the given value or values at the end of the list
1639 * @this {CircularDoublyLinkedList}
1640 * @param vals the value(s) to insert
1641 * @return {ListNode<T>} the node for the last value inserted (a list of values is inserted sequentially)
1642 */
1643 push(...vals) {
1644 if (vals.length === 0) {
1645 throw new Error('You must push at least one element!');
1646 }
1647 const sp = this.start !== null ? this.start.prev : null;
1648 let last = this.insertAfter(vals[0], sp);
1649 for (let i = 1; i < vals.length; i++) {
1650 last = this.insertAfter(vals[i], last);
1651 }
1652 return last;
1653 }
1654 /**
1655 * Removes the given node from the list
1656 * @this {CircularDoublyLinkedList}
1657 * @param node the node to remove
1658 */
1659 remove(node) {
1660 this.length--;
1661 if (this.length) {
1662 node.prev.next = node.next;
1663 node.next.prev = node.prev;
1664 if (node === this.start) {
1665 this.start = node.next;
1666 }
1667 }
1668 else {
1669 this.start = null;
1670 }
1671 }
1672 /**
1673 * Removes all nodes between the given start and end point (exclusive).
1674 * Returns the given end node.
1675 * @this {CircularDoublyLinkedList}
1676 * @param start node to start removing after
1677 * @param end node to stop removing at
1678 * @return {ListNode<T>} the end node
1679 */
1680 removeBetween(start, end) {
1681 if (start !== end) {
1682 let last = start.next;
1683 let count = 0;
1684 while (last !== end) {
1685 if (last === this.start) {
1686 this.start = end;
1687 }
1688 count++;
1689 last = last.next;
1690 }
1691 start.next = end;
1692 end.prev = start;
1693 this.length -= count;
1694 return end;
1695 }
1696 return start;
1697 }
1698}
1699/**
1700 * The following is a BSD-licensed implementation of the
1701 * Matousek-Sharir-Welzl algorithm for finding the smallest
1702 * enclosing circle around a given set of circles. The
1703 * original algorithm was adapted to support enclosing points
1704 * by assuming that the radius of a point is 0.
1705 */
1706/**
1707 * Copyright 2010-2016 Mike Bostock
1708 * All rights reserved.
1709 *
1710 * Redistribution and use in source and binary forms, with or without modification,
1711 * are permitted provided that the following conditions are met:
1712 *
1713 * * Redistributions of source code must retain the above copyright notice, this
1714 * list of conditions and the following disclaimer.
1715 *
1716 * * Redistributions in binary form must reproduce the above copyright notice,
1717 * this list of conditions and the following disclaimer in the documentation
1718 * and/or other materials provided with the distribution.
1719 *
1720 * * Neither the name of the author nor the names of contributors may be used to
1721 * endorse or promote products derived from this software without specific prior
1722 * written permission.
1723 *
1724 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
1725 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
1726 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
1727 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
1728 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
1729 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
1730 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
1731 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
1732 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
1733 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1734 */
1735/**
1736 * @hidden @internal
1737 * Represents a circle for the purposes of the smallest-enclosing
1738 * circle algorithm. The x and y values represent the center of
1739 * the circle.
1740 */
1741class Circle extends go.Point {
1742 constructor(x, y, r) {
1743 super(x, y);
1744 this.r = r;
1745 }
1746}
1747/**
1748 * @hidden @internal
1749 * @param circles array of circles of points to find the enclosing circle for
1750 */
1751function enclose(circles) {
1752 let i = 0;
1753 const n = (circles = shuffle(circles.slice())).length;
1754 let B = new Array();
1755 let p;
1756 let e = null;
1757 while (i < n) {
1758 p = circles[i];
1759 if (e !== null && enclosesWeak(e, p))
1760 ++i;
1761 else
1762 e = encloseBasis(B = extendBasis(B, p)), i = 0;
1763 }
1764 if (e !== null) {
1765 return circleToRect(e);
1766 }
1767 else { // this will never happen, but needs to be here for strict TypeScript compilation
1768 throw new Error('Assertion error');
1769 }
1770}
1771/**
1772 * @hidden @internal
1773 * Converts a Circle to a go.Rect object
1774 * @param c the Circle to convert
1775 */
1776function circleToRect(c) {
1777 return new go.Rect(c.x - c.r, c.y - c.r, c.r * 2, c.r * 2);
1778}
1779/**
1780 * @hidden @internal
1781 */
1782function extendBasis(B, p) {
1783 if (enclosesWeakAll(p, B))
1784 return [p];
1785 // If we get here then B must have at least one element.
1786 for (let i = 0; i < B.length; ++i) {
1787 if (enclosesNot(p, B[i])
1788 && enclosesWeakAll(encloseBasis2(B[i], p), B)) {
1789 return [B[i], p];
1790 }
1791 }
1792 // If we get here then B must have at least two elements.
1793 for (let i = 0; i < B.length - 1; ++i) {
1794 for (let j = i + 1; j < B.length; ++j) {
1795 if (enclosesNot(encloseBasis2(B[i], B[j]), p)
1796 && enclosesNot(encloseBasis2(B[i], p), B[j])
1797 && enclosesNot(encloseBasis2(B[j], p), B[i])
1798 && enclosesWeakAll(encloseBasis3(B[i], B[j], p), B)) {
1799 return [B[i], B[j], p];
1800 }
1801 }
1802 }
1803 // If we get here then something is very wrong.
1804 throw new Error('Assertion error');
1805}
1806/**
1807 * @hidden @internal
1808 */
1809function enclosesNot(a, b) {
1810 const ar = a instanceof Circle ? a.r : 0;
1811 const br = b instanceof Circle ? b.r : 0;
1812 const dr = ar - br;
1813 const dx = b.x - a.x;
1814 const dy = b.y - a.y;
1815 return dr < 0 || dr * dr < dx * dx + dy * dy;
1816}
1817/**
1818 * @hidden @internal
1819 */
1820function enclosesWeak(a, b) {
1821 const ar = a instanceof Circle ? a.r : 0;
1822 const br = b instanceof Circle ? b.r : 0;
1823 const dr = ar - br + 1e-6;
1824 const dx = b.x - a.x;
1825 const dy = b.y - a.y;
1826 return dr > 0 && dr * dr > dx * dx + dy * dy;
1827}
1828/**
1829 * @hidden @internal
1830 */
1831function enclosesWeakAll(a, B) {
1832 for (let i = 0; i < B.length; ++i) {
1833 if (!enclosesWeak(a, B[i])) {
1834 return false;
1835 }
1836 }
1837 return true;
1838}
1839/**
1840 * @hidden @internal
1841 */
1842function encloseBasis(B) {
1843 switch (B.length) {
1844 case 2: return encloseBasis2(B[0], B[1]);
1845 case 3: return encloseBasis3(B[0], B[1], B[2]);
1846 default: return encloseBasis1(B[0]); // case 1
1847 }
1848}
1849/**
1850 * @hidden @internal
1851 */
1852function encloseBasis1(a) {
1853 const ar = a instanceof Circle ? a.r : 0;
1854 return new Circle(a.x, a.y, ar);
1855}
1856/**
1857 * @hidden @internal
1858 */
1859function encloseBasis2(a, b) {
1860 const ar = a instanceof Circle ? a.r : 0;
1861 const br = b instanceof Circle ? b.r : 0;
1862 const x1 = a.x;
1863 const y1 = a.y;
1864 const r1 = ar;
1865 const x2 = b.x;
1866 const y2 = b.y;
1867 const r2 = br;
1868 const x21 = x2 - x1;
1869 const y21 = y2 - y1;
1870 const r21 = r2 - r1;
1871 const l = Math.sqrt(x21 * x21 + y21 * y21);
1872 return new Circle((x1 + x2 + x21 / l * r21) / 2, (y1 + y2 + y21 / l * r21) / 2, (l + r1 + r2) / 2);
1873}
1874/**
1875 * @hidden @internal
1876 */
1877function encloseBasis3(a, b, c) {
1878 const ar = a instanceof Circle ? a.r : 0;
1879 const br = b instanceof Circle ? b.r : 0;
1880 const cr = c instanceof Circle ? c.r : 0;
1881 const x1 = a.x;
1882 const y1 = a.y;
1883 const r1 = ar;
1884 const x2 = b.x;
1885 const y2 = b.y;
1886 const r2 = br;
1887 const x3 = c.x;
1888 const y3 = c.y;
1889 const r3 = cr;
1890 const a2 = x1 - x2;
1891 const a3 = x1 - x3;
1892 const b2 = y1 - y2;
1893 const b3 = y1 - y3;
1894 const c2 = r2 - r1;
1895 const c3 = r3 - r1;
1896 const d1 = x1 * x1 + y1 * y1 - r1 * r1;
1897 const d2 = d1 - x2 * x2 - y2 * y2 + r2 * r2;
1898 const d3 = d1 - x3 * x3 - y3 * y3 + r3 * r3;
1899 const ab = a3 * b2 - a2 * b3;
1900 const xa = (b2 * d3 - b3 * d2) / (ab * 2) - x1;
1901 const xb = (b3 * c2 - b2 * c3) / ab;
1902 const ya = (a3 * d2 - a2 * d3) / (ab * 2) - y1;
1903 const yb = (a2 * c3 - a3 * c2) / ab;
1904 const A = xb * xb + yb * yb - 1;
1905 const B = 2 * (r1 + xa * xb + ya * yb);
1906 const C = xa * xa + ya * ya - r1 * r1;
1907 const r = -(A ? (B + Math.sqrt(B * B - 4 * A * C)) / (2 * A) : C / B);
1908 return new Circle(x1 + xa + xb * r, y1 + ya + yb * r, r);
1909}
1910/**
1911 * @hidden @internal
1912 * Shuffles array in place.
1913 * @param {Array} a items An array containing the items.
1914 */
1915function shuffle(a) {
1916 let j;
1917 let x;
1918 let i;
1919 for (i = a.length - 1; i > 0; i--) {
1920 j = Math.floor(Math.random() * (i + 1));
1921 x = a[i];
1922 a[i] = a[j];
1923 a[j] = x;
1924 }
1925 return a;
1926}