1 /*
  2     Copyright 2008-2025
  3         Matthias Ehmann,
  4         Michael Gerhaeuser,
  5         Carsten Miller,
  6         Bianca Valentin,
  7         Alfred Wassermann,
  8         Peter Wilfahrt
  9 
 10     This file is part of JSXGraph.
 11 
 12     JSXGraph is free software dual licensed under the GNU LGPL or MIT License.
 13 
 14     You can redistribute it and/or modify it under the terms of the
 15 
 16       * GNU Lesser General Public License as published by
 17         the Free Software Foundation, either version 3 of the License, or
 18         (at your option) any later version
 19       OR
 20       * MIT License: https://github.com/jsxgraph/jsxgraph/blob/master/LICENSE.MIT
 21 
 22     JSXGraph is distributed in the hope that it will be useful,
 23     but WITHOUT ANY WARRANTY; without even the implied warranty of
 24     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 25     GNU Lesser General Public License for more details.
 26 
 27     You should have received a copy of the GNU Lesser General Public License and
 28     the MIT License along with JSXGraph. If not, see <https://www.gnu.org/licenses/>
 29     and <https://opensource.org/licenses/MIT/>.
 30  */
 31 
 32 /*global JXG: true, define: true*/
 33 /*jslint nomen: true, plusplus: true*/
 34 
 35 /**
 36  * @fileoverview In this file the class Group is defined, a class for
 37  * managing grouping of points.
 38  */
 39 
 40 import JXG from "../jxg.js";
 41 import Const from "./constants.js";
 42 import Mat from "../math/math.js";
 43 import Geometry from "../math/geometry.js";
 44 import Type from "../utils/type.js";
 45 
 46 /**
 47  * Creates a new instance of Group.
 48  * @class In this class all group management is done.
 49  * @param {JXG.Board} board
 50  * @param {String} id Unique identifier for this object.  If null or an empty string is given,
 51  * an unique id will be generated by Board
 52  * @param {String} name Not necessarily unique name, displayed on the board.  If null or an
 53  * empty string is given, an unique name will be generated.
 54  * @param {Array} objects Array of points to add to this group.
 55  * @param {Object} attributes Defines the visual appearance of the group.
 56  * @constructor
 57  */
 58 JXG.Group = function (board, id, name, objects, attributes) {
 59     var number, objArray, i, obj;
 60 
 61     this.board = board;
 62     this.objects = {};
 63     number = this.board.numObjects;
 64     this.board.numObjects += 1;
 65 
 66     if (id === "" || !Type.exists(id)) {
 67         this.id = this.board.id + "Group" + number;
 68     } else {
 69         this.id = id;
 70     }
 71     this.board.groups[this.id] = this;
 72 
 73     this.type = Const.OBJECT_TYPE_POINT;
 74     this.elementClass = Const.OBJECT_CLASS_POINT;
 75 
 76     if (name === "" || !Type.exists(name)) {
 77         this.name = "group_" + this.board.generateName(this);
 78     } else {
 79         this.name = name;
 80     }
 81     delete this.type;
 82 
 83     /**
 84      * Cache coordinates of points. From this and the actual position
 85      * of the points, the translation is determined.
 86      * It has to be kept updated in this class "by hand"-
 87      *
 88      * @private
 89      * @type Object
 90      * @see JXG.Group#_updateCoordsCache
 91      */
 92     this.coords = {};
 93     this.needsRegularUpdate = attributes.needsregularupdate;
 94 
 95     this.rotationCenter = "centroid";
 96     this.scaleCenter = null;
 97     this.rotationPoints = [];
 98     this.translationPoints = [];
 99     this.scalePoints = [];
100     this.scaleDirections = {};
101 
102     this.parents = [];
103 
104     if (Type.isArray(objects)) {
105         objArray = objects;
106     } else {
107         objArray = Array.prototype.slice.call(arguments, 3);
108     }
109 
110     for (i = 0; i < objArray.length; i++) {
111         obj = this.board.select(objArray[i]);
112 
113         if (!obj.evalVisProp('fixed') && Type.exists(obj.coords)) {
114             this.addPoint(obj);
115         }
116     }
117 
118     this.methodMap = {
119         ungroup: "ungroup",
120         add: "addPoint",
121         addPoint: "addPoint",
122         addPoints: "addPoints",
123         addGroup: "addGroup",
124         remove: "removePoint",
125         removePoint: "removePoint",
126         setAttribute: "setAttribute",
127         setProperty: "setAttribute"
128     };
129 };
130 
131 JXG.extend(
132     JXG.Group.prototype,
133     /** @lends JXG.Group.prototype */ {
134         /**
135          * Releases all elements of this group.
136          * @returns {JXG.Group} returns this (empty) group
137          */
138         ungroup: function () {
139             var el, p, i;
140             for (el in this.objects) {
141                 if (this.objects.hasOwnProperty(el)) {
142                     p = this.objects[el].point;
143                     if (Type.isArray(p.groups)) {
144                         i = Type.indexOf(p.groups, this.id);
145                         if (i >= 0) {
146                             delete p.groups[i];
147                         }
148                     }
149                 }
150             }
151 
152             this.objects = {};
153             return this;
154         },
155 
156         /**
157          * Adds ids of elements to the array this.parents. This is a copy
158          * of {@link Element.addParents}.
159          * @param {Array} parents Array of elements or ids of elements.
160          * Alternatively, one can give a list of objects as parameters.
161          * @returns {JXG.Object} reference to the object itself.
162          **/
163         addParents: function (parents) {
164             var i, len, par;
165 
166             if (Type.isArray(parents)) {
167                 par = parents;
168             } else {
169                 par = arguments;
170             }
171 
172             len = par.length;
173             for (i = 0; i < len; ++i) {
174                 if (Type.isId(this.board, par[i])) {
175                     this.parents.push(par[i]);
176                 } else if (Type.exists(par[i].id)) {
177                     this.parents.push(par[i].id);
178                 }
179             }
180 
181             this.parents = Type.uniqueArray(this.parents);
182         },
183 
184         /**
185          * Sets ids of elements to the array this.parents. This is a copy
186          * of {@link Element.setParents}
187          * First, this.parents is cleared. See {@link Group#addParents}.
188          * @param {Array} parents Array of elements or ids of elements.
189          * Alternatively, one can give a list of objects as parameters.
190          * @returns {JXG.Object} reference to the object itself.
191          **/
192         setParents: function (parents) {
193             this.parents = [];
194             this.addParents(parents);
195             return this;
196         },
197 
198         /**
199          * List of the element ids resp. values used as parents in {@link JXG.Board#create}.
200          * @returns {Array}
201          */
202         getParents: function () {
203             return Type.isArray(this.parents) ? this.parents : [];
204         },
205 
206         /**
207          * Update the cached coordinates of a group element.
208          * @param  {String} el element id of the group element whose cached coordinates
209          * are going to be updated.
210          * @return null
211          */
212         _updateCoordsCache: function (el) {
213             var obj;
214             if (el !== "" && Type.exists(this.objects[el])) {
215                 obj = this.objects[el].point;
216                 this.coords[obj.id] = { usrCoords: obj.coords.usrCoords.slice(0) };
217             }
218         },
219 
220         /**
221          * Sends an update to all group members.
222          * This method is called from the points' coords object event listeners
223          * and not by the board.
224          * @returns {JXG.Group} returns this group
225          */
226         update: function () {
227             var i, drag, el,
228                 actionCenter,
229                 desc,
230                 s, sx, sy,
231                 alpha,
232                 t, T,
233                 center,
234                 obj = null;
235 
236             if (!this.needsUpdate) {
237                 return this;
238             }
239 
240             drag = this._update_find_drag_type();
241             if (drag.action === "nothing") {
242                 this._updateCoordsCache(drag.id);
243                 return this;
244             }
245 
246             obj = this.objects[drag.id].point;
247 
248             // Prepare translation, scaling or rotation.
249             // Scaling and rotation is handled by transformations for all elements.
250             // Translation is handled by direct coordinate manipulation for points.
251             // For images and texts, all translation, scaling and rotation is
252             // done by binding a transformation to the element.
253             if (drag.action === "translation") {
254                 t = [
255                     obj.coords.usrCoords[1] - this.coords[drag.id].usrCoords[1],
256                     obj.coords.usrCoords[2] - this.coords[drag.id].usrCoords[2]
257                 ];
258 
259                 if (obj.elementClass !== Const.OBJECT_CLASS_POINT) {
260                     // For images and texts we have to update the drag direction
261                     // by reapplying all transformations.
262 
263                     t.unshift(0);
264                     for (i = 0; i < obj.transformations.length; i++) {
265                         t = Mat.matVecMult(obj.transformations[i].matrix, t);
266                     }
267                     t.shift();
268                 }
269 
270                 // For images and texts
271                 T = this.board.create("transform", t, { type: "translate" });
272                 T.update();
273             } else if (drag.action === "rotation" || drag.action === "scaling") {
274                 if (drag.action === "rotation") {
275                     actionCenter = "rotationCenter";
276                 } else {
277                     actionCenter = "scaleCenter";
278                 }
279 
280                 // if (Type.isPoint(this.board, this[actionCenter])) {
281                 if (Type.exists(this[actionCenter].coords)) {
282                     center = this[actionCenter].coords.usrCoords.slice(1);
283                 } else if (this[actionCenter] === "centroid") {
284                     center = this._update_centroid_center();
285                 } else if (Type.isArray(this[actionCenter])) {
286                     center = this[actionCenter];
287                 } else if (Type.isFunction(this[actionCenter])) {
288                     center = this[actionCenter]();
289                 } else {
290                     // No valid center for this transformation, get out of here.
291                     JXG.debug('Group.update: No valid center for this transformation, get out of here.');
292                     return this;
293                 }
294 
295                 if (drag.action === "rotation") {
296                     alpha = Geometry.rad(
297                         this.coords[drag.id].usrCoords.slice(1),
298                         center,
299                         this.objects[drag.id].point
300                     );
301                     t = this.board.create("transform", [alpha, center[0], center[1]], {
302                         type: "rotate"
303                     });
304                     t.update(); // t.update initializes t.matrix, which is needed if the action element is the first group element.
305                 } else if (drag.action === "scaling") {
306                     s = Geometry.distance(this.coords[drag.id].usrCoords.slice(1), center);
307                     if (Math.abs(s) < Mat.eps) {
308                         return this;
309                     }
310                     s = Geometry.distance(obj.coords.usrCoords.slice(1), center) / s;
311                     sx = this.scaleDirections[drag.id].indexOf("x") >= 0 ? s : 1.0;
312                     sy = this.scaleDirections[drag.id].indexOf("y") >= 0 ? s : 1.0;
313 
314                     // Shift scale center to origin, scale and shift the scale center back.
315                     t = this.board.create(
316                         "transform",
317                         [1, 0, 0, center[0] * (1 - sx), sx, 0, center[1] * (1 - sy), 0, sy],
318                         { type: "generic" }
319                     );
320                     t.update(); // This initializes t.matrix, which is needed if the action element is the first group element.
321                 } else {
322                     // This should not be reached
323                     return this;
324                 }
325             }
326 
327             // Bind the transformation to any images and texts
328             for (el in this.objects) {
329                 obj = this.objects[el].point;
330                 if (obj.elementClass !== Const.OBJECT_CLASS_POINT) {
331                     if (Type.exists(t.board)) {
332                         // t itself is a transformation
333                         t.meltTo(obj);
334                     } else {
335                         // Drag element is a point, therefore
336                         // t is an array and we have to use the transformation T.
337                         if (drag.id !== obj.id) {
338                             T.meltTo(obj);
339                         }
340                     }
341                 }
342             }
343 
344             this._update_apply_transformation(drag, t);
345 
346             this.needsUpdate = false; // This is needed here to prevent infinite recursion because
347             // of the board.updateElements call below,
348 
349             // Prepare dependent objects for update
350             for (el in this.objects) {
351                 if (this.objects.hasOwnProperty(el)) {
352                     for (desc in this.objects[el].descendants) {
353                         if (this.objects[el].descendants.hasOwnProperty(desc)) {
354                             this.objects[el].descendants.needsUpdate =
355                                 this.objects[el].descendants.needsRegularUpdate ||
356                                 this.board.needsFullUpdate;
357                         }
358                     }
359                 }
360             }
361             this.board.updateElements(drag);
362 
363             // Now, all group elements have their new position and
364             // we can update the bookkeeping of the coordinates of the group elements.
365             for (el in this.objects) {
366                 if (this.objects.hasOwnProperty(el)) {
367                     this._updateCoordsCache(el);
368                 }
369             }
370 
371             return this;
372         },
373 
374         /**
375          * @private
376         */
377         //  Determine what the dragging of a group element should do:
378         //  rotation, translation, scaling or nothing.
379         _update_find_drag_type: function () {
380             var el,
381                 obj,
382                 action = "nothing",
383                 changed = [],
384                 dragObjId;
385 
386             // Determine how many elements have changed their position
387             // If more than one element changed its position, it is a translation.
388             // If exactly one element changed its position we have to find the type of the point.
389             for (el in this.objects) {
390                 if (this.objects.hasOwnProperty(el)) {
391                     obj = this.objects[el].point;
392 
393                     if (obj.coords.distance(Const.COORDS_BY_USER, this.coords[el]) > Mat.eps) {
394                         changed.push(obj.id);
395                     }
396                 }
397             }
398 
399             // Determine type of action: translation, scaling or rotation
400             if (changed.length === 0) {
401                 return {
402                     action: action,
403                     id: "",
404                     changed: changed
405                 };
406             }
407 
408             dragObjId = changed[0];
409             obj = this.objects[dragObjId].point;
410 
411             if (changed.length > 1) {
412                 // More than one point moved => translation
413                 action = "translation";
414             } else {
415                 // One point moved => we have to determine the type
416                 if (
417                     Type.isInArray(this.rotationPoints, obj) &&
418                     Type.exists(this.rotationCenter)
419                 ) {
420                     action = "rotation";
421                 } else if (
422                     Type.isInArray(this.scalePoints, obj) &&
423                     Type.exists(this.scaleCenter)
424                 ) {
425                     action = "scaling";
426                 } else if (Type.isInArray(this.translationPoints, obj)) {
427                     action = "translation";
428                 }
429             }
430 
431             return {
432                 action: action,
433                 id: dragObjId,
434                 changed: changed
435             };
436         },
437 
438         /**
439          * Determine the Euclidean (affine) coordinates of the centroid of the group.
440          * @private
441          * @returns {Array} array of length two,
442         */
443         _update_centroid_center: function () {
444             var center, len, el;
445 
446             center = [0, 0];
447             len = 0;
448             for (el in this.coords) {
449                 if (this.coords.hasOwnProperty(el)) {
450                     center[0] += this.coords[el].usrCoords[1];
451                     center[1] += this.coords[el].usrCoords[2];
452                     ++len;
453                 }
454             }
455             if (len > 0) {
456                 center[0] /= len;
457                 center[1] /= len;
458             }
459 
460             return center;
461         },
462 
463         /**
464          * @private
465         */
466         // Apply the transformation to all elements of the group
467         _update_apply_transformation: function (drag, t) {
468             var el, obj;
469 
470             for (el in this.objects) {
471                 if (this.objects.hasOwnProperty(el)) {
472                     if (Type.exists(this.board.objects[el])) {
473                         obj = this.objects[el].point;
474 
475                         // Here, it is important that we change the position
476                         // of elements by using setCoordinates.
477                         // Thus, we avoid the call of snapToGrid().
478                         // This is done in the subsequent call of board.updateElements()
479                         // in Group.update() above.
480                         if (obj.id !== drag.id) {
481                             if (drag.action === "translation") {
482                                 if (!Type.isInArray(drag.changed, obj.id)) {
483                                     if (obj.elementClass === Const.OBJECT_CLASS_POINT) {
484                                         obj.coords.setCoordinates(Const.COORDS_BY_USER, [
485                                             this.coords[el].usrCoords[1] + t[0],
486                                             this.coords[el].usrCoords[2] + t[1]
487                                         ]);
488                                     }
489                                 }
490                             } else if (
491                                 drag.action === "rotation" ||
492                                 drag.action === "scaling"
493                             ) {
494                                 if (obj.elementClass === Const.OBJECT_CLASS_POINT) {
495                                     t.applyOnce([obj]);
496                                 }
497                             }
498                         } else {
499                             if (drag.action === "rotation" || drag.action === "scaling") {
500                                 obj.coords.setCoordinates(
501                                     Const.COORDS_BY_USER,
502                                     Mat.matVecMult(t.matrix, this.coords[obj.id].usrCoords)
503                                 );
504                             }
505                         }
506                     } else {
507                         delete this.objects[el];
508                     }
509                 }
510             }
511         },
512 
513         /**
514          * Adds an Point to this group.
515          * @param {JXG.Point} object The point added to the group.
516          * @returns {JXG.Group} returns this group
517          */
518         addPoint: function (object) {
519             this.objects[object.id] = { point: this.board.select(object) };
520             this._updateCoordsCache(object.id);
521             this.translationPoints.push(object);
522 
523             object.groups.push(this.id);
524             object.groups = Type.uniqueArray(object.groups);
525 
526             return this;
527         },
528 
529         /**
530          * Adds multiple points to this group.
531          * @param {Array} objects An array of points to add to the group.
532          * @returns {JXG.Group} returns this group
533          */
534         addPoints: function (objects) {
535             var p;
536 
537             for (p = 0; p < objects.length; p++) {
538                 this.addPoint(objects[p]);
539             }
540 
541             return this;
542         },
543 
544         /**
545          * Adds all points in a group to this group.
546          * @param {JXG.Group} group The group added to this group.
547          * @returns {JXG.Group} returns this group
548          */
549         addGroup: function (group) {
550             var el;
551 
552             for (el in group.objects) {
553                 if (group.objects.hasOwnProperty(el)) {
554                     this.addPoint(group.objects[el].point);
555                 }
556             }
557 
558             return this;
559         },
560 
561         /**
562          * Removes a point from the group.
563          * @param {JXG.Point} point
564          * @returns {JXG.Group} returns this group
565          */
566         removePoint: function (point) {
567             delete this.objects[point.id];
568 
569             return this;
570         },
571 
572         /**
573          * Sets the center of rotation for the group. This is either a point or the centroid of the group.
574          * @param {JXG.Point|String|Array|Function} object A point which will be the center of rotation, the string "centroid", or
575          * an array of length two, or a function returning an array of length two.
576          * @default 'centroid'
577          * @returns {JXG.Group} returns this group
578          */
579         setRotationCenter: function (object) {
580             this.rotationCenter = object;
581 
582             return this;
583         },
584 
585         /**
586          * Sets the rotation points of the group. Dragging at one of these points results into a rotation of the whole group around
587          * the rotation center of the group {@see JXG.Group#setRotationCenter}.
588          * @param {Array|JXG.Point} objects Array of {@link JXG.Point} or arbitrary number of {@link JXG.Point} elements.
589          * @returns {JXG.Group} returns this group
590          */
591         setRotationPoints: function (objects) {
592             return this._setActionPoints("rotation", objects);
593         },
594 
595         /**
596          * Adds a point to the set of rotation points of the group. Dragging at one of these points results into a rotation of the whole group around
597          * the rotation center of the group {@see JXG.Group#setRotationCenter}.
598          * @param {JXG.Point} point {@link JXG.Point} element.
599          * @returns {JXG.Group} returns this group
600          */
601         addRotationPoint: function (point) {
602             return this._addActionPoint("rotation", point);
603         },
604 
605         /**
606          * Removes the rotation property from a point of the group.
607          * @param {JXG.Point} point {@link JXG.Point} element.
608          * @returns {JXG.Group} returns this group
609          */
610         removeRotationPoint: function (point) {
611             return this._removeActionPoint("rotation", point);
612         },
613 
614         /**
615          * Sets the translation points of the group. Dragging at one of these points results into a translation of the whole group.
616          * @param {Array|JXG.Point} objects Array of {@link JXG.Point} or arbitrary number of {@link JXG.Point} elements.
617          *
618          * By default, all points of the group are translation points.
619          * @returns {JXG.Group} returns this group
620          */
621         setTranslationPoints: function (objects) {
622             return this._setActionPoints("translation", objects);
623         },
624 
625         /**
626          * Adds a point to the set of the translation points of the group.
627          * Dragging one of these points results into a translation of the whole group.
628          * @param {JXG.Point} point {@link JXG.Point} element.
629          * @returns {JXG.Group} returns this group
630          */
631         addTranslationPoint: function (point) {
632             return this._addActionPoint("translation", point);
633         },
634 
635         /**
636          * Removes the translation property from a point of the group.
637          * @param {JXG.Point} point {@link JXG.Point} element.
638          * @returns {JXG.Group} returns this group
639          */
640         removeTranslationPoint: function (point) {
641             return this._removeActionPoint("translation", point);
642         },
643 
644         /**
645          * Sets the center of scaling for the group. This is either a point or the centroid of the group.
646          * @param {JXG.Point|String} object A point which will be the center of scaling, the string "centroid", or
647          * an array of length two, or a function returning an array of length two.
648          * @returns {JXG.Group} returns this group
649          */
650         setScaleCenter: function (object) {
651             this.scaleCenter = object;
652 
653             return this;
654         },
655 
656         /**
657          * Sets the scale points of the group. Dragging at one of these points results into a scaling of the whole group.
658          * @param {Array|JXG.Point} objects Array of {@link JXG.Point} or arbitrary number of {@link JXG.Point} elements.
659          * @param {String} direction Restricts the directions to be scaled. Possible values are 'x', 'y', 'xy'. Default value is 'xy'.
660          *
661          * By default, all points of the group are translation points.
662          * @returns {JXG.Group} returns this group
663          */
664         setScalePoints: function (objects, direction) {
665             var objs, i, len;
666             if (Type.isArray(objects)) {
667                 objs = objects;
668             } else {
669                 objs = arguments;
670             }
671 
672             len = objs.length;
673             for (i = 0; i < len; ++i) {
674                 this.scaleDirections[this.board.select(objs[i]).id] = direction || "xy";
675             }
676 
677             return this._setActionPoints("scale", objects);
678         },
679 
680         /**
681          * Adds a point to the set of the scale points of the group. Dragging at one of these points results into a scaling of the whole group.
682          * @param {JXG.Point} point {@link JXG.Point} element.
683          * @param {String} direction Restricts the directions to be scaled. Possible values are 'x', 'y', 'xy'. Default value is 'xy'.
684          * @returns {JXG.Group} returns this group
685          */
686         addScalePoint: function (point, direction) {
687             this._addActionPoint("scale", point);
688             this.scaleDirections[this.board.select(point).id] = direction || "xy";
689 
690             return this;
691         },
692 
693         /**
694          * Removes the scaling property from a point of the group.
695          * @param {JXG.Point} point {@link JXG.Point} element.
696          * @returns {JXG.Group} returns this group
697          */
698         removeScalePoint: function (point) {
699             return this._removeActionPoint("scale", point);
700         },
701 
702         /**
703          * Generic method for {@link JXG.Group@setTranslationPoints} and {@link JXG.Group@setRotationPoints}
704          * @private
705          */
706         _setActionPoints: function (action, objects) {
707             var objs, i, len;
708             if (Type.isArray(objects)) {
709                 objs = objects;
710             } else {
711                 objs = arguments;
712             }
713 
714             len = objs.length;
715             this[action + "Points"] = [];
716             for (i = 0; i < len; ++i) {
717                 this._addActionPoint(action, objs[i]);
718             }
719 
720             return this;
721         },
722 
723         /**
724          * Generic method for {@link JXG.Group@addTranslationPoint} and {@link JXG.Group@addRotationPoint}
725          * @private
726          */
727         _addActionPoint: function (action, point) {
728             this[action + "Points"].push(this.board.select(point));
729 
730             return this;
731         },
732 
733         /**
734          * Generic method for {@link JXG.Group@removeTranslationPoint} and {@link JXG.Group@removeRotationPoint}
735          * @private
736          */
737         _removeActionPoint: function (action, point) {
738             var idx = this[action + "Points"].indexOf(this.board.select(point));
739             if (idx > -1) {
740                 this[action + "Points"].splice(idx, 1);
741             }
742 
743             return this;
744         },
745 
746         /**
747          * @deprecated
748          * Use setAttribute
749          */
750         setProperty: function () {
751             JXG.deprecated("Group.setProperty", "Group.setAttribute()");
752             this.setAttribute.apply(this, arguments);
753         },
754 
755         setAttribute: function () {
756             var el;
757 
758             for (el in this.objects) {
759                 if (this.objects.hasOwnProperty(el)) {
760                     this.objects[el].point.setAttribute.apply(
761                         this.objects[el].point,
762                         arguments
763                     );
764                 }
765             }
766 
767             return this;
768         }
769     }
770 );
771 
772 /**
773  * @class A container element to control the movement of given set of point, image or text elements simultaneously.
774  * The elements of the group and dependent elements can be translated, rotated and scaled by
775  * dragging one of the group elements.
776  *
777  * @pseudo
778  * @name Group
779  * @augments JXG.Group
780  * @constructor
781  * @type JXG.Group
782  * @param {JXG.Board} board The board the points are on.
783  * @param {Array} parents Array of points to group.
784  * @param {Object} attributes Visual properties (unused).
785  * @returns {JXG.Group}
786  *
787  * @example
788  *
789  *  // Create some free points. e.g. A, B, C, D
790  *  // Create a group
791  *
792  *  var p, col, g;
793  *  col = 'blue';
794  *  p = [];
795  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
796  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
797  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:col, fillColor:col}));
798  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
799  *  g = board.create('group', p);
800  *
801  * </pre><div class="jxgbox" id="JXGa2204533-db91-4af9-b720-70394de4d367" style="width: 400px; height: 300px;"></div>
802  * <script type="text/javascript">
803  *  (function () {
804  *  var board, p, col, g;
805  *  board = JXG.JSXGraph.initBoard('JXGa2204533-db91-4af9-b720-70394de4d367', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
806  *  col = 'blue';
807  *  p = [];
808  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
809  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
810  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:col, fillColor:col}));
811  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
812  *  g = board.create('group', p);
813  *  })();
814  * </script><pre>
815  *
816  *
817  * @example
818  *
819  *  // Create some free points. e.g. A, B, C, D
820  *  // Create a group
821  *  // If the points define a polygon and the polygon has the attribute hasInnerPoints:true,
822  *  // the polygon can be dragged around.
823  *
824  *  var p, col, pol, g;
825  *  col = 'blue';
826  *  p = [];
827  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
828  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
829  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:col, fillColor:col}));
830  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
831  *
832  *  pol = board.create('polygon', p, {hasInnerPoints: true});
833  *  g = board.create('group', p);
834  *
835  * </pre><div class="jxgbox" id="JXG781b5564-a671-4327-81c6-de915c8f924e" style="width: 400px; height: 300px;"></div>
836  * <script type="text/javascript">
837  *  (function () {
838  *  var board, p, col, pol, g;
839  *  board = JXG.JSXGraph.initBoard('JXG781b5564-a671-4327-81c6-de915c8f924e', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
840  *  col = 'blue';
841  *  p = [];
842  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
843  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
844  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:col, fillColor:col}));
845  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
846  *  pol = board.create('polygon', p, {hasInnerPoints: true});
847  *  g = board.create('group', p);
848  *  })();
849  * </script><pre>
850  *
851  *  @example
852  *
853  *  // Allow rotations:
854  *  // Define a center of rotation and declare points of the group as "rotation points".
855  *
856  *  var p, col, pol, g;
857  *  col = 'blue';
858  *  p = [];
859  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
860  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
861  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
862  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
863  *
864  *  pol = board.create('polygon', p, {hasInnerPoints: true});
865  *  g = board.create('group', p);
866  *  g.setRotationCenter(p[0]);
867  *  g.setRotationPoints([p[1], p[2]]);
868  *
869  * </pre><div class="jxgbox" id="JXGf0491b62-b377-42cb-b55c-4ef5374b39fc" style="width: 400px; height: 300px;"></div>
870  * <script type="text/javascript">
871  *  (function () {
872  *  var board, p, col, pol, g;
873  *  board = JXG.JSXGraph.initBoard('JXGf0491b62-b377-42cb-b55c-4ef5374b39fc', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
874  *  col = 'blue';
875  *  p = [];
876  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
877  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
878  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
879  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
880  *  pol = board.create('polygon', p, {hasInnerPoints: true});
881  *  g = board.create('group', p);
882  *  g.setRotationCenter(p[0]);
883  *  g.setRotationPoints([p[1], p[2]]);
884  *  })();
885  * </script><pre>
886  *
887  *  @example
888  *
889  *  // Allow rotations:
890  *  // As rotation center, arbitrary points, coordinate arrays,
891  *  // or functions returning coordinate arrays can be given.
892  *  // Another possibility is to use the predefined string 'centroid'.
893  *
894  *  // The methods to define the rotation points can be chained.
895  *
896  *  var p, col, pol, g;
897  *  col = 'blue';
898  *  p = [];
899  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
900  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
901  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
902  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
903  *
904  *  pol = board.create('polygon', p, {hasInnerPoints: true});
905  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[1], p[2]]);
906  *
907  * </pre><div class="jxgbox" id="JXG8785b099-a75e-4769-bfd8-47dd4376fe27" style="width: 400px; height: 300px;"></div>
908  * <script type="text/javascript">
909  *  (function () {
910  *  var board, p, col, pol, g;
911  *  board = JXG.JSXGraph.initBoard('JXG8785b099-a75e-4769-bfd8-47dd4376fe27', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
912  *  col = 'blue';
913  *  p = [];
914  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
915  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
916  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
917  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
918  *  pol = board.create('polygon', p, {hasInnerPoints: true});
919  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[1], p[2]]);
920  *  })();
921  * </script><pre>
922  *
923  *  @example
924  *
925  *  // Allow scaling:
926  *  // As for rotation one can declare points of the group to trigger a scaling operation.
927  *  // For this, one has to define a scaleCenter, in analogy to rotations.
928  *
929  *  // Here, the yellow  point enables scaling, the red point a rotation.
930  *
931  *  var p, col, pol, g;
932  *  col = 'blue';
933  *  p = [];
934  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
935  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
936  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
937  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
938  *
939  *  pol = board.create('polygon', p, {hasInnerPoints: true});
940  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[2]]);
941  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
942  *
943  * </pre><div class="jxgbox" id="JXGc3ca436b-e4fc-4de5-bab4-09790140c675" style="width: 400px; height: 300px;"></div>
944  * <script type="text/javascript">
945  *  (function () {
946  *  var board, p, col, pol, g;
947  *  board = JXG.JSXGraph.initBoard('JXGc3ca436b-e4fc-4de5-bab4-09790140c675', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
948  *  col = 'blue';
949  *  p = [];
950  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
951  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
952  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
953  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
954  *  pol = board.create('polygon', p, {hasInnerPoints: true});
955  *  g = board.create('group', p).setRotationCenter('centroid').setRotationPoints([p[2]]);
956  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
957  *  })();
958  * </script><pre>
959  *
960  *  @example
961  *
962  *  // Allow Translations:
963  *  // By default, every point of a group triggers a translation.
964  *  // There may be situations, when this is not wanted.
965  *
966  *  // In this example, E triggers nothing, but itself is rotation center
967  *  // and is translated, if other points are moved around.
968  *
969  *  var p, q, col, pol, g;
970  *  col = 'blue';
971  *  p = [];
972  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
973  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
974  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
975  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
976  *  q = board.create('point',[0, 0], {size: 5, strokeColor:col, fillColor:col});
977  *
978  *  pol = board.create('polygon', p, {hasInnerPoints: true});
979  *  g = board.create('group', p.concat(q)).setRotationCenter('centroid').setRotationPoints([p[2]]);
980  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
981  *  g.removeTranslationPoint(q);
982  *
983  * </pre><div class="jxgbox" id="JXGd19b800a-57a9-4303-b49a-8f5b7a5488f0" style="width: 400px; height: 300px;"></div>
984  * <script type="text/javascript">
985  *  (function () {
986  *  var board, p, q, col, pol, g;
987  *  board = JXG.JSXGraph.initBoard('JXGd19b800a-57a9-4303-b49a-8f5b7a5488f0', {boundingbox:[-5,5,5,-5], keepaspectratio:true, axis:true, showcopyright: false});
988  *  col = 'blue';
989  *  p = [];
990  *  p.push(board.create('point',[-2, -1 ], {size: 5, strokeColor:col, fillColor:col}));
991  *  p.push(board.create('point',[2, -1 ], {size: 5, strokeColor:'yellow', fillColor:'yellow'}));
992  *  p.push(board.create('point',[2, 1 ], {size: 5, strokeColor:'red', fillColor:'red'}));
993  *  p.push(board.create('point',[-2, 1], {size: 5, strokeColor:col, fillColor:col}));
994  *  q = board.create('point',[0, 0], {size: 5, strokeColor:col, fillColor:col});
995  *
996  *  pol = board.create('polygon', p, {hasInnerPoints: true});
997  *  g = board.create('group', p.concat(q)).setRotationCenter('centroid').setRotationPoints([p[2]]);
998  *  g.setScaleCenter(p[0]).setScalePoints(p[1]);
999  *  g.removeTranslationPoint(q);
1000  *  })();
1001  * </script><pre>
1002  *
1003  *  @example
1004  *
1005  *        // Add an image and use the group tools to manipulate it
1006  *       let urlImg = "https://jsxgraph.org/distrib/images/uccellino.jpg";
1007  *       let lowleft = [-2, -1]
1008  *
1009  *       let col = 'blue';
1010  *       let p = [];
1011  *       p.push(board.create('point', lowleft, { size: 5, strokeColor: col, fillColor: col }));
1012  *       p.push(board.create('point', [2, -1], { size: 5, strokeColor: 'yellow', fillColor: 'yellow', name: 'scale' }));
1013  *       p.push(board.create('point', [2, 1], { size: 5, strokeColor: 'red', fillColor: 'red', name: 'rotate' }));
1014  *       p.push(board.create('point', [-2, 1], { size: 5, strokeColor: col, fillColor: col, name: 'translate' }));
1015  *
1016  *       let im = board.create('image', [urlImg, lowleft, [2, 2]]);
1017  *       let pol = board.create('polygon', p, { hasInnerPoints: true });
1018  *
1019  *       let g = board.create('group', p.concat(im))
1020  *       // g.addPoint(im)   // image, but adds as a point
1021  *
1022  *       g.setRotationCenter(lowleft)
1023  *       g.setRotationPoints([p[2]]);
1024  *
1025  *       g.setScaleCenter(p[0]).setScalePoints(p[1]);
1026  *
1027  * </pre><div class="jxgbox" id="JXGd19b800a-57a9-4303-b49a-8f5b7a5489f1" style="width: 400px; height: 300px;"></div>
1028  * <script type="text/javascript">
1029  *  (function () {
1030  *       let board = JXG.JSXGraph.initBoard('JXGd19b800a-57a9-4303-b49a-8f5b7a5489f1')
1031  *
1032  *       // Add an image and use the group tools to manipulate it
1033  *       let urlImg = "https://jsxgraph.org/distrib/images/uccellino.jpg";
1034  *       let lowleft = [-2, -1]
1035  *
1036  *       let col = 'blue';
1037  *       let p = [];
1038  *       p.push(board.create('point', lowleft, { size: 5, strokeColor: col, fillColor: col }));
1039  *       p.push(board.create('point', [2, -1], { size: 5, strokeColor: 'yellow', fillColor: 'yellow', name: 'scale' }));
1040  *       p.push(board.create('point', [2, 1], { size: 5, strokeColor: 'red', fillColor: 'red', name: 'rotate' }));
1041  *       p.push(board.create('point', [-2, 1], { size: 5, strokeColor: col, fillColor: col, name: 'translate' }));
1042  *
1043  *       let im = board.create('image', [urlImg, lowleft, [2, 2]]);
1044  *       let pol = board.create('polygon', p, { hasInnerPoints: true });
1045  *
1046  *       let g = board.create('group', p.concat(im))
1047  *       // g.addPoint(im)   // image, but adds as a point
1048  *
1049  *       g.setRotationCenter(lowleft)
1050  *       g.setRotationPoints([p[2]]);
1051  *
1052  *       g.setScaleCenter(p[0]).setScalePoints(p[1]);
1053  *  })();
1054  * </script><pre>
1055  */
1056 JXG.createGroup = function (board, parents, attributes) {
1057     var attr = Type.copyAttributes(attributes, board.options, "group"),
1058         g = new JXG.Group(board, attr.id, attr.name, parents, attr);
1059 
1060     g.elType = "group";
1061     g.setParents(parents);
1062 
1063     return g;
1064 };
1065 
1066 JXG.registerElement("group", JXG.createGroup);
1067 
1068 export default JXG.Group;
1069 // export default {
1070 //     Group: JXG.Group,
1071 //     createGroup: JXG.createGroup
1072 // };
1073